1use core::{
7 convert::Infallible,
8 fmt::{self, Write},
9};
10
11use crate::{
12 Error as LinkageError, LinkageFixed, Rgb888,
13 bvh::{self, Motion},
14 linkage_file,
15 render::Projection,
16};
17use device_envoy_core::{
18 Error as CoreError,
19 button::Button,
20 cyd::{
21 CydDisplay,
22 display::{CydFrame, Image565Fixed, Orientation, tga},
23 },
24};
25use embassy_time::{Duration, Instant};
26use embedded_graphics::mono_font::{MonoFont, ascii::FONT_6X10};
27use embedded_graphics::prelude::Point;
28
29pub const ORIENTATION: Orientation = Orientation::Portrait;
34pub const TOP_FONT: MonoFont<'static> = FONT_6X10;
36
37pub const BACKGROUND_COLOR: Rgb888 = Rgb888::new(13, 13, 11);
42pub const FOREGROUND_COLOR: Rgb888 = Rgb888::new(255, 214, 123);
44
45linkage_file! {
47 pirouette {
48 file: "../assets/mocap/pirouette.lb.rs",
49 }
50}
51const STYLE: LinkageFixed<0, 0, 3> = LinkageFixed::start()
52 .pen_color(FOREGROUND_COLOR)
53 .pen_width(3.2);
54const LINKAGE: LinkageFixed<
55 { pirouette::DOF },
56 { pirouette::MARKS },
57 { STYLE.step_count() + pirouette::STEP_COUNT - 1 },
58> = STYLE.combine(pirouette::view());
59
60#[allow(long_running_const_eval)]
62const MOTION: Motion<{ pirouette::DOF }, 592> = bvh::motion!("../assets/mocap/pirouette.bvh");
64const MOTION_FPS: f32 = 120.0; const BACKGROUND_BITMAP: Image565Fixed<240, 320, { 240 * 320 }> =
68 tga!("../assets/ballet_background.tga").to_565();
69
70const PROJECTION: Projection = Projection::front_orthographic(
72 Point::new(84, 275), 1.4, );
75
76pub async fn run<CydDisplayDevice>(
80 display: &mut CydDisplayDevice,
81 button: &impl Button,
82) -> Result<Infallible, Error<CydDisplayDevice::Error>>
83where
84 CydDisplayDevice: CydDisplay,
85{
86 let mut last_sample_duration: Option<Duration> = None;
87 let mut boot_was_pressed = false;
88
89 loop {
91 for (sample_index, params) in MOTION.samples().enumerate() {
92 let boot_is_pressed = button.is_pressed();
93 if boot_is_pressed && !boot_was_pressed {
94 boot_was_pressed = true;
95 break;
96 }
97 boot_was_pressed = boot_is_pressed;
98 let started = Instant::now();
99
100 let mut cyd_frame = display.full_frame_mut();
102
103 BACKGROUND_BITMAP.copy_to(&mut cyd_frame)?;
106
107 for draw_item_3d in LINKAGE.view().draw_items_3d(¶ms)? {
109 draw_item_3d.project(&PROJECTION).draw(&mut cyd_frame);
110 }
111
112 let status = status_text(sample_index, last_sample_duration)?;
114
115 cyd_frame
117 .write_text(&status)
118 .flush()
119 .await
120 .map_err(Error::Flush)?;
121
122 last_sample_duration = Some(sample_duration(started));
123 }
124 }
125}
126
127#[derive(Debug, derive_more::From)]
129pub enum Error<FlushError> {
130 Linkage(LinkageError),
132 StatusText(fmt::Error),
134 Core(CoreError),
137 #[from(ignore)]
139 Flush(FlushError),
140}
141
142#[cfg(not(test))]
143fn sample_duration(started: Instant) -> Duration {
144 Instant::now() - started
145}
146
147#[cfg(test)]
148fn sample_duration(_started: Instant) -> Duration {
149 Duration::from_millis(1)
150}
151
152fn status_text(
153 sample_index: usize,
154 last_sample_duration: Option<Duration>,
155) -> Result<heapless::String<64>, fmt::Error> {
156 let mut status_text = heapless::String::<64>::new();
157
158 let Some(last_sample_duration) = last_sample_duration else {
159 return Ok(status_text);
161 };
162
163 let elapsed_secs = last_sample_duration.as_micros() as f32 * 1e-6_f32;
164 let fps = elapsed_secs.recip();
165 let slomo = MOTION_FPS / fps;
166
167 write!(
168 &mut status_text,
169 " #{:03}/{:03} | {:>4.1} fps | slomo {:>4.1}x",
170 sample_index + 1,
171 MOTION.sample_count(),
172 fps,
173 slomo,
174 )?;
175 Ok(status_text)
176}
177
178#[cfg(test)]
179mod tests {
180 use device_envoy_core::{
181 button::Button,
182 memory::{CydMemory, assert_framebuffer_matches_expected_png},
183 };
184 use embedded_graphics::{geometry::Point, image::GetPixel};
185 use futures_executor::block_on;
186
187 use super::{BACKGROUND_COLOR, FOREGROUND_COLOR, ORIENTATION, TOP_FONT, run};
188
189 fn render_ballet(memory_cyd: &mut CydMemory, button: &impl Button) {
190 let mut display = memory_cyd.display();
191 block_on(run(&mut display, button))
192 .expect_err("the free-running loop should stop at the frame budget");
193 }
194
195 #[test]
196 fn boot_restarts_the_motion_sequence_at_the_initial_frame() {
197 let mut baseline = CydMemory::new(
198 ORIENTATION.size(),
199 BACKGROUND_COLOR,
200 FOREGROUND_COLOR,
201 &TOP_FONT,
202 );
203 baseline.set_frame_budget(2);
204 let baseline_button = baseline.button_memory();
205 render_ballet(&mut baseline, &baseline_button);
206
207 let mut restarted = CydMemory::new(
208 ORIENTATION.size(),
209 BACKGROUND_COLOR,
210 FOREGROUND_COLOR,
211 &TOP_FONT,
212 );
213 restarted.set_frame_budget(4);
214 let mut restarted_button = restarted.button_memory();
215 restarted_button.set_pressed_for_frame(2, true);
216 render_ballet(&mut restarted, &restarted_button);
217
218 for position_y in 0..ORIENTATION.height() as usize {
219 for position_x in 0..ORIENTATION.width() as usize {
220 assert_eq!(
221 restarted.pixel(Point::new(position_x as i32, position_y as i32)),
222 baseline.pixel(Point::new(position_x as i32, position_y as i32)),
223 "restarted frame differs at ({position_x}, {position_y})",
224 );
225 }
226 }
227 }
228
229 #[test]
230 fn ballet_renders_expected_frame() {
231 const GOLDEN_TEST_FRAME_BUDGET: usize = 225;
232
233 let mut memory_cyd = CydMemory::new(
234 ORIENTATION.size(),
235 BACKGROUND_COLOR,
236 FOREGROUND_COLOR,
237 &TOP_FONT,
238 );
239 memory_cyd.set_frame_budget(GOLDEN_TEST_FRAME_BUDGET);
240 let memory_button = memory_cyd.button_memory();
241
242 let ballet_error = {
243 let mut display = memory_cyd.display();
244 block_on(run(&mut display, &memory_button))
245 }
246 .expect_err("the free-running loop should stop at the frame budget");
247 drop(ballet_error);
248
249 assert_framebuffer_matches_expected_png(
250 &memory_cyd,
251 env!("CARGO_MANIFEST_DIR"),
252 "ballet.png",
253 )
254 .expect("rendered frame should match the golden image");
255 }
256}