1use core::{array::from_fn, convert::Infallible, fmt};
7
8use crate::{Error as LinkageError, LinkageFixed, Rgb888, linkage_file, render::Projection};
9use device_envoy_core::{
10 UnwrapInfallible,
11 button::Button,
12 clock_sync::{ClockSync, h12_m_s},
13};
14use embassy_futures::select::{Either, select};
15use embedded_graphics::{
16 Drawable,
17 mono_font::{MonoFont, MonoTextStyle, ascii::FONT_7X13, ascii::FONT_10X20},
18 pixelcolor::Rgb565,
19 prelude::{DrawTarget, Point, Size},
20 primitives::Rectangle,
21 text::{Alignment, Baseline, Text, TextStyleBuilder},
22};
23use log::info;
24use time::OffsetDateTime;
25
26use device_envoy_core::cyd::{
27 CydDisplay,
28 display::{
29 CydFrame, DrawItem, Image565Fixed, Image888Fixed, MaskFixed, MaskedDrawable, Orientation,
30 mask_byte_count, tga, tiling::TileGrid,
31 },
32};
33
34pub const BACKGROUND_COLOR: Rgb888 = Rgb888::new(13, 13, 11);
38const FIGURE_COLOR: Rgb888 = Rgb888::new(255, 214, 123); pub const FOREGROUND_COLOR: Rgb888 = Rgb888::new(255, 214, 123);
41const PLACARD_TEXT_COLOR: Rgb888 = BACKGROUND_COLOR; linkage_file! {
47 pirouette {
48 file: "../assets/mocap/pirouette.lb.rs",
49 }
50}
51
52const STYLE: LinkageFixed<0, 0, 3> = LinkageFixed::start().pen_width(3.5).pen_color(FIGURE_COLOR);
54const CLOCK_PARAM_NAMES: &[&str] = &["head_yrotation", "l_shldr_zrotation", "r_shldr_zrotation"];
55const LINKAGE_WITH_STYLE: LinkageFixed<
56 { pirouette::DOF },
57 { pirouette::MARKS },
58 { STYLE.step_count() + pirouette::STEP_COUNT - 1 },
59> = STYLE.combine(pirouette::view());
60
61const LINKAGE: LinkageFixed<
64 { CLOCK_PARAM_NAMES.len() },
65 { LINKAGE_WITH_STYLE.mark_count() },
66 { LINKAGE_WITH_STYLE.step_count() },
67> = LINKAGE_WITH_STYLE
68 .freeze_param_name::<{ pirouette::DOF - 1 }>("l_shin_yrotation", 57.6)
69 .retain_param_names(CLOCK_PARAM_NAMES);
70
71const PROJECTION: Projection = Projection::front_orthographic(
74 Point::new(139, 306), 1.35, );
77
78const BACKGROUND_BITMAP: Image565Fixed<240, 320, { 240 * 320 }> =
82 tga!("../assets/clock_back.small.tga").to_565();
83
84const HOURS_SIGN_TGA: Image888Fixed<45, 73, { 45 * 73 }> = tga!("../assets/hours.small.tga");
85const HOURS_SIGN_BITMAP: Image565Fixed<45, 73, { 45 * 73 }> = HOURS_SIGN_TGA.to_565();
86const HOURS_SIGN_MASK: MaskFixed<45, 73, { mask_byte_count(45, 73) }> =
87 HOURS_SIGN_TGA.to_mask_magenta();
88const HOURS_SIGN_ANCHOR_X: f32 = 22.0;
89const HOURS_SIGN_VALUE_CENTER: Point = Point::new(22, 50);
90
91const MINUTE_SIGN_TGA: Image888Fixed<45, 77, { 45 * 77 }> = tga!("../assets/minute.small.tga");
92const MINUTE_SIGN_BITMAP: Image565Fixed<45, 77, { 45 * 77 }> = MINUTE_SIGN_TGA.to_565();
93const MINUTE_SIGN_MASK: MaskFixed<45, 77, { mask_byte_count(45, 77) }> =
94 MINUTE_SIGN_TGA.to_mask_magenta();
95const MINUTE_SIGN_ANCHOR_X: f32 = 22.0;
96const MINUTE_SIGN_VALUE_CENTER: Point = Point::new(22, 56);
97
98pub const ORIENTATION: Orientation = Orientation::Portrait;
102pub const TOP_FONT: MonoFont<'static> = FONT_7X13;
104pub const WIFI_STATUS_RECTANGLE: Rectangle = Rectangle::new(Point::new(6, 6), Size::new(155, 14));
106const TIME_RECTANGLE: Rectangle = Rectangle::new(
107 Point::new(
108 WIFI_STATUS_RECTANGLE.top_left.x + WIFI_STATUS_RECTANGLE.size.width as i32,
109 WIFI_STATUS_RECTANGLE.top_left.y,
110 ),
111 Size::new(
112 ORIENTATION.width() - WIFI_STATUS_RECTANGLE.size.width,
113 WIFI_STATUS_RECTANGLE.size.height,
114 ),
115);
116
117const FIGURE_Y: u32 = if WIFI_STATUS_RECTANGLE.top_left.y as u32 + WIFI_STATUS_RECTANGLE.size.height
119 > TIME_RECTANGLE.top_left.y as u32 + TIME_RECTANGLE.size.height
120{
121 WIFI_STATUS_RECTANGLE.top_left.y as u32 + WIFI_STATUS_RECTANGLE.size.height
122} else {
123 TIME_RECTANGLE.top_left.y as u32 + TIME_RECTANGLE.size.height
124};
125pub const FIGURE_TILE_GRID: TileGrid = TileGrid::new(
127 Rectangle::new(
128 Point::new(0, FIGURE_Y as i32),
129 Size::new(ORIENTATION.width(), ORIENTATION.height() - FIGURE_Y),
130 ),
131 3,
132 3,
133);
134pub async fn run<CydDisplayDevice, ClockSyncDevice>(
139 display: &mut CydDisplayDevice,
140 clock_sync: &ClockSyncDevice,
141 button: &mut impl Button,
142) -> Result<Exit, Error<CydDisplayDevice::Error>>
143where
144 CydDisplayDevice: CydDisplay,
145 ClockSyncDevice: ClockSync,
146{
147 loop {
148 let tick = match select(button.wait_for_press(), clock_sync.wait_for_tick()).await {
150 Either::First(()) => return Ok(Exit::ResetWifi),
151 Either::Second(tick) => tick,
152 };
153 let local_time = &tick.local_time;
154 let (hour_12, minute, _) = h12_m_s(local_time);
155 info!("tick {}", text_24h(local_time));
156
157 display
159 .frame_mut(TIME_RECTANGLE)
160 .write_text(&text_12h(local_time))
161 .flush()
162 .await
163 .map_err(Error::Flush)?;
164
165 let params = linkage_params(local_time);
168
169 let linkage = LINKAGE.view();
171 let mut draw_items_3d = linkage.draw_items_3d(¶ms)?;
172
173 let mut projected_items =
175 heapless::Vec::<_, { LINKAGE.view().draw_item_3d_count() }>::new();
176 for draw_item_3d in &mut draw_items_3d {
177 projected_items
178 .push(draw_item_3d.project(&PROJECTION))
179 .map_err(Error::VecOverflow)?;
180 }
181
182 let (hours_anchor_x, hours_anchor_y) = draw_items_3d
184 .pose_by_mark_name("lMid2")?
185 .project(&PROJECTION);
186 let (minute_anchor_x, minute_anchor_y) = draw_items_3d
187 .pose_by_mark_name("rMid2")?
188 .project(&PROJECTION);
189
190 let hours_top_left = Point::new(
192 (hours_anchor_x - HOURS_SIGN_ANCHOR_X) as i32,
193 hours_anchor_y as i32,
194 );
195 let minute_top_left = Point::new(
196 (minute_anchor_x - MINUTE_SIGN_ANCHOR_X) as i32,
197 minute_anchor_y as i32,
198 );
199
200 display
203 .for_each_tile(FIGURE_TILE_GRID, |tile| {
204 BACKGROUND_BITMAP.draw(tile).unwrap_infallible();
205
206 for projected_item in &projected_items {
208 projected_item.draw(tile);
209 }
210
211 HOURS_SIGN_BITMAP
213 .at(hours_top_left)
214 .draw_masked(&HOURS_SIGN_MASK, tile)
215 .unwrap_infallible();
216 draw_centered_sign_value(
217 tile,
218 hours_top_left,
219 HOURS_SIGN_VALUE_CENTER,
220 hour_12 as u32,
221 );
222
223 MINUTE_SIGN_BITMAP
225 .at(minute_top_left)
226 .draw_masked(&MINUTE_SIGN_MASK, tile)
227 .unwrap_infallible();
228 draw_centered_sign_value(
229 tile,
230 minute_top_left,
231 MINUTE_SIGN_VALUE_CENTER,
232 minute as u32,
233 );
234 })
235 .await
236 .map_err(Error::Flush)?;
237 }
238}
239
240pub async fn splash<CydDisplayDevice>(
247 display: &mut CydDisplayDevice,
248) -> Result<(), Error<CydDisplayDevice::Error>>
249where
250 CydDisplayDevice: CydDisplay,
251{
252 display
253 .frame_mut(WIFI_STATUS_RECTANGLE)
254 .write_text("WiFi: --")
255 .flush()
256 .await
257 .map_err(Error::Flush)?;
258
259 display
260 .frame_mut(TIME_RECTANGLE)
261 .write_text("--:--:-- --")
262 .flush()
263 .await
264 .map_err(Error::Flush)?;
265
266 display
267 .for_each_tile(FIGURE_TILE_GRID, |frame| {
268 BACKGROUND_BITMAP.draw(frame).unwrap_infallible();
269 })
270 .await
271 .map_err(Error::Flush)?;
272
273 Ok(())
274}
275
276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
278pub enum Exit {
279 ResetWifi,
281}
282
283#[derive(Debug, derive_more::From)]
290pub enum Error<FlushError> {
291 Linkage(LinkageError),
293 #[from(ignore)]
295 Flush(FlushError),
296 #[from(ignore)]
298 VecOverflow(DrawItem),
299}
300
301fn text_12h(local_time: &OffsetDateTime) -> heapless::String<24> {
307 let (hour_12, minute, second) = h12_m_s(local_time);
308 let suffix = if local_time.hour() % 24 < 12 {
309 "AM"
310 } else {
311 "PM"
312 };
313 let mut text = heapless::String::new();
317 fmt::write(
318 &mut text,
319 format_args!("{hour_12:>2}:{minute:02}:{second:02} {suffix}"),
320 )
321 .expect("clock string fits in 24 bytes");
322 text
323}
324
325fn text_24h(local_time: &OffsetDateTime) -> heapless::String<9> {
327 let mut text = heapless::String::new();
328 fmt::write(
329 &mut text,
330 format_args!(
331 "{:02}:{:02}:{:02}",
332 local_time.hour(),
333 local_time.minute(),
334 local_time.second()
335 ),
336 )
337 .expect("clock string fits in 9 bytes");
338 text
339}
340
341fn linkage_params(local_time: &OffsetDateTime) -> [f32; 3] {
342 const SECOND_INDEX: usize = 0;
347 const MINUTE_INDEX: usize = 1;
348 const HOUR_INDEX: usize = 2;
349
350 const SECOND_SPAN_TURNS: f32 = param_span_turns(SECOND_INDEX);
353 const MINUTE_SPAN_TURNS: f32 = param_span_turns(MINUTE_INDEX);
354 const HOUR_SPAN_TURNS: f32 = param_span_turns(HOUR_INDEX);
355
356 const _: () = {
358 assert_param_name(SECOND_INDEX, "head_yrotation");
359 assert_param_name(MINUTE_INDEX, "r_shldr_zrotation");
360 assert_param_name(HOUR_INDEX, "l_shldr_zrotation");
361 assert!(SECOND_SPAN_TURNS == 4.0);
362 assert!(MINUTE_SPAN_TURNS == 4.0);
363 assert!(HOUR_SPAN_TURNS == 4.0);
364 };
365
366 const HEAD_AT_12_PARAM: f32 = 0.5;
368 const RIGHT_ARM_AT_12_PARAM: f32 = 0.4375;
369 const LEFT_ARM_AT_12_PARAM: f32 = 0.5625;
370
371 let seconds_turn = local_time.second() as f32 / 60.0;
373 let minutes_turn = (local_time.minute() as f32 + seconds_turn) / 60.0;
374 let hours_turn = ((local_time.hour() % 12) as f32 + minutes_turn) / 12.0;
375
376 from_fn(|index| match index {
378 SECOND_INDEX => wrap_unit(HEAD_AT_12_PARAM + seconds_turn / SECOND_SPAN_TURNS),
379 MINUTE_INDEX => wrap_unit(RIGHT_ARM_AT_12_PARAM + minutes_turn / MINUTE_SPAN_TURNS),
380 HOUR_INDEX => wrap_unit(LEFT_ARM_AT_12_PARAM + hours_turn / HOUR_SPAN_TURNS),
381 _ => unreachable!(),
382 })
383}
384
385fn wrap_unit(value: f32) -> f32 {
386 let mut value = value;
387 while value >= 1.0 {
388 value -= 1.0;
389 }
390 while value < 0.0 {
391 value += 1.0;
392 }
393 value
394}
395
396const fn assert_param_name(index: usize, name: &str) {
398 assert!(str_eq(LINKAGE.view().param(index).name(), name));
399}
400
401const fn param_span_turns(index: usize) -> f32 {
404 use core::f32::consts::TAU;
405 let (low, high) = LINKAGE.view().scan_param_range(index);
406 (high - low) / TAU
407}
408
409const fn str_eq(left: &str, right: &str) -> bool {
411 let (left, right) = (left.as_bytes(), right.as_bytes());
412 if left.len() != right.len() {
413 return false;
414 }
415 let mut i = 0;
416 while i < left.len() {
417 if left[i] != right[i] {
418 return false;
419 }
420 i += 1;
421 }
422 true
423}
424
425fn draw_centered_text<D>(
434 target: &mut D,
435 text: &str,
436 center: Point,
437 font: &'static MonoFont<'static>,
438 color: Rgb565,
439) where
440 D: DrawTarget<Color = Rgb565, Error = Infallible>,
441{
442 let text_style = TextStyleBuilder::new()
443 .alignment(Alignment::Center)
444 .baseline(Baseline::Middle)
445 .build();
446 Text::with_text_style(text, center, MonoTextStyle::new(font, color), text_style)
447 .draw(target)
448 .unwrap_infallible();
449}
450
451fn draw_centered_sign_value<D>(
457 target: &mut D,
458 sign_top_left: Point,
459 value_center: Point,
460 number: u32,
461) where
462 D: DrawTarget<Color = Rgb565, Error = Infallible>,
463{
464 let mut value_text = heapless::String::<4>::new();
465 fmt::write(&mut value_text, format_args!("{:02}", number % 100))
466 .expect("two-digit sign value fits in 4 bytes");
467 draw_centered_text(
468 target,
469 &value_text,
470 sign_top_left + value_center,
471 &FONT_10X20,
472 Rgb565::from(PLACARD_TEXT_COLOR),
473 );
474}
475
476#[cfg(test)]
479mod tests {
480 use core::cell::Cell;
481
482 use device_envoy_core::button::{__ButtonMonitor, Button};
483 use device_envoy_core::clock_sync::{ClockSync, ClockSyncTick, UnixSeconds};
484 use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
485 use futures_executor::block_on;
486 use time::OffsetDateTime;
487
488 use super::{BACKGROUND_COLOR, Exit, FOREGROUND_COLOR, ORIENTATION, TOP_FONT, run};
489
490 struct FixedClockSync {
493 local_time: OffsetDateTime,
494 }
495
496 impl ClockSync for FixedClockSync {
497 async fn wait_for_tick(&self) -> ClockSyncTick {
498 ClockSyncTick {
499 local_time: self.local_time,
500 since_last_sync: embassy_time::Duration::from_secs(0),
501 }
502 }
503
504 fn now_local(&self) -> OffsetDateTime {
505 self.local_time
506 }
507
508 fn set_offset_minutes(&self, _minutes: i32) {}
509
510 fn offset_minutes(&self) -> i32 {
511 0
512 }
513
514 fn set_tick_interval(&self, _interval: Option<embassy_time::Duration>) {}
515
516 fn set_speed(&self, _speed_multiplier: f32) {}
517
518 fn set_utc_time(&self, _unix_seconds: UnixSeconds) {}
519 }
520
521 struct ImmediateButton;
522
523 impl __ButtonMonitor for ImmediateButton {
524 fn is_pressed_raw(&self) -> bool {
525 false
526 }
527
528 async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
529 }
530
531 impl Button for ImmediateButton {
532 async fn wait_for_press(&mut self) {}
533 }
534
535 #[test]
536 fn boot_requests_wifi_reset_before_rendering_the_next_tick() {
537 let memory_cyd = CydMemory::new(
538 ORIENTATION.size(),
539 BACKGROUND_COLOR,
540 FOREGROUND_COLOR,
541 &TOP_FONT,
542 );
543 let clock_sync = FixedClockSync {
544 local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
545 .expect("valid fixed timestamp"),
546 };
547 let mut button = ImmediateButton;
548
549 let result = {
550 let mut display = memory_cyd.display();
551 block_on(run(&mut display, &clock_sync, &mut button))
552 };
553
554 assert_eq!(
555 result.expect("BOOT should be a typed exit"),
556 Exit::ResetWifi
557 );
558 }
559
560 #[test]
561 fn boot_requests_wifi_reset_after_a_rendered_tick() {
562 let mut memory_cyd = CydMemory::new(
563 ORIENTATION.size(),
564 BACKGROUND_COLOR,
565 FOREGROUND_COLOR,
566 &TOP_FONT,
567 );
568 memory_cyd.set_frame_budget(100);
569 let clock_sync = OneTickClockSync {
570 local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
571 .expect("valid fixed timestamp"),
572 ticks: Cell::new(0),
573 };
574 let mut button = AfterTickButton {
575 waits: Cell::new(0),
576 };
577
578 let result = {
579 let mut display = memory_cyd.display();
580 block_on(run(&mut display, &clock_sync, &mut button))
581 };
582
583 assert_eq!(
584 result.expect("BOOT should exit after a rendered tick"),
585 Exit::ResetWifi
586 );
587 assert!(memory_cyd.flush_count() > 0);
588 }
589
590 const ONE_COMPLETE_FRAME_BUDGET: usize = 10;
593
594 #[test]
595 fn skeleton_clock_renders_expected_frame() {
596 let mut memory_cyd = CydMemory::new(
597 ORIENTATION.size(),
598 BACKGROUND_COLOR,
599 FOREGROUND_COLOR,
600 &TOP_FONT,
601 );
602 memory_cyd.set_frame_budget(ONE_COMPLETE_FRAME_BUDGET);
603 let clock_sync = FixedClockSync {
604 local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
605 .expect("valid fixed timestamp"),
606 };
607 let mut memory_button = NeverButton;
608
609 let skeleton_clock_result = {
610 let mut display = memory_cyd.display();
611 block_on(run(&mut display, &clock_sync, &mut memory_button))
612 };
613 skeleton_clock_result.expect_err("the free-running loop should stop at the frame budget");
614
615 assert_framebuffer_matches_expected_png(
616 &memory_cyd,
617 env!("CARGO_MANIFEST_DIR"),
618 "skeleton_clock.png",
619 )
620 .expect("rendered frame should match the golden image");
621 }
622
623 struct NeverButton;
624
625 impl __ButtonMonitor for NeverButton {
626 fn is_pressed_raw(&self) -> bool {
627 false
628 }
629
630 async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
631 }
632
633 impl Button for NeverButton {
634 async fn wait_for_press(&mut self) {
635 core::future::pending().await
636 }
637 }
638
639 struct AfterTickButton {
640 waits: Cell<u8>,
641 }
642
643 impl __ButtonMonitor for AfterTickButton {
644 fn is_pressed_raw(&self) -> bool {
645 false
646 }
647
648 async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
649 }
650
651 impl Button for AfterTickButton {
652 async fn wait_for_press(&mut self) {
653 let wait_number = self.waits.get();
654 self.waits.set(wait_number + 1);
655 if wait_number == 0 {
656 core::future::pending().await
657 }
658 }
659 }
660
661 struct OneTickClockSync {
662 local_time: OffsetDateTime,
663 ticks: Cell<u8>,
664 }
665
666 impl ClockSync for OneTickClockSync {
667 async fn wait_for_tick(&self) -> ClockSyncTick {
668 if self.ticks.replace(1) == 0 {
669 ClockSyncTick {
670 local_time: self.local_time,
671 since_last_sync: embassy_time::Duration::from_secs(0),
672 }
673 } else {
674 core::future::pending().await
675 }
676 }
677
678 fn now_local(&self) -> OffsetDateTime {
679 self.local_time
680 }
681
682 fn set_offset_minutes(&self, _minutes: i32) {}
683
684 fn offset_minutes(&self) -> i32 {
685 0
686 }
687
688 fn set_tick_interval(&self, _interval: Option<embassy_time::Duration>) {}
689
690 fn set_speed(&self, _speed_multiplier: f32) {}
691
692 fn set_utc_time(&self, _unix_seconds: UnixSeconds) {}
693 }
694}