1mod controls;
10mod reverse_kinematics;
11
12use core::convert::Infallible;
13
14use crate::{
15 Error as LinkageError, LinkageFixed, LinkageView, Rgb888, Step, linkage_file,
16 render::Projection,
17};
18use device_envoy_core::{
19 button::Button,
20 cyd::{
21 Cyd, CydDisplay, CydTouch,
22 display::{CydFrame, Orientation},
23 },
24};
25use embassy_time::Instant;
26use embedded_graphics::{geometry::Point, pixelcolor::WebColors};
27use nanorand::{Rng, WyRand};
28
29use crate::examples::ui::{Error as UiError, UiFrame, UiState};
30use controls::{
31 CALIBRATE_BUTTON, DISTANCE_LABEL, DOLLY_SLIDER, FPS_LABEL, NEXT_TARGET_BUTTON,
32 PARAM_SLIDER_COUNT, PARAM_SLIDERS, PREVIOUS_TARGET_BUTTON, RK_STEP_BUTTON, TARGET_LABEL,
33 TILT_SLIDER, VERSION_LABEL, VERSION_TEXT, XY_VIEW_SLIDER,
34};
35use reverse_kinematics::ReverseKinematics;
36
37pub const BACKGROUND_COLOR: Rgb888 = Rgb888::CSS_BLACK;
41pub const FOREGROUND_COLOR: Rgb888 = Rgb888::CSS_WHITE;
43
44linkage_file! {
53 camera_control {
54 file: "../../assets/examples/armatron/camera_control.lb.rs",
55 }
56}
57linkage_file! {
58 grid9x9 {
59 file: "../../assets/examples/armatron/grid_9x9.lb.rs",
60 }
61}
62linkage_file! {
63 armatron1 {
64 file: "../../assets/examples/armatron/armatron1.lb.rs",
65 }
66}
67const CAMERA_AND_GRID: LinkageFixed<
68 { camera_control::DOF + grid9x9::DOF },
69 { camera_control::MARKS + grid9x9::MARKS },
70 { camera_control::STEP_COUNT + grid9x9::STEP_COUNT - 1 },
71> = camera_control::fixed().combine(grid9x9::view());
72const ARMATRON_WITH_JOINTS: LinkageFixed<
73 { armatron1::DOF },
74 { armatron1::MARKS },
75 { joint_sphere_step_count(&armatron1::fixed()) },
76> = with_joint_spheres(armatron1::fixed(), 0.15);
77const SCENE_WITH_ARM: LinkageFixed<
78 { CAMERA_AND_GRID.dof() + ARMATRON_WITH_JOINTS.dof() }, { CAMERA_AND_GRID.mark_count() + ARMATRON_WITH_JOINTS.mark_count() }, { CAMERA_AND_GRID.step_count() + ARMATRON_WITH_JOINTS.step_count() },
82> = CAMERA_AND_GRID.combine(ARMATRON_WITH_JOINTS.view());
83const TARGET_SUFFIX_STEP_COUNT: usize = 2;
85const LINKAGE_FIXED: LinkageFixed<
86 { SCENE_WITH_ARM.dof() + armatron1::DOF },
87 { SCENE_WITH_ARM.mark_count() + armatron1::MARKS },
88 { SCENE_WITH_ARM.step_count() + armatron1::STEP_COUNT + TARGET_SUFFIX_STEP_COUNT },
89> = SCENE_WITH_ARM
90 .restore("scene origin")
91 .combine(armatron1::view())
92 .pen_color(Rgb888::CSS_RED)
93 .sphere_param("close hand", 0.5, 0.0);
94const LINKAGE: LinkageView<{ LINKAGE_FIXED.dof() }, { LINKAGE_FIXED.mark_count() }> =
95 LINKAGE_FIXED.view();
96const ARM_TIP_LINKAGE_FIXED: LinkageFixed<
98 { camera_control::DOF + armatron1::DOF },
99 { camera_control::MARKS + armatron1::MARKS },
100 { camera_control::STEP_COUNT + armatron1::STEP_COUNT - 1 },
101> = camera_control::fixed().combine(armatron1::view());
102const ARM_TIP_LINKAGE: LinkageView<
103 { ARM_TIP_LINKAGE_FIXED.dof() },
104 { ARM_TIP_LINKAGE_FIXED.mark_count() },
105> = ARM_TIP_LINKAGE_FIXED.view();
106
107const TARGET_PARAM_START: usize = SCENE_WITH_ARM.dof();
109const ORIENTATION: Orientation = Orientation::Landscape;
110
111const XY_VIEW_PARAM_INDEX: usize = LINKAGE.param_index(XY_VIEW_SLIDER.label(), 0);
112const TILT_PARAM_INDEX: usize = LINKAGE.param_index(TILT_SLIDER.label(), 0);
113const DOLLY_PARAM_INDEX: usize = LINKAGE.param_index(DOLLY_SLIDER.label(), 0);
114const ARM_PARAM_INDEXES: [usize; PARAM_SLIDER_COUNT] = {
116 let mut indexes = [0; PARAM_SLIDER_COUNT];
117 let mut slider_index = 0;
118 while slider_index < PARAM_SLIDER_COUNT {
119 indexes[slider_index] = LINKAGE.param_index(PARAM_SLIDERS[slider_index].label(), 0);
120 slider_index += 1;
121 }
122 indexes
123};
124pub const DOF: usize = LINKAGE.dof();
126
127const PROJECTION: Projection = Projection::front_perspective(
128 Point::new(
129 ORIENTATION.width() as i32 / 2,
130 ORIENTATION.height() as i32 / 2,
131 ),
132 ORIENTATION.width() as f32 / 16.0, 30.0,
134);
135
136pub async fn run<CydDevice, ButtonDevice>(
140 cyd: &mut CydDevice,
141 button: &mut ButtonDevice,
142) -> Result<Exit, Error<CydDevice::Error>>
143where
144 CydDevice: Cyd,
145 ButtonDevice: Button,
146{
147 let mut params = LINKAGE.param_defaults();
149 let mut target_seed: u8 = 0;
150 randomize_target_from_seed(target_seed, &mut params);
151
152 let mut ui_state = UiState::new();
154 let mut reverse_kinematics = ReverseKinematics::new();
155 let mut previous_tick = None;
156
157 loop {
158 if button.is_pressed() {
159 return Ok(Exit::CalibrationRequested);
160 }
161
162 let (display, touch) = cyd.parts();
163 let touch_event = touch.try_read().map_err(Error::Cyd)?;
164 let mut frame = display.full_frame_mut();
165 let current_tick = Instant::now();
166 frame.clear();
167
168 for draw_item_3d in LINKAGE.draw_items_3d(¶ms)? {
170 draw_item_3d.project(&PROJECTION).draw(&mut frame);
171 }
172
173 let mut ui_frame = UiFrame::new(&mut ui_state, touch_event, &mut frame);
174
175 ui_frame.slider(&TILT_SLIDER, &mut params[TILT_PARAM_INDEX])?;
176 ui_frame.slider(&DOLLY_SLIDER, &mut params[DOLLY_PARAM_INDEX])?;
177 ui_frame.slider(&XY_VIEW_SLIDER, &mut params[XY_VIEW_PARAM_INDEX])?;
178 for (param_slider, param_index) in PARAM_SLIDERS.iter().zip(ARM_PARAM_INDEXES) {
179 if ui_frame.slider(param_slider, &mut params[param_index])? {
180 reverse_kinematics.clear();
181 }
182 }
183
184 if ui_frame.button(&PREVIOUS_TARGET_BUTTON)? {
185 reverse_kinematics.clear();
186 target_seed = target_seed.wrapping_sub(1);
187 randomize_target_from_seed(target_seed, &mut params);
188 }
189 if ui_frame.button(&NEXT_TARGET_BUTTON)? {
190 reverse_kinematics.clear();
191 target_seed = target_seed.wrapping_add(1);
192 randomize_target_from_seed(target_seed, &mut params);
193 }
194 if ui_frame.icon_button(reverse_kinematics.run_button())? {
195 reverse_kinematics.toggle(¶ms)?;
196 }
197 let hold_button_state = ui_frame.hold_button(&RK_STEP_BUTTON)?;
198
199 if ui_frame.button(&CALIBRATE_BUTTON)? {
200 return Ok(Exit::CalibrationRequested);
201 }
202
203 let dt_seconds = previous_tick.map_or(0.0, |previous_tick| {
205 current_tick
206 .saturating_duration_since(previous_tick)
207 .as_micros() as f32
208 / 1_000_000.0
209 });
210 reverse_kinematics.hold_step(&mut params, hold_button_state, dt_seconds)?;
211 reverse_kinematics.tick(&mut params, dt_seconds)?;
212
213 ui_frame.label(&TARGET_LABEL, format_args!("target #{target_seed}"))?;
214 let distance_hundredths = target_distance_hundredths(¶ms)?;
215 ui_frame.label(
216 &DISTANCE_LABEL,
217 format_args!(
218 "distance {:02}.{:02}",
219 distance_hundredths / 100,
220 distance_hundredths % 100
221 ),
222 )?;
223 if let Some((fps_whole, fps_fraction)) =
224 previous_tick.and_then(|previous_tick| display_fps_since(previous_tick, current_tick))
225 {
226 ui_frame.label(
227 &FPS_LABEL,
228 format_args!("{fps_whole:>2}.{fps_fraction} fps"),
229 )?;
230 }
231 if !cfg!(test) {
232 ui_frame.label(&VERSION_LABEL, format_args!("{VERSION_TEXT}"))?;
233 }
234
235 ui_frame.draw_touch_cursor()?;
236
237 frame.flush().await.map_err(Error::Cyd)?;
238 previous_tick = Some(current_tick);
239 }
240}
241
242#[derive(Debug, derive_more::From)]
250pub enum Error<CydError> {
251 Linkage(LinkageError),
253 Ui(UiError<Infallible>),
255 #[from(ignore)]
257 Cyd(CydError),
258}
259
260#[derive(Debug)]
262pub enum Exit {
263 CalibrationRequested,
265}
266
267#[cfg(test)]
268mod tests {
269 use device_envoy_core::cyd::touch::TouchEvent;
270 use device_envoy_core::memory::{
271 CydMemory, Error as CydMemoryError, assert_framebuffer_matches_expected_png,
272 };
273 use embedded_graphics::mono_font::ascii::FONT_9X15_BOLD;
274 use futures_executor::block_on;
275
276 use super::controls::CALIBRATE_BUTTON;
277 use super::{Error, Exit, run};
278
279 fn test_memory_cyd() -> CydMemory {
280 CydMemory::new(
281 embedded_graphics::geometry::Size::new(320, 240),
282 super::BACKGROUND_COLOR,
283 super::FOREGROUND_COLOR,
284 &FONT_9X15_BOLD,
285 )
286 }
287
288 #[test]
289 fn tapping_the_calibrate_button_requests_calibration() -> Result<(), Error<CydMemoryError>> {
290 let mut memory_cyd = test_memory_cyd();
291 let touch_rectangle = CALIBRATE_BUTTON.touch_rectangle;
292 let touch_center = touch_rectangle.top_left
293 + embedded_graphics::geometry::Point::new(
294 touch_rectangle.size.width as i32 / 2,
295 touch_rectangle.size.height as i32 / 2,
296 );
297 memory_cyd.push_touch_event(TouchEvent::Down {
298 point: touch_center,
299 });
300 let mut memory_button = memory_cyd.button_memory();
301
302 let armatron_exit = block_on(run(&mut memory_cyd, &mut memory_button))?;
303
304 assert!(matches!(armatron_exit, Exit::CalibrationRequested));
305 assert_eq!(
306 memory_cyd.flush_count(),
307 0,
308 "the calibrate-button exit happens before the frame is flushed"
309 );
310 Ok(())
311 }
312
313 #[test]
314 fn boot_requests_calibration() -> Result<(), Error<CydMemoryError>> {
315 let mut memory_cyd = test_memory_cyd();
316 memory_cyd.set_frame_budget(1);
317 let mut memory_button = memory_cyd.button_memory();
318 memory_button.set_pressed_for_frame(0, true);
319
320 let armatron_exit = block_on(run(&mut memory_cyd, &mut memory_button))?;
321
322 assert!(matches!(armatron_exit, Exit::CalibrationRequested));
323 assert_eq!(memory_cyd.flush_count(), 0);
324 Ok(())
325 }
326
327 #[test]
328 fn armatron_renders_expected_frame() {
329 let mut memory_cyd = test_memory_cyd();
330 memory_cyd.set_frame_budget(1);
331 let mut memory_button = memory_cyd.button_memory();
332
333 let armatron_error = block_on(run(&mut memory_cyd, &mut memory_button))
334 .expect_err("the free-running loop should stop at the frame budget");
335 assert!(matches!(
336 armatron_error,
337 Error::Cyd(CydMemoryError::OutOfFrames)
338 ));
339
340 assert_framebuffer_matches_expected_png(
341 &memory_cyd,
342 env!("CARGO_MANIFEST_DIR"),
343 "armatron.png",
344 )
345 .expect("rendered frame should match the golden image");
346 }
347}
348
349const fn joint_sphere_step_count<const DOF: usize, const MARKS: usize, const N: usize>(
352 linkage: &LinkageFixed<DOF, MARKS, N>,
353) -> usize {
354 let mut count = linkage.len;
355 let mut step_index = 0;
356 while step_index < linkage.len {
357 if matches!(
358 linkage.steps[step_index],
359 Step::Forward(_) | Step::Left(_) | Step::Up(_)
360 ) {
361 count += 2;
362 }
363 step_index += 1;
364 }
365 count
366}
367
368const fn with_joint_spheres<
369 const DOF: usize,
370 const MARKS: usize,
371 const N: usize,
372 const N_OUT: usize,
373>(
374 linkage: LinkageFixed<DOF, MARKS, N>,
375 joint_radius: f32,
376) -> LinkageFixed<DOF, MARKS, N_OUT> {
377 let mut output = LinkageFixed {
378 steps: [const { Step::Start }; N_OUT],
379 len: 0,
380 params: linkage.params,
381 param_len: linkage.param_len,
382 mark_names: linkage.mark_names,
383 mark_len: linkage.mark_len,
384 };
385 let mut step_index = 0;
386 while step_index < linkage.len {
387 let step = linkage.steps[step_index];
388 let is_translation = matches!(step, Step::Forward(_) | Step::Left(_) | Step::Up(_));
389 if is_translation {
390 assert!(output.len < N_OUT, "joint-sphere output capacity too small");
391 output.steps[output.len] = Step::Sphere(joint_radius);
392 output.len += 1;
393 }
394 assert!(output.len < N_OUT, "joint-sphere output capacity too small");
395 output.steps[output.len] = step;
396 output.len += 1;
397 if is_translation {
398 assert!(output.len < N_OUT, "joint-sphere output capacity too small");
399 output.steps[output.len] = Step::Sphere(joint_radius);
400 output.len += 1;
401 }
402 step_index += 1;
403 }
404 output
405}
406
407fn randomize_target_from_seed(target_seed: u8, params: &mut [f32; DOF]) {
408 let mut rng = WyRand::new_seed(u64::from(target_seed));
409 for param in params[TARGET_PARAM_START..].iter_mut() {
410 *param = rng.generate::<u32>() as f32 / (u32::MAX as f32 + 1.0);
411 }
412}
413
414fn target_distance_hundredths(params: &[f32; DOF]) -> Result<u32, LinkageError> {
415 Ok(libm::roundf(target_distance(params)?.clamp(0.0, 99.99) * 100.0) as u32)
417}
418
419fn display_fps_since(previous_tick: Instant, current_tick: Instant) -> Option<(u32, u32)> {
420 let elapsed_micros = current_tick
421 .saturating_duration_since(previous_tick)
422 .as_micros();
423
424 (elapsed_micros != 0).then(|| {
425 let fps_tenths = 10_000_000_u64.saturating_add(elapsed_micros / 2) / elapsed_micros;
427 let fps_tenths = fps_tenths.min(999) as u32;
428 (fps_tenths / 10, fps_tenths % 10)
429 })
430}
431
432fn target_distance(params: &[f32; DOF]) -> Result<f32, LinkageError> {
433 let mut arm_params = [0.0f32; TARGET_PARAM_START];
434 arm_params.copy_from_slice(¶ms[..TARGET_PARAM_START]);
435 let arm_tip = ARM_TIP_LINKAGE.final_pose(&arm_params)?.position();
436 let target_center = LINKAGE.final_pose(params)?.position();
437 Ok(arm_tip.distance_to(target_center))
438}