1#[cfg(test)]
15use core::ops::Range;
16use core::{
17 cell::{Cell, RefCell},
18 convert::Infallible,
19 future::{Future, ready},
20};
21use std::{
22 fs,
23 io::BufWriter,
24 path::{Path, PathBuf},
25 process,
26 rc::Rc,
27 time::{SystemTime, UNIX_EPOCH},
28 vec::Vec,
29};
30
31#[cfg(test)]
32use crate::cyd::backend::TouchUncalibrated;
33#[cfg(test)]
34use crate::cyd::touch::flow::{MIN_SAMPLES_PER_POINT, SAMPLES_DISCARDED_AFTER_DOWN};
35use crate::cyd::{
36 Cyd, CydDisplay, CydTouch,
37 backend::{CalibrationConfig, RawTouchEvent},
38 display::{CydFrame, Orientation},
39 touch::TouchEvent,
40};
41#[cfg(test)]
42use crate::flash_block::{
43 Error as FlashBlockError, FlashBlock, FlashDevice, clear_block, load_block, save_block,
44};
45use crate::{
46 UnwrapInfallible,
47 button::{__ButtonMonitor, Button},
48 pixel_target::{PixelTarget, rgb888_from_rgb565},
49};
50use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
51use embedded_graphics::{
52 Drawable, Pixel,
53 mono_font::{MonoFont, MonoTextStyle, ascii::FONT_9X15_BOLD},
54 pixelcolor::{IntoStorage, Rgb565, raw::RawU16},
55 prelude::{Dimensions, DrawTarget, Point, Size},
56 primitives::Rectangle,
57 text::{Baseline, Text},
58};
59#[cfg(test)]
60use serde::{Deserialize, Serialize};
61
62const DEFAULT_FRAME_BUDGET: usize = 1000;
63#[cfg(test)]
64const FLASH_BLOCK_SIZE: usize = 4096;
65#[cfg(test)]
66const FLASH_BLOCK_OFFSET: u32 = 0;
67#[cfg(test)]
68const FLASH_ERASED_BYTE: u8 = 0xFF;
69
70const fn identity_calibration_config() -> CalibrationConfig {
71 CalibrationConfig::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
72}
73
74#[derive(Clone)]
75pub(crate) struct FrameClockMemory {
76 frame_index: Rc<Cell<usize>>,
77}
78
79impl FrameClockMemory {
80 #[must_use]
81 pub fn frame_index(&self) -> usize {
82 self.frame_index.get()
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum Error {
94 OutOfFrames,
97}
98#[cfg_attr(
99 feature = "doc-images",
100 doc = ::embed_doc_image::embed_image!("cyd_memory_bitmap", "docs/assets/cyd_memory_bitmap.png")
101)]
102pub struct CydMemory {
170 display: CydDisplayMemory,
171 touch: CydTouchMemory,
172 shared: Rc<RefCell<CydMemoryShared>>,
173 orientation: Orientation,
174}
175
176struct CydMemoryShared {
177 framebuffer: Vec<u16>,
178 flush_count: usize,
179 last_flush_rectangle: Option<Rectangle>,
180 frame_budget: usize,
181 raw_touch_script: FrameScript<RawTouchEvent>,
182 touch_script: FrameScript<TouchEvent>,
183 frame_clock: FrameClockMemory,
184}
185
186#[derive(Clone)]
188pub struct CydDisplayMemory {
189 size: Size,
190 background_color: Rgb888,
191 foreground_color: Rgb888,
192 background565: Rgb565,
193 foreground565: Rgb565,
194 font: &'static MonoFont<'static>,
195 shared: Rc<RefCell<CydMemoryShared>>,
196}
197
198#[derive(Clone)]
200pub struct CydTouchMemory {
201 shared: Rc<RefCell<CydMemoryShared>>,
202 calibration_config: CalibrationConfig,
203}
204
205#[cfg(test)]
207pub(crate) struct CydTouchUncalibratedMemory {
208 shared: Rc<RefCell<CydMemoryShared>>,
209}
210
211pub struct CydFrameMemory {
213 shared: Rc<RefCell<CydMemoryShared>>,
214 screen_size: Size,
215 rectangle: Rectangle,
216 background565: Rgb565,
217 foreground565: Rgb565,
218 font: &'static MonoFont<'static>,
219 pixels: Vec<u16>,
220}
221
222struct FrameScript<Event> {
223 current_frame: Vec<Event>,
224 future_frames: Vec<Vec<Event>>,
225 current_read_index: usize,
226}
227
228#[cfg(test)]
229pub(crate) struct FlashBlockMemory {
230 flash_device_memory: FlashDeviceMemory,
231 save_count: usize,
232}
233
234#[cfg(test)]
235struct FlashDeviceMemory {
236 bytes: [u8; FLASH_BLOCK_SIZE],
237}
238
239pub struct ButtonMemory {
252 pressed: bool,
253 pressed_frames: Vec<(usize, bool)>,
254 frame_clock: Option<FrameClockMemory>,
255}
256
257impl CydMemory {
258 #[must_use]
263 pub fn new(
264 size: Size,
265 background_color: Rgb888,
266 foreground_color: Rgb888,
267 font: &'static MonoFont<'static>,
268 ) -> Self {
269 let orientation = if size.width > size.height {
270 Orientation::Landscape
271 } else {
272 Orientation::Portrait
273 };
274 Self::new_inner(size, orientation, background_color, foreground_color, font)
275 }
276
277 #[must_use]
314 pub fn new_with_orientation(
315 orientation: Orientation,
316 background_color: Rgb888,
317 foreground_color: Rgb888,
318 font: &'static MonoFont<'static>,
319 ) -> Self {
320 Self::new_inner(
321 orientation.size(),
322 orientation,
323 background_color,
324 foreground_color,
325 font,
326 )
327 }
328
329 fn new_inner(
330 size: Size,
331 orientation: Orientation,
332 background_color: Rgb888,
333 foreground_color: Rgb888,
334 font: &'static MonoFont<'static>,
335 ) -> Self {
336 let background565 = Rgb565::from(background_color);
337 let pixel_count = size.width as usize * size.height as usize;
338 let shared = Rc::new(RefCell::new(CydMemoryShared {
339 framebuffer: vec![background565.into_storage(); pixel_count],
340 flush_count: 0,
341 last_flush_rectangle: None,
342 frame_budget: DEFAULT_FRAME_BUDGET,
343 raw_touch_script: FrameScript::default(),
344 touch_script: FrameScript::default(),
345 frame_clock: FrameClockMemory {
346 frame_index: Rc::new(Cell::new(0)),
347 },
348 }));
349 let display = CydDisplayMemory {
350 size,
351 background_color,
352 foreground_color,
353 background565,
354 foreground565: Rgb565::from(foreground_color),
355 font,
356 shared: shared.clone(),
357 };
358 let touch = CydTouchMemory {
359 shared: shared.clone(),
360 calibration_config: identity_calibration_config(),
361 };
362 Self {
363 display,
364 touch,
365 shared,
366 orientation,
367 }
368 }
369
370 #[must_use]
371 pub fn display(&self) -> CydDisplayMemory {
373 self.display.clone()
374 }
375
376 #[must_use]
378 pub fn owned_parts(&self) -> (CydDisplayMemory, CydTouchMemory) {
379 (self.display.clone(), self.touch.clone())
380 }
381
382 #[must_use]
383 #[cfg(test)]
384 pub(crate) fn parts_uncalibrated(&self) -> (CydDisplayMemory, CydTouchUncalibratedMemory) {
385 (
386 self.display.clone(),
387 CydTouchUncalibratedMemory {
388 shared: Rc::clone(&self.touch.shared),
389 },
390 )
391 }
392}
393
394impl Cyd for CydMemory {
395 type Error = Error;
396 type Display = CydDisplayMemory;
397 type Touch = CydTouchMemory;
398
399 fn parts(&mut self) -> (&mut Self::Display, &mut Self::Touch) {
400 (&mut self.display, &mut self.touch)
401 }
402
403 fn orientation(&self) -> Orientation {
404 self.orientation
405 }
406}
407
408impl CydMemory {
409 pub fn set_frame_budget(&mut self, frame_budget: usize) {
442 self.shared.borrow_mut().frame_budget = frame_budget;
443 }
444
445 #[must_use]
446 pub(crate) fn frame_clock(&self) -> FrameClockMemory {
447 self.shared.borrow().frame_clock.clone()
448 }
449
450 #[must_use]
455 pub fn button_memory(&self) -> ButtonMemory {
456 ButtonMemory::with_frame_clock(self.frame_clock())
457 }
458
459 #[cfg(test)]
460 pub(crate) fn script_raw_frames(&mut self, raw_touch_frames: &[&[RawTouchEvent]]) {
461 self.shared
462 .borrow_mut()
463 .raw_touch_script
464 .replace_frames(raw_touch_frames);
465 }
466
467 #[cfg(test)]
468 pub(crate) fn script_raw_frames_owned(&mut self, raw_touch_frames: Vec<Vec<RawTouchEvent>>) {
469 self.shared
470 .borrow_mut()
471 .raw_touch_script
472 .replace_owned_frames(raw_touch_frames);
473 }
474
475 #[cfg(test)]
476 pub(crate) fn push_raw_touch_event(&mut self, raw_touch_event: RawTouchEvent) {
477 self.shared
478 .borrow_mut()
479 .raw_touch_script
480 .push_current_frame_event(raw_touch_event);
481 }
482
483 pub fn push_touch_event(&mut self, touch_event: TouchEvent) {
488 self.shared
489 .borrow_mut()
490 .touch_script
491 .push_current_frame_event(touch_event);
492 }
493
494 #[must_use]
496 pub fn flush_count(&self) -> usize {
497 self.shared.borrow().flush_count
498 }
499
500 #[must_use]
502 pub fn last_flush_rectangle(&self) -> Option<Rectangle> {
503 self.shared.borrow().last_flush_rectangle
504 }
505
506 #[must_use]
508 pub fn pixel(&self, position_x: usize, position_y: usize) -> Rgb565 {
509 assert!(
510 position_x < self.display.size.width as usize,
511 "position_x must stay within the screen"
512 );
513 assert!(
514 position_y < self.display.size.height as usize,
515 "position_y must stay within the screen"
516 );
517 let stride = self.display.size.width as usize;
518 let shared = self.shared.borrow();
519 Rgb565::from(RawU16::new(
520 shared.framebuffer[position_y * stride + position_x],
521 ))
522 }
523
524 #[cfg(feature = "host")]
533 pub fn rotate_framebuffer_180(&self) {
534 let mut shared = self.shared.borrow_mut();
535 let width = self.display.size.width as usize;
536 let height = self.display.size.height as usize;
537 for row_index in 0..height / 2 {
538 let opposite_row_index = height - 1 - row_index;
539 for column_index in 0..width {
540 let first_index = row_index * width + column_index;
541 let second_index = opposite_row_index * width + (width - 1 - column_index);
542 shared.framebuffer.swap(first_index, second_index);
543 }
544 }
545 if height % 2 == 1 {
546 let row_start = (height / 2) * width;
547 let row_end = row_start + width;
548 shared.framebuffer[row_start..row_end].reverse();
549 }
550 }
551
552 pub(crate) fn write_framebuffer_png(
554 &self,
555 path: impl AsRef<Path>,
556 ) -> Result<(), Box<dyn std::error::Error>> {
557 let width = self.display.size.width;
558 let height = self.display.size.height;
559 let mut rgb_bytes = Vec::with_capacity(width as usize * height as usize * 3);
560 let shared = self.shared.borrow();
561 for pixel in &shared.framebuffer {
562 let color = rgb888_from_rgb565(*pixel);
563 rgb_bytes.push(color.r());
564 rgb_bytes.push(color.g());
565 rgb_bytes.push(color.b());
566 }
567
568 let path = path.as_ref();
569 if let Some(parent) = path.parent() {
570 fs::create_dir_all(parent)?;
571 }
572 let file = fs::File::create(path)?;
573 let writer = BufWriter::new(file);
574 let mut encoder = png::Encoder::new(writer, width, height);
575 encoder.set_color(png::ColorType::Rgb);
576 encoder.set_depth(png::BitDepth::Eight);
577 let mut png_writer = encoder.write_header()?;
578 png_writer.write_image_data(&rgb_bytes)?;
579 Ok(())
580 }
581}
582
583pub fn assert_framebuffer_matches_expected_png(
610 cyd_memory: &CydMemory,
611 manifest_dir: &str,
612 relative_filename: &str,
613) -> Result<(), Box<dyn std::error::Error>> {
614 if let Some(preview_output_path) = std::env::var_os("DEVICE_ENVOY_PREVIEW_OUTPUT_PATH") {
621 cyd_memory.write_framebuffer_png(preview_output_path)?;
622 }
623
624 let mut expected_path = PathBuf::from(manifest_dir);
625 expected_path.push("tests");
626 expected_path.push("assets");
627 expected_path.push(relative_filename);
628
629 if std::env::var_os("DEVICE_ENVOY_UPDATE_CYD_PNGS").is_some() {
630 cyd_memory.write_framebuffer_png(&expected_path)?;
631 std::println!("updated PNG at {}", expected_path.display());
632 return Ok(());
633 }
634
635 if !expected_path.exists() {
636 return Err(std::format!(
637 "expected PNG is missing at {}; rerun with DEVICE_ENVOY_UPDATE_CYD_PNGS=1 to create it",
638 expected_path.display()
639 )
640 .into());
641 }
642
643 let unix_nanos = SystemTime::now()
644 .duration_since(UNIX_EPOCH)
645 .map(|duration| duration.as_nanos())
646 .unwrap_or(0);
647 let temp_path = std::env::temp_dir().join(std::format!(
648 "{}-{}-{unix_nanos}",
649 relative_filename.replace('/', "_"),
650 process::id()
651 ));
652 cyd_memory.write_framebuffer_png(&temp_path)?;
653
654 let expected_bytes = fs::read(&expected_path)?;
655 let actual_bytes = fs::read(&temp_path)?;
656 if let Err(error) = fs::remove_file(&temp_path)
657 && error.kind() != std::io::ErrorKind::NotFound
658 {
659 return Err(error.into());
660 }
661
662 if expected_bytes != actual_bytes {
663 return Err(std::format!(
664 "PNG bytes differ from {}; rerun with DEVICE_ENVOY_UPDATE_CYD_PNGS=1 to accept the new image",
665 expected_path.display()
666 )
667 .into());
668 }
669 Ok(())
670}
671
672impl Default for CydMemory {
673 fn default() -> Self {
674 Self::new(
675 Size::new(320, 240),
676 Rgb888::BLACK,
677 Rgb888::WHITE,
678 &FONT_9X15_BOLD,
679 )
680 }
681}
682
683impl core::fmt::Debug for CydMemory {
684 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
685 formatter.debug_struct("CydMemory").finish_non_exhaustive()
686 }
687}
688
689impl core::fmt::Debug for CydTouchMemory {
690 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
691 formatter
692 .debug_struct("CydTouchMemory")
693 .field("calibration_config", &self.calibration_config)
694 .finish_non_exhaustive()
695 }
696}
697
698#[cfg(test)]
699impl core::fmt::Debug for CydTouchUncalibratedMemory {
700 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
701 formatter
702 .debug_struct("CydTouchUncalibratedMemory")
703 .finish_non_exhaustive()
704 }
705}
706
707#[cfg(test)]
708impl TouchUncalibrated for CydTouchUncalibratedMemory {
709 type Error = Error;
710 type Calibrated = CydTouchMemory;
711
712 fn read_raw_touch_event(&mut self) -> Result<Option<RawTouchEvent>, Self::Error> {
713 Ok(self
714 .shared
715 .borrow_mut()
716 .raw_touch_script
717 .pop_current_frame_event())
718 }
719
720 fn calibrate(
721 self,
722 calibration_config: CalibrationConfig,
723 _orientation: Orientation,
724 ) -> Self::Calibrated {
725 CydTouchMemory {
726 shared: self.shared,
727 calibration_config,
728 }
729 }
730}
731
732impl crate::cyd::backend::DisplayBackend for CydDisplayMemory {
733 type Error = Error;
734
735 type Frame<'a> = CydFrameMemory;
736
737 fn create_frame_mut(&mut self, rectangle: Rectangle) -> Self::Frame<'_> {
738 let pixel_count = rectangle.size.width as usize * rectangle.size.height as usize;
739 CydFrameMemory {
740 shared: self.shared.clone(),
741 screen_size: self.size,
742 rectangle,
743 background565: self.background565,
744 foreground565: self.foreground565,
745 font: self.font,
746 pixels: vec![self.background565.into_storage(); pixel_count],
747 }
748 }
749}
750
751impl CydDisplay for CydDisplayMemory {
752 fn screen_size(&self) -> Size {
753 self.size
754 }
755
756 fn background_color(&self) -> Rgb888 {
757 self.background_color
758 }
759
760 fn foreground_color(&self) -> Rgb888 {
761 self.foreground_color
762 }
763
764 fn background_565(&self) -> Rgb565 {
765 self.background565
766 }
767
768 fn foreground_565(&self) -> Rgb565 {
769 self.foreground565
770 }
771
772 fn fill_rectangle(&mut self, rectangle: Rectangle, color: Rgb565) -> Result<(), Self::Error> {
773 fill_rectangle_in_framebuffer(
774 &mut self.shared.borrow_mut().framebuffer,
775 self.size,
776 rectangle,
777 color.into_storage(),
778 );
779 Ok(())
780 }
781
782 fn fill_contiguous<I>(&mut self, rectangle: Rectangle, pixels: I) -> Result<(), Self::Error>
783 where
784 I: IntoIterator<Item = Rgb565>,
785 {
786 fill_contiguous_in_framebuffer(
787 &mut self.shared.borrow_mut().framebuffer,
788 self.size,
789 rectangle,
790 pixels.into_iter().map(IntoStorage::into_storage),
791 );
792 Ok(())
793 }
794}
795
796impl CydTouch for CydTouchMemory {
797 type Error = Error;
798
799 fn try_read(&mut self) -> Result<Option<TouchEvent>, Self::Error> {
800 Ok(self
801 .shared
802 .borrow_mut()
803 .touch_script
804 .pop_current_frame_event())
805 }
806}
807
808impl CydFrameMemory {
809 fn width(&self) -> usize {
810 self.rectangle.size.width as usize
811 }
812
813 fn height(&self) -> usize {
814 self.rectangle.size.height as usize
815 }
816
817 fn local_x(&self, position_x: i32) -> Option<usize> {
818 usize::try_from(position_x.checked_sub(self.rectangle.top_left.x)?).ok()
819 }
820
821 fn local_y(&self, position_y: i32) -> Option<usize> {
822 usize::try_from(position_y.checked_sub(self.rectangle.top_left.y)?).ok()
823 }
824
825 fn flush_now(&mut self) -> Result<(), Error> {
826 let mut shared = self.shared.borrow_mut();
827 if shared.flush_count >= shared.frame_budget {
828 return Err(Error::OutOfFrames);
829 }
830
831 blit_frame_to_screen(
832 &mut shared.framebuffer,
833 self.screen_size,
834 self.rectangle,
835 &self.pixels,
836 );
837 shared.last_flush_rectangle = Some(self.rectangle);
838 shared.flush_count += 1;
839 shared.raw_touch_script.advance_frame();
840 shared.touch_script.advance_frame();
841 shared
842 .frame_clock
843 .frame_index
844 .set(shared.frame_clock.frame_index.get() + 1);
845 Ok(())
846 }
847}
848
849impl DrawTarget for CydFrameMemory {
850 type Color = Rgb565;
851 type Error = Infallible;
852
853 fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
854 self.fill(color);
855 Ok(())
856 }
857
858 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
859 where
860 I: IntoIterator<Item = Pixel<Self::Color>>,
861 {
862 for Pixel(point, color) in pixels {
863 let Some(local_x) = self.local_x(point.x) else {
864 continue;
865 };
866 let Some(local_y) = self.local_y(point.y) else {
867 continue;
868 };
869 if local_x >= self.width() || local_y >= self.height() {
870 continue;
871 }
872 let stride = self.width();
873 self.pixels[local_y * stride + local_x] = color.into_storage();
874 }
875 Ok(())
876 }
877}
878
879impl Dimensions for CydFrameMemory {
880 fn bounding_box(&self) -> Rectangle {
881 self.rectangle
882 }
883}
884
885impl PixelTarget for CydFrameMemory {
886 fn width(&self) -> usize {
887 usize::try_from(self.rectangle.top_left.x)
888 .expect("frame top-left x must be non-negative")
889 .checked_add(self.width())
890 .expect("frame width must fit in usize")
891 }
892
893 fn height(&self) -> usize {
894 usize::try_from(self.rectangle.top_left.y)
895 .expect("frame top-left y must be non-negative")
896 .checked_add(self.height())
897 .expect("frame height must fit in usize")
898 }
899
900 fn put_pixel(&mut self, x: usize, y: usize, color: Rgb888) {
901 self.put_pixel_565(x, y, Rgb565::from(color).into_storage());
902 }
903
904 fn put_pixel_565(&mut self, x: usize, y: usize, rgb565: u16) {
905 let Some(local_x) = self.local_x(x as i32) else {
906 return;
907 };
908 let Some(local_y) = self.local_y(y as i32) else {
909 return;
910 };
911 if local_x >= self.width() || local_y >= self.height() {
912 return;
913 }
914 let stride = self.width();
915 self.pixels[local_y * stride + local_x] = rgb565;
916 }
917}
918
919impl CydFrame for CydFrameMemory {
920 type Error = Error;
921
922 fn rectangle(&self) -> Rectangle {
923 self.rectangle
924 }
925
926 fn fill(&mut self, color: Rgb565) -> &mut Self {
927 self.pixels.fill(color.into_storage());
928 self
929 }
930
931 fn clear(&mut self) -> &mut Self {
932 self.fill(self.background565)
933 }
934
935 fn write_text(&mut self, text: &str) -> &mut Self {
936 Text::with_baseline(
937 text,
938 self.rectangle.top_left,
939 MonoTextStyle::new(self.font, self.foreground565),
940 Baseline::Top,
941 )
942 .draw(self)
943 .unwrap_infallible();
944 self
945 }
946
947 fn copy_from_565(&mut self, src: &[u16]) -> crate::Result<()> {
948 if self.pixels.len() != src.len() {
949 return Err(crate::Error::CopySize {
950 src_len: src.len(),
951 frame_len: self.pixels.len(),
952 });
953 }
954 self.pixels.copy_from_slice(src);
955 Ok(())
956 }
957
958 fn flush(&mut self) -> impl Future<Output = Result<(), <Self as CydFrame>::Error>> {
959 ready(self.flush_now())
960 }
961}
962
963impl<Event> Default for FrameScript<Event> {
964 fn default() -> Self {
965 Self {
966 current_frame: Vec::new(),
967 future_frames: Vec::new(),
968 current_read_index: 0,
969 }
970 }
971}
972
973impl<Event: Clone> FrameScript<Event> {
974 #[cfg(test)]
975 fn replace_frames(&mut self, frames: &[&[Event]]) {
976 self.current_frame.clear();
977 self.future_frames.clear();
978 self.current_read_index = 0;
979 if let Some((first_frame, remaining_frames)) = frames.split_first() {
980 self.current_frame = first_frame.to_vec();
981 self.future_frames = remaining_frames
982 .iter()
983 .map(|frame| frame.to_vec())
984 .collect();
985 }
986 }
987
988 #[cfg(test)]
989 fn replace_owned_frames(&mut self, mut frames: Vec<Vec<Event>>) {
990 self.current_frame.clear();
991 self.future_frames.clear();
992 self.current_read_index = 0;
993 if frames.is_empty() {
994 return;
995 }
996 self.current_frame = frames.remove(0);
997 self.future_frames = frames;
998 }
999
1000 fn push_current_frame_event(&mut self, event: Event) {
1001 self.current_frame.push(event);
1002 }
1003
1004 fn pop_current_frame_event(&mut self) -> Option<Event> {
1005 let event = self.current_frame.get(self.current_read_index).cloned();
1006 if event.is_some() {
1007 self.current_read_index += 1;
1008 }
1009 event
1010 }
1011
1012 fn advance_frame(&mut self) {
1013 if self.current_read_index >= self.current_frame.len() {
1014 if let Some(next_frame) = self.future_frames.first().cloned() {
1015 self.current_frame = next_frame;
1016 self.future_frames.remove(0);
1017 } else {
1018 self.current_frame.clear();
1019 }
1020 self.current_read_index = 0;
1021 return;
1022 }
1023
1024 self.current_frame.drain(0..self.current_read_index);
1025 self.current_read_index = 0;
1026 }
1027}
1028
1029#[cfg(test)]
1030impl FlashBlockMemory {
1031 #[must_use]
1032 pub fn new() -> Self {
1033 Self {
1034 flash_device_memory: FlashDeviceMemory::new(),
1035 save_count: 0,
1036 }
1037 }
1038
1039 #[must_use]
1040 pub fn with_value<T>(value: &T) -> Self
1041 where
1042 T: Serialize + for<'de> Deserialize<'de>,
1043 {
1044 let mut flash_block_memory = Self::new();
1045 flash_block_memory
1046 .save(value)
1047 .expect("saving a small in-memory flash value should succeed");
1048 flash_block_memory
1049 }
1050
1051 #[must_use]
1052 pub fn with_raw_bytes(bytes: &[u8]) -> Self {
1053 let mut flash_block_memory = Self::new();
1054 flash_block_memory
1055 .flash_device_memory
1056 .write_raw_bytes(bytes);
1057 flash_block_memory
1058 }
1059
1060 #[must_use]
1061 pub fn save_count(&self) -> usize {
1062 self.save_count
1063 }
1064}
1065
1066#[cfg(test)]
1067impl Default for FlashBlockMemory {
1068 fn default() -> Self {
1069 Self::new()
1070 }
1071}
1072
1073#[cfg(test)]
1074impl FlashBlock for FlashBlockMemory {
1075 type Error = FlashBlockError<Infallible>;
1076
1077 fn load<T>(&mut self) -> Result<Option<T>, Self::Error>
1078 where
1079 T: Serialize + for<'de> Deserialize<'de>,
1080 {
1081 match load_block::<FLASH_BLOCK_SIZE, T, _>(
1082 &mut self.flash_device_memory,
1083 FLASH_BLOCK_OFFSET,
1084 ) {
1085 Ok(value) => Ok(value),
1086 Err(FlashBlockError::StorageCorrupted | FlashBlockError::FormatError) => Ok(None),
1087 Err(FlashBlockError::Io(infallible)) => match infallible {},
1088 }
1089 }
1090
1091 fn save<T>(&mut self, value: &T) -> Result<(), Self::Error>
1092 where
1093 T: Serialize + for<'de> Deserialize<'de>,
1094 {
1095 save_block::<FLASH_BLOCK_SIZE, _, _>(
1096 &mut self.flash_device_memory,
1097 FLASH_BLOCK_OFFSET,
1098 value,
1099 )?;
1100 self.save_count += 1;
1101 Ok(())
1102 }
1103
1104 fn clear(&mut self) -> Result<(), Self::Error> {
1105 clear_block::<FLASH_BLOCK_SIZE, _>(&mut self.flash_device_memory, FLASH_BLOCK_OFFSET)
1106 }
1107}
1108
1109#[cfg(test)]
1110impl FlashDeviceMemory {
1111 fn new() -> Self {
1114 Self {
1115 bytes: [FLASH_ERASED_BYTE; FLASH_BLOCK_SIZE],
1116 }
1117 }
1118
1119 fn checked_range(&self, offset: u32, len: usize) -> Range<usize> {
1120 let start = usize::try_from(offset).expect("flash offset must fit in usize");
1121 let end = start
1122 .checked_add(len)
1123 .expect("flash range must fit in usize");
1124 assert!(
1125 end <= FLASH_BLOCK_SIZE,
1126 "flash range must stay in the block"
1127 );
1128 start..end
1129 }
1130
1131 fn write_raw_bytes(&mut self, bytes: &[u8]) {
1132 self.bytes.fill(FLASH_ERASED_BYTE);
1133 let len = bytes.len().min(FLASH_BLOCK_SIZE);
1134 self.bytes[..len].copy_from_slice(&bytes[..len]);
1135 }
1136}
1137
1138#[cfg(test)]
1139impl FlashDevice for FlashDeviceMemory {
1140 type Error = Infallible;
1141
1142 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
1143 let checked_range = self.checked_range(offset, bytes.len());
1144 bytes.copy_from_slice(&self.bytes[checked_range]);
1145 Ok(())
1146 }
1147
1148 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
1149 let checked_range = self.checked_range(offset, bytes.len());
1150 self.bytes[checked_range].copy_from_slice(bytes);
1151 Ok(())
1152 }
1153
1154 fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
1155 let len = usize::try_from(to.saturating_sub(from)).expect("flash erase length fits usize");
1156 let checked_range = self.checked_range(from, len);
1157 self.bytes[checked_range].fill(FLASH_ERASED_BYTE);
1158 Ok(())
1159 }
1160}
1161
1162impl ButtonMemory {
1163 #[must_use]
1166 pub fn new() -> Self {
1167 Self {
1168 pressed: false,
1169 pressed_frames: Vec::new(),
1170 frame_clock: None,
1171 }
1172 }
1173
1174 #[must_use]
1175 pub(crate) fn with_frame_clock(frame_clock: FrameClockMemory) -> Self {
1176 Self {
1177 pressed: false,
1178 pressed_frames: Vec::new(),
1179 frame_clock: Some(frame_clock),
1180 }
1181 }
1182
1183 pub fn set_pressed(&mut self, pressed: bool) {
1186 self.pressed = pressed;
1187 }
1188
1189 pub fn set_pressed_for_frame(&mut self, frame_index: usize, pressed: bool) {
1194 if let Some(existing_state) = self
1195 .pressed_frames
1196 .iter_mut()
1197 .find(|(existing_frame_index, _pressed_state)| *existing_frame_index == frame_index)
1198 {
1199 existing_state.1 = pressed;
1200 return;
1201 }
1202 self.pressed_frames.push((frame_index, pressed));
1203 }
1204
1205 fn current_pressed_state(&self) -> bool {
1206 let Some(frame_clock) = &self.frame_clock else {
1207 return self.pressed;
1208 };
1209 let frame_index = frame_clock.frame_index();
1210 self.pressed_frames
1211 .iter()
1212 .find_map(|(pressed_frame_index, pressed)| {
1213 (*pressed_frame_index == frame_index).then_some(*pressed)
1214 })
1215 .unwrap_or(self.pressed)
1216 }
1217}
1218
1219impl Default for ButtonMemory {
1220 fn default() -> Self {
1221 Self::new()
1222 }
1223}
1224
1225impl __ButtonMonitor for ButtonMemory {
1226 fn is_pressed_raw(&self) -> bool {
1227 self.current_pressed_state()
1228 }
1229
1230 async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
1231}
1232
1233impl Button for ButtonMemory {}
1234
1235fn fill_rectangle_in_framebuffer(
1236 framebuffer: &mut [u16],
1237 screen_size: Size,
1238 rectangle: Rectangle,
1239 color: u16,
1240) {
1241 let clipped_rectangle = rectangle.intersection(&Rectangle::new(Point::zero(), screen_size));
1242 if clipped_rectangle.size.width == 0 || clipped_rectangle.size.height == 0 {
1243 return;
1244 }
1245 let stride = screen_size.width as usize;
1246 for position_y in clipped_rectangle.top_left.y
1247 ..clipped_rectangle.top_left.y + clipped_rectangle.size.height as i32
1248 {
1249 for position_x in clipped_rectangle.top_left.x
1250 ..clipped_rectangle.top_left.x + clipped_rectangle.size.width as i32
1251 {
1252 let index = position_y as usize * stride + position_x as usize;
1253 framebuffer[index] = color;
1254 }
1255 }
1256}
1257
1258fn fill_contiguous_in_framebuffer<I>(
1259 framebuffer: &mut [u16],
1260 screen_size: Size,
1261 rectangle: Rectangle,
1262 pixels: I,
1263) where
1264 I: IntoIterator<Item = u16>,
1265{
1266 if rectangle.size.width == 0 || rectangle.size.height == 0 {
1267 return;
1268 }
1269 let stride = screen_size.width as usize;
1270 for (pixel_index, pixel) in pixels.into_iter().enumerate() {
1271 let local_x = pixel_index % rectangle.size.width as usize;
1272 let local_y = pixel_index / rectangle.size.width as usize;
1273 if local_y >= rectangle.size.height as usize {
1274 break;
1275 }
1276 let position_x = rectangle.top_left.x + local_x as i32;
1277 let position_y = rectangle.top_left.y + local_y as i32;
1278 if position_x < 0
1279 || position_y < 0
1280 || position_x >= screen_size.width as i32
1281 || position_y >= screen_size.height as i32
1282 {
1283 continue;
1284 }
1285 framebuffer[position_y as usize * stride + position_x as usize] = pixel;
1286 }
1287}
1288
1289fn blit_frame_to_screen(
1290 framebuffer: &mut [u16],
1291 screen_size: Size,
1292 rectangle: Rectangle,
1293 pixels: &[u16],
1294) {
1295 fill_contiguous_in_framebuffer(framebuffer, screen_size, rectangle, pixels.iter().copied());
1296}
1297
1298#[cfg(test)]
1299mod tests {
1300 use super::{
1301 ButtonMemory, CydMemory, CydTouchMemory, Error, FlashBlockMemory, MIN_SAMPLES_PER_POINT,
1302 SAMPLES_DISCARDED_AFTER_DOWN,
1303 };
1304 use crate::cyd::touch::driver::{
1305 CAPTURE_ACK_FRAME_COUNT, MAX_RAW_EVENTS_PER_FRAME, REJECTED_FRAME_COUNT,
1306 VERIFY_TIMEOUT_FRAMES,
1307 };
1308 use crate::cyd::{
1309 Cyd, CydDisplay, CydTouch,
1310 backend::{
1311 CalibrationConfig, Error as CalibrationError, RawTouchEvent, TouchUncalibrated,
1312 ensure_calibration,
1313 },
1314 display::{CydFrame, Orientation},
1315 touch::{
1316 RawPoint, TouchEvent,
1317 calibration::{
1318 CalibrationCorner, VERIFY_HIT_RADIUS_PIXELS, calibration_corner_center,
1319 calibration_verify_target_center, distort_demo_screen_to_raw,
1320 },
1321 },
1322 };
1323 use crate::flash_block::FlashBlock;
1324 use embedded_graphics::{
1325 Pixel,
1326 mono_font::ascii::FONT_9X15_BOLD,
1327 pixelcolor::{IntoStorage, Rgb565, Rgb888, WebColors},
1328 prelude::{DrawTarget, Point, Size},
1329 primitives::Rectangle,
1330 };
1331 use futures_executor::block_on;
1332 use serde::{Deserialize, Serialize};
1333
1334 #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1335 struct DemoValue {
1336 count: u16,
1337 }
1338
1339 fn test_cyd_memory() -> CydMemory {
1340 CydMemory::new(
1341 Size::new(320, 240),
1342 Rgb888::CSS_BLACK,
1343 Rgb888::CSS_WHITE,
1344 &FONT_9X15_BOLD,
1345 )
1346 }
1347
1348 #[test]
1349 fn orientation_is_preserved_by_oriented_memory() {
1350 for orientation in [
1351 crate::cyd::display::Orientation::Landscape,
1352 crate::cyd::display::Orientation::Portrait,
1353 crate::cyd::display::Orientation::LandscapeInverted,
1354 crate::cyd::display::Orientation::PortraitInverted,
1355 ] {
1356 let memory_cyd = CydMemory::new_with_orientation(
1357 orientation,
1358 Rgb888::CSS_BLACK,
1359 Rgb888::CSS_WHITE,
1360 &FONT_9X15_BOLD,
1361 );
1362 assert_eq!(memory_cyd.orientation(), orientation);
1363 }
1364 }
1365
1366 fn read_next_raw_touch_event(memory_cyd: &CydMemory) -> Result<Option<RawTouchEvent>, Error> {
1367 let (_display, mut touch) = memory_cyd.parts_uncalibrated();
1368 touch.read_raw_touch_event()
1369 }
1370
1371 fn run_ensure_calibration(
1372 memory_cyd: &CydMemory,
1373 memory_flash_block: &mut FlashBlockMemory,
1374 memory_button: &mut ButtonMemory,
1375 confirmed_message: Option<&str>,
1376 ) -> Result<CydTouchMemory, CalibrationError<Error, <FlashBlockMemory as FlashBlock>::Error>>
1377 {
1378 let (mut display, touch) = memory_cyd.parts_uncalibrated();
1379 block_on(ensure_calibration(
1380 &mut display,
1381 touch,
1382 memory_flash_block,
1383 memory_button,
1384 confirmed_message,
1385 memory_cyd.orientation(),
1386 ))
1387 }
1388
1389 #[test]
1390 fn fresh_frame_starts_cleared_to_background() {
1391 let memory_cyd = test_cyd_memory();
1392 let mut display = memory_cyd.display();
1393 let frame = display.frame_mut(Rectangle::new(Point::new(3, 4), Size::new(2, 2)));
1394 assert_eq!(frame.pixels, &[Rgb565::CSS_BLACK.into_storage(); 4]);
1395 }
1396
1397 #[test]
1398 fn short_fill_contiguous_iterator_changes_only_supplied_pixels() {
1399 let memory_cyd = test_cyd_memory();
1400 let rectangle = Rectangle::new(Point::new(2, 3), Size::new(2, 2));
1401 {
1402 let mut display = memory_cyd.display();
1403 display
1404 .fill_contiguous(rectangle, [Rgb565::CSS_RED, Rgb565::CSS_GREEN])
1405 .expect("memory streaming should succeed");
1406 }
1407
1408 assert_eq!(memory_cyd.pixel(2, 3), Rgb565::CSS_RED);
1409 assert_eq!(memory_cyd.pixel(3, 3), Rgb565::CSS_GREEN);
1410 assert_eq!(memory_cyd.pixel(2, 4), Rgb565::CSS_BLACK);
1411 assert_eq!(memory_cyd.pixel(3, 4), Rgb565::CSS_BLACK);
1412 }
1413
1414 #[test]
1415 fn overlong_fill_contiguous_iterator_ignores_pixels_beyond_rectangle() {
1416 let memory_cyd = test_cyd_memory();
1417 let rectangle = Rectangle::new(Point::new(2, 3), Size::new(2, 2));
1418 {
1419 let mut display = memory_cyd.display();
1420 display
1421 .fill_contiguous(
1422 rectangle,
1423 [
1424 Rgb565::CSS_RED,
1425 Rgb565::CSS_GREEN,
1426 Rgb565::CSS_BLUE,
1427 Rgb565::CSS_WHITE,
1428 Rgb565::CSS_YELLOW,
1429 ],
1430 )
1431 .expect("memory streaming should succeed");
1432 }
1433
1434 assert_eq!(memory_cyd.pixel(2, 3), Rgb565::CSS_RED);
1435 assert_eq!(memory_cyd.pixel(3, 3), Rgb565::CSS_GREEN);
1436 assert_eq!(memory_cyd.pixel(2, 4), Rgb565::CSS_BLUE);
1437 assert_eq!(memory_cyd.pixel(3, 4), Rgb565::CSS_WHITE);
1438 assert_eq!(memory_cyd.pixel(4, 3), Rgb565::CSS_BLACK);
1439 }
1440
1441 #[test]
1442 fn draw_target_pixel_flushes_to_screen_coordinate() {
1443 let memory_cyd = test_cyd_memory();
1444 {
1445 let mut display = memory_cyd.display();
1446 let mut frame = display.frame_mut(Rectangle::new(Point::new(10, 20), Size::new(4, 3)));
1447 frame
1448 .draw_iter([Pixel(Point::new(11, 21), Rgb565::CSS_RED)])
1449 .expect("drawing into memory frame should succeed");
1450 block_on(frame.flush()).expect("flush should succeed");
1451 }
1452 assert_eq!(memory_cyd.pixel(11, 21), Rgb565::CSS_RED);
1453 assert_eq!(
1454 memory_cyd.last_flush_rectangle(),
1455 Some(Rectangle::new(Point::new(10, 20), Size::new(4, 3)))
1456 );
1457 }
1458
1459 #[test]
1460 fn fill_rectangle_clips_to_screen_edges() {
1461 let memory_cyd = CydMemory::new(
1462 Size::new(4, 4),
1463 Rgb888::CSS_BLACK,
1464 Rgb888::CSS_WHITE,
1465 &FONT_9X15_BOLD,
1466 );
1467 {
1468 let mut display = memory_cyd.display();
1469 display
1470 .fill_rectangle(
1471 Rectangle::new(Point::new(-1, -1), Size::new(3, 3)),
1472 Rgb565::CSS_GREEN,
1473 )
1474 .expect("fill_rectangle should succeed");
1475 display
1476 .fill_rectangle(
1477 Rectangle::new(Point::new(10, 10), Size::new(2, 2)),
1478 Rgb565::CSS_RED,
1479 )
1480 .expect("off-screen fill_rectangle should stay a no-op");
1481 }
1482 assert_eq!(memory_cyd.pixel(0, 0), Rgb565::CSS_GREEN);
1483 assert_eq!(memory_cyd.pixel(1, 1), Rgb565::CSS_GREEN);
1484 assert_eq!(memory_cyd.pixel(3, 3), Rgb565::CSS_BLACK);
1485 }
1486
1487 #[test]
1488 fn raw_touch_frames_drain_then_advance_after_flush() {
1489 let mut memory_cyd = test_cyd_memory();
1490 let first_frame = [
1491 RawTouchEvent::Down { raw_x: 1, raw_y: 2 },
1492 RawTouchEvent::Up,
1493 ];
1494 let second_frame = [RawTouchEvent::Down { raw_x: 3, raw_y: 4 }];
1495 memory_cyd.script_raw_frames(&[&first_frame, &second_frame]);
1496
1497 assert_eq!(
1498 read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1499 Some(RawTouchEvent::Down { raw_x: 1, raw_y: 2 })
1500 );
1501 assert_eq!(
1502 read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1503 Some(RawTouchEvent::Up)
1504 );
1505 assert_eq!(
1506 read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1507 None
1508 );
1509
1510 {
1511 let mut display = memory_cyd.display();
1512 let mut frame = display.full_frame_mut();
1513 block_on(frame.flush()).expect("flush should succeed");
1514 }
1515
1516 assert_eq!(memory_cyd.flush_count(), 1);
1517 assert_eq!(
1518 read_next_raw_touch_event(&memory_cyd).expect("read should succeed"),
1519 Some(RawTouchEvent::Down { raw_x: 3, raw_y: 4 })
1520 );
1521 }
1522
1523 #[test]
1524 fn flush_budget_returns_out_of_frames() {
1525 let mut memory_cyd = test_cyd_memory();
1526 memory_cyd.set_frame_budget(1);
1527 {
1528 let mut display = memory_cyd.display();
1529 let mut frame = display.full_frame_mut();
1530 block_on(frame.flush()).expect("first flush should succeed");
1531 }
1532 {
1533 let mut display = memory_cyd.display();
1534 let mut frame = display.full_frame_mut();
1535 let error = block_on(frame.flush()).expect_err("second flush should hit frame budget");
1536 assert_eq!(error, Error::OutOfFrames);
1537 }
1538 assert_eq!(memory_cyd.flush_count(), 1);
1539 }
1540
1541 #[test]
1542 fn memory_flash_block_round_trips_and_handles_corruption() {
1543 let mut memory_flash_block = FlashBlockMemory::new();
1544 memory_flash_block
1545 .save(&DemoValue { count: 7 })
1546 .expect("save should succeed");
1547 assert_eq!(
1548 memory_flash_block
1549 .load::<DemoValue>()
1550 .expect("load should succeed"),
1551 Some(DemoValue { count: 7 })
1552 );
1553
1554 let mut corrupt_flash_block = FlashBlockMemory::with_raw_bytes(&[1, 2, 3, 4]);
1555 assert_eq!(
1556 corrupt_flash_block
1557 .load::<DemoValue>()
1558 .expect("corrupt load should degrade to None"),
1559 None
1560 );
1561
1562 memory_flash_block.clear().expect("clear should succeed");
1563 assert_eq!(
1564 memory_flash_block
1565 .load::<DemoValue>()
1566 .expect("load should succeed"),
1567 None
1568 );
1569 }
1570
1571 #[test]
1572 fn ensure_calibration_happy_path_saves_predictable_config() {
1573 let mut memory_cyd = test_cyd_memory();
1574 let mut memory_flash_block = FlashBlockMemory::new();
1575 let mut memory_button = memory_cyd.button_memory();
1576 let raw_points = script_happy_path(&mut memory_cyd);
1577
1578 let _touch = run_ensure_calibration(
1579 &memory_cyd,
1580 &mut memory_flash_block,
1581 &mut memory_button,
1582 Some("saved"),
1583 )
1584 .expect("happy-path calibration should succeed");
1585
1586 assert_eq!(memory_flash_block.save_count(), 1);
1587
1588 let saved_config = memory_flash_block
1589 .load::<CalibrationConfig>()
1590 .expect("saved config should deserialize")
1591 .expect("saved config should exist");
1592
1593 for (raw_point, calibration_corner) in raw_points.into_iter().zip([
1594 CalibrationCorner::UpperLeft,
1595 CalibrationCorner::UpperRight,
1596 CalibrationCorner::LowerRight,
1597 CalibrationCorner::LowerLeft,
1598 ]) {
1599 let expected_screen_point = calibration_corner_center(calibration_corner);
1600 let (mapped_x, mapped_y) = saved_config.map_raw_to_screen(raw_point.x, raw_point.y);
1601 assert!(
1602 (mapped_x - expected_screen_point.x as f32).abs() <= 1.0,
1603 "mapped_x={mapped_x} expected_x={}",
1604 expected_screen_point.x
1605 );
1606 assert!(
1607 (mapped_y - expected_screen_point.y as f32).abs() <= 1.0,
1608 "mapped_y={mapped_y} expected_y={}",
1609 expected_screen_point.y
1610 );
1611 }
1612 assert!(memory_cyd.flush_count() > 0);
1613 assert_eq!(
1617 memory_cyd.last_flush_rectangle(),
1618 Some(Rectangle::new(Point::new(0, 220), Size::new(320, 20)))
1619 );
1620 }
1621
1622 #[test]
1623 fn ensure_calibration_uses_preloaded_flash_without_flushing() {
1624 let mut memory_cyd = test_cyd_memory();
1625 let saved_config = CalibrationConfig::new(1.0, 0.0, 2.0, 0.0, 1.0, 3.0);
1626 memory_cyd.push_raw_touch_event(RawTouchEvent::Down { raw_x: 7, raw_y: 9 });
1627 let mut memory_flash_block = FlashBlockMemory::with_value(&saved_config);
1628 let mut memory_button = memory_cyd.button_memory();
1629
1630 let _touch = run_ensure_calibration(
1631 &memory_cyd,
1632 &mut memory_flash_block,
1633 &mut memory_button,
1634 None,
1635 )
1636 .expect("preloaded calibration should load");
1637
1638 assert_eq!(memory_cyd.flush_count(), 0);
1639 let (_display, mut touch) = memory_cyd.parts_uncalibrated();
1640 assert_eq!(
1641 touch
1642 .read_raw_touch_event()
1643 .expect("touch read should succeed"),
1644 Some(RawTouchEvent::Down { raw_x: 7, raw_y: 9 })
1645 );
1646 }
1647
1648 #[test]
1649 fn preloaded_calibration_preserves_already_oriented_memory_events() {
1650 for orientation in [
1651 Orientation::Landscape,
1652 Orientation::Portrait,
1653 Orientation::LandscapeInverted,
1654 Orientation::PortraitInverted,
1655 ] {
1656 let mut memory_cyd = CydMemory::new_with_orientation(
1657 orientation,
1658 Rgb888::CSS_BLACK,
1659 Rgb888::CSS_WHITE,
1660 &FONT_9X15_BOLD,
1661 );
1662 let point = Point::new(
1663 orientation.width() as i32 - 1,
1664 orientation.height() as i32 - 1,
1665 );
1666 memory_cyd.push_touch_event(TouchEvent::Down { point });
1667 let saved_config = super::identity_calibration_config();
1668 let mut memory_flash_block = FlashBlockMemory::with_value(&saved_config);
1669 let mut memory_button = memory_cyd.button_memory();
1670 let mut touch = run_ensure_calibration(
1671 &memory_cyd,
1672 &mut memory_flash_block,
1673 &mut memory_button,
1674 None,
1675 )
1676 .expect("preloaded calibration should load");
1677
1678 assert!(matches!(
1679 touch.try_read(),
1680 Ok(Some(TouchEvent::Down { point: actual_point }))
1681 if actual_point == point
1682 ));
1683 }
1684 }
1685
1686 #[test]
1687 fn ensure_calibration_corrupt_flash_reruns_and_overwrites() {
1688 let mut memory_cyd = test_cyd_memory();
1689 let mut memory_flash_block = FlashBlockMemory::with_raw_bytes(&[1, 2, 3, 4]);
1690 let mut memory_button = memory_cyd.button_memory();
1691 script_happy_path(&mut memory_cyd);
1692
1693 let _touch = run_ensure_calibration(
1694 &memory_cyd,
1695 &mut memory_flash_block,
1696 &mut memory_button,
1697 None,
1698 )
1699 .expect("corrupt flash should fall back to calibration");
1700
1701 assert_eq!(memory_flash_block.save_count(), 1);
1702 assert!(
1703 memory_flash_block
1704 .load::<CalibrationConfig>()
1705 .expect("load should succeed")
1706 .is_some()
1707 );
1708 }
1709
1710 #[test]
1711 fn ensure_calibration_paces_with_one_flush_per_iteration() {
1712 let mut memory_cyd = test_cyd_memory();
1713 memory_cyd.set_frame_budget(3);
1714 let mut memory_flash_block = FlashBlockMemory::new();
1715 let mut memory_button = memory_cyd.button_memory();
1716
1717 let error = run_ensure_calibration(
1718 &memory_cyd,
1719 &mut memory_flash_block,
1720 &mut memory_button,
1721 None,
1722 )
1723 .expect_err("empty input should stop at the frame budget");
1724
1725 assert!(matches!(
1726 error,
1727 CalibrationError::Device(Error::OutOfFrames)
1728 ));
1729 assert_eq!(memory_cyd.flush_count(), 3);
1730 }
1731
1732 #[test]
1733 fn ensure_calibration_drains_a_full_tap_in_one_frame() {
1734 let mut memory_cyd = test_cyd_memory();
1735 memory_cyd.set_frame_budget(1);
1736 let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1737 memory_cyd.script_raw_frames_owned(vec![tap_events(upper_left_raw_point)]);
1738 let mut memory_flash_block = FlashBlockMemory::new();
1739 let mut memory_button = memory_cyd.button_memory();
1740
1741 let error = run_ensure_calibration(
1742 &memory_cyd,
1743 &mut memory_flash_block,
1744 &mut memory_button,
1745 None,
1746 )
1747 .expect_err("single-frame budget should stop after the first drawn frame");
1748
1749 assert!(matches!(
1750 error,
1751 CalibrationError::Device(Error::OutOfFrames)
1752 ));
1753 let upper_left_center = calibration_corner_center(CalibrationCorner::UpperLeft);
1754 let upper_right_center = calibration_corner_center(CalibrationCorner::UpperRight);
1755 assert_eq!(
1756 memory_cyd.pixel(upper_left_center.x as usize, upper_left_center.y as usize),
1757 Rgb565::CSS_WHITE
1758 );
1759 assert_eq!(
1760 memory_cyd.pixel(upper_right_center.x as usize, upper_right_center.y as usize),
1761 Rgb565::CSS_WHITE
1762 );
1763 assert_eq!(memory_cyd.pixel(160, 120), Rgb565::CSS_BLACK);
1764 }
1765
1766 #[test]
1767 fn ensure_calibration_verify_timeout_restarts_and_then_succeeds() {
1768 let mut memory_cyd = test_cyd_memory();
1769 let mut frames = happy_path_frames();
1770 frames.truncate(frames.len() - 1);
1771 frames.extend((0..verify_timeout_extra_idle_frames()).map(|_| Vec::new()));
1772 frames.extend((0..rejected_restart_idle_frames()).map(|_| Vec::new()));
1773 frames.extend(happy_path_frames());
1774 memory_cyd.script_raw_frames_owned(frames);
1775
1776 let mut memory_flash_block = FlashBlockMemory::new();
1777 let mut memory_button = memory_cyd.button_memory();
1778
1779 let _touch = run_ensure_calibration(
1780 &memory_cyd,
1781 &mut memory_flash_block,
1782 &mut memory_button,
1783 None,
1784 )
1785 .expect("flow should restart after verify timeout and then save");
1786
1787 assert_eq!(memory_flash_block.save_count(), 1);
1788 }
1789
1790 #[test]
1791 fn ensure_calibration_dropout_does_not_leak_corner_two_into_corner_three() {
1792 let mut memory_cyd = test_cyd_memory();
1793 let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1794 let upper_right_raw_point = raw_point_for_corner(CalibrationCorner::UpperRight);
1795 let lower_right_raw_point = raw_point_for_corner(CalibrationCorner::LowerRight);
1796 let lower_left_raw_point = raw_point_for_corner(CalibrationCorner::LowerLeft);
1797 let verify_raw_point = raw_point_for_verify_target();
1798 let mut frames = vec![tap_events(upper_left_raw_point)];
1799 append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1800 frames.push(dropout_tap_events(upper_right_raw_point));
1801 append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1802 frames.push(tap_events(lower_right_raw_point));
1803 append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1804 frames.push(tap_events(lower_left_raw_point));
1805 frames.push(tap_events(verify_raw_point));
1806 memory_cyd.script_raw_frames_owned(frames);
1807
1808 let mut memory_flash_block = FlashBlockMemory::new();
1809 let mut memory_button = memory_cyd.button_memory();
1810 let _touch = run_ensure_calibration(
1811 &memory_cyd,
1812 &mut memory_flash_block,
1813 &mut memory_button,
1814 None,
1815 )
1816 .expect("dropout sequence should still save a calibration");
1817
1818 let calibration_config = memory_flash_block
1819 .load::<CalibrationConfig>()
1820 .unwrap()
1821 .unwrap();
1822 assert_maps_near_corner(
1823 calibration_config,
1824 lower_right_raw_point,
1825 CalibrationCorner::LowerRight,
1826 );
1827 }
1828
1829 #[test]
1830 fn ensure_calibration_lift_off_drift_keeps_captured_point_near_stable_raw_point() {
1831 let mut memory_cyd = test_cyd_memory();
1832 let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1833 let drifted_raw_point = RawPoint {
1834 x: upper_left_raw_point.x + 400,
1835 y: upper_left_raw_point.y + 400,
1836 };
1837 let upper_right_raw_point = raw_point_for_corner(CalibrationCorner::UpperRight);
1838 let lower_right_raw_point = raw_point_for_corner(CalibrationCorner::LowerRight);
1839 let lower_left_raw_point = raw_point_for_corner(CalibrationCorner::LowerLeft);
1840 let verify_raw_point = raw_point_for_verify_target();
1841 let mut frames = vec![long_press_with_lift_off_drift_frame(
1842 upper_left_raw_point,
1843 drifted_raw_point,
1844 )];
1845 append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
1846 frames.extend(calibration_attempt_frames(&[
1847 upper_right_raw_point,
1848 lower_right_raw_point,
1849 lower_left_raw_point,
1850 verify_raw_point,
1851 ]));
1852 memory_cyd.script_raw_frames_owned(frames);
1853
1854 let mut memory_flash_block = FlashBlockMemory::new();
1855 let mut memory_button = memory_cyd.button_memory();
1856 let _touch = run_ensure_calibration(
1857 &memory_cyd,
1858 &mut memory_flash_block,
1859 &mut memory_button,
1860 None,
1861 )
1862 .expect("lift-off drift sequence should still save a calibration");
1863
1864 let calibration_config = memory_flash_block
1865 .load::<CalibrationConfig>()
1866 .unwrap()
1867 .unwrap();
1868 assert_maps_near_corner(
1869 calibration_config,
1870 upper_left_raw_point,
1871 CalibrationCorner::UpperLeft,
1872 );
1873 }
1874
1875 #[test]
1876 fn ensure_calibration_rejected_solve_restarts_and_then_saves_honest_script() {
1877 let mut memory_cyd = test_cyd_memory();
1878 let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1879 let lower_right_raw_point = raw_point_for_corner(CalibrationCorner::LowerRight);
1880 let lower_left_raw_point = raw_point_for_corner(CalibrationCorner::LowerLeft);
1881 let mut frames = calibration_attempt_frames(&[
1882 upper_left_raw_point,
1883 upper_left_raw_point,
1884 lower_right_raw_point,
1885 lower_left_raw_point,
1886 raw_point_for_verify_target(),
1887 ]);
1888 append_idle_frames(&mut frames, rejected_restart_idle_frames());
1889 frames.extend(happy_path_frames());
1890 memory_cyd.script_raw_frames_owned(frames);
1891
1892 let mut memory_flash_block = FlashBlockMemory::new();
1893 let mut memory_button = memory_cyd.button_memory();
1894 let _touch = run_ensure_calibration(
1895 &memory_cyd,
1896 &mut memory_flash_block,
1897 &mut memory_button,
1898 None,
1899 )
1900 .expect("rejected solve should restart and then save");
1901
1902 let calibration_config = memory_flash_block
1903 .load::<CalibrationConfig>()
1904 .unwrap()
1905 .unwrap();
1906 assert_eq!(memory_flash_block.save_count(), 1);
1907 assert_maps_near_corner(
1908 calibration_config,
1909 raw_point_for_corner(CalibrationCorner::UpperRight),
1910 CalibrationCorner::UpperRight,
1911 );
1912 }
1913
1914 #[test]
1915 fn ensure_calibration_verify_miss_restarts_without_saving_candidate() {
1916 let mut memory_cyd = test_cyd_memory();
1917 let verify_target_center = calibration_verify_target_center();
1918 let verify_miss_screen_x =
1919 verify_target_center.x + VERIFY_HIT_RADIUS_PIXELS.ceil() as i32 + 10;
1920 let verify_miss_raw_point =
1921 distort_demo_screen_to_raw(verify_miss_screen_x as f32, verify_target_center.y as f32);
1922 let mut frames = calibration_attempt_frames(&[
1923 raw_point_for_corner(CalibrationCorner::UpperLeft),
1924 raw_point_for_corner(CalibrationCorner::UpperRight),
1925 raw_point_for_corner(CalibrationCorner::LowerRight),
1926 raw_point_for_corner(CalibrationCorner::LowerLeft),
1927 verify_miss_raw_point,
1928 ]);
1929 append_idle_frames(&mut frames, rejected_restart_idle_frames());
1930 frames.extend(happy_path_frames());
1931 memory_cyd.script_raw_frames_owned(frames);
1932
1933 let mut memory_flash_block = FlashBlockMemory::new();
1934 let mut memory_button = memory_cyd.button_memory();
1935 let _touch = run_ensure_calibration(
1936 &memory_cyd,
1937 &mut memory_flash_block,
1938 &mut memory_button,
1939 None,
1940 )
1941 .expect("verify miss should restart and then save");
1942
1943 assert_eq!(memory_flash_block.save_count(), 1);
1944 }
1945
1946 #[test]
1947 fn ensure_calibration_recalibration_button_restarts_mid_flow() {
1948 let mut memory_cyd = test_cyd_memory();
1949 let mut frames = vec![tap_events(raw_point_for_corner(
1950 CalibrationCorner::UpperLeft,
1951 ))];
1952 append_idle_frames(&mut frames, 2);
1953 frames.extend(happy_path_frames());
1954 memory_cyd.script_raw_frames_owned(frames);
1955
1956 let mut memory_flash_block = FlashBlockMemory::new();
1957 let mut memory_button = memory_cyd.button_memory();
1958 memory_button.set_pressed_for_frame(2, true);
1959 let _touch = run_ensure_calibration(
1960 &memory_cyd,
1961 &mut memory_flash_block,
1962 &mut memory_button,
1963 None,
1964 )
1965 .expect("button-triggered recalibration should restart and then save");
1966
1967 let calibration_config = memory_flash_block
1968 .load::<CalibrationConfig>()
1969 .unwrap()
1970 .unwrap();
1971 assert_eq!(memory_flash_block.save_count(), 1);
1972 assert_maps_near_corner(
1973 calibration_config,
1974 raw_point_for_corner(CalibrationCorner::UpperLeft),
1975 CalibrationCorner::UpperLeft,
1976 );
1977 }
1978
1979 #[test]
1980 fn ensure_calibration_drain_cap_flushes_and_preserves_leftovers_during_hold() {
1981 let mut memory_cyd = test_cyd_memory();
1982 memory_cyd.set_frame_budget(2);
1983 let upper_left_raw_point = raw_point_for_corner(CalibrationCorner::UpperLeft);
1984 let mut oversized_hold_frame = Vec::new();
1985 oversized_hold_frame.push(RawTouchEvent::Down {
1986 raw_x: upper_left_raw_point.x,
1987 raw_y: upper_left_raw_point.y,
1988 });
1989 for _raw_event_index in 0..MAX_RAW_EVENTS_PER_FRAME.saturating_sub(1) {
1990 oversized_hold_frame.push(RawTouchEvent::Move {
1991 raw_x: upper_left_raw_point.x,
1992 raw_y: upper_left_raw_point.y,
1993 });
1994 }
1995 oversized_hold_frame.push(RawTouchEvent::Up);
1996 memory_cyd.script_raw_frames_owned(vec![oversized_hold_frame]);
1997
1998 let mut memory_flash_block = FlashBlockMemory::new();
1999 let mut memory_button = memory_cyd.button_memory();
2000 let error = run_ensure_calibration(
2001 &memory_cyd,
2002 &mut memory_flash_block,
2003 &mut memory_button,
2004 None,
2005 )
2006 .expect_err("oversized hold should stop at the frame budget");
2007
2008 assert!(matches!(
2009 error,
2010 CalibrationError::Device(Error::OutOfFrames)
2011 ));
2012 assert_eq!(memory_cyd.flush_count(), 2);
2013 let upper_left_center = calibration_corner_center(CalibrationCorner::UpperLeft);
2014 let upper_right_center = calibration_corner_center(CalibrationCorner::UpperRight);
2015 assert_eq!(
2016 memory_cyd.pixel(upper_left_center.x as usize, upper_left_center.y as usize),
2017 Rgb565::CSS_WHITE
2018 );
2019 assert_eq!(
2020 memory_cyd.pixel(upper_right_center.x as usize, upper_right_center.y as usize),
2021 Rgb565::CSS_WHITE
2022 );
2023 assert_eq!(
2024 read_next_raw_touch_event(&memory_cyd)
2025 .expect("the oversized frame should be fully drained by the second iteration"),
2026 None
2027 );
2028 }
2029
2030 fn script_happy_path(memory_cyd: &mut CydMemory) -> [RawPoint; 4] {
2031 memory_cyd.script_raw_frames_owned(happy_path_frames());
2032 [
2033 raw_point_for_corner(CalibrationCorner::UpperLeft),
2034 raw_point_for_corner(CalibrationCorner::UpperRight),
2035 raw_point_for_corner(CalibrationCorner::LowerRight),
2036 raw_point_for_corner(CalibrationCorner::LowerLeft),
2037 ]
2038 }
2039
2040 fn happy_path_frames() -> Vec<Vec<RawTouchEvent>> {
2041 calibration_attempt_frames(&[
2042 raw_point_for_corner(CalibrationCorner::UpperLeft),
2043 raw_point_for_corner(CalibrationCorner::UpperRight),
2044 raw_point_for_corner(CalibrationCorner::LowerRight),
2045 raw_point_for_corner(CalibrationCorner::LowerLeft),
2046 raw_point_for_verify_target(),
2047 ])
2048 }
2049
2050 fn raw_point_for_corner(calibration_corner: CalibrationCorner) -> RawPoint {
2051 let screen_point = calibration_corner_center(calibration_corner);
2052 distort_demo_screen_to_raw(screen_point.x as f32, screen_point.y as f32)
2053 }
2054
2055 fn tap_events(raw_point: RawPoint) -> Vec<RawTouchEvent> {
2056 let mut raw_touch_events = Vec::new();
2057 raw_touch_events.push(RawTouchEvent::Down {
2058 raw_x: raw_point.x,
2059 raw_y: raw_point.y,
2060 });
2061 for _discarded_sample_index in 0..SAMPLES_DISCARDED_AFTER_DOWN {
2062 raw_touch_events.push(RawTouchEvent::Move {
2063 raw_x: raw_point.x,
2064 raw_y: raw_point.y,
2065 });
2066 }
2067 for _usable_sample_index in 0..MIN_SAMPLES_PER_POINT {
2068 raw_touch_events.push(RawTouchEvent::Move {
2069 raw_x: raw_point.x,
2070 raw_y: raw_point.y,
2071 });
2072 }
2073 raw_touch_events.push(RawTouchEvent::Up);
2074 raw_touch_events
2075 }
2076
2077 fn dropout_tap_events(raw_point: RawPoint) -> Vec<RawTouchEvent> {
2078 let mut raw_touch_events = tap_events(raw_point);
2079 raw_touch_events.extend([
2080 RawTouchEvent::Down {
2081 raw_x: raw_point.x,
2082 raw_y: raw_point.y,
2083 },
2084 RawTouchEvent::Move {
2085 raw_x: raw_point.x,
2086 raw_y: raw_point.y,
2087 },
2088 RawTouchEvent::Up,
2089 ]);
2090 raw_touch_events
2091 }
2092
2093 fn long_press_with_lift_off_drift_frame(
2094 stable_raw_point: RawPoint,
2095 drifted_raw_point: RawPoint,
2096 ) -> Vec<RawTouchEvent> {
2097 let mut raw_touch_events = Vec::new();
2098 raw_touch_events.push(RawTouchEvent::Down {
2099 raw_x: stable_raw_point.x,
2100 raw_y: stable_raw_point.y,
2101 });
2102 for _stable_move_index in 0..2_004 {
2103 raw_touch_events.push(RawTouchEvent::Move {
2104 raw_x: stable_raw_point.x,
2105 raw_y: stable_raw_point.y,
2106 });
2107 }
2108 for _drifted_move_index in 0..3 {
2109 raw_touch_events.push(RawTouchEvent::Move {
2110 raw_x: drifted_raw_point.x,
2111 raw_y: drifted_raw_point.y,
2112 });
2113 }
2114 raw_touch_events.push(RawTouchEvent::Up);
2115 raw_touch_events
2116 }
2117
2118 fn calibration_attempt_frames(raw_points: &[RawPoint]) -> Vec<Vec<RawTouchEvent>> {
2119 let mut frames = Vec::new();
2120 for (tap_index, raw_point) in raw_points.iter().copied().enumerate() {
2121 frames.push(tap_events(raw_point));
2122 if tap_index + 2 < raw_points.len() {
2123 append_idle_frames(&mut frames, capture_ack_extra_idle_frames());
2124 }
2125 }
2126 frames
2127 }
2128
2129 fn append_idle_frames(frames: &mut Vec<Vec<RawTouchEvent>>, idle_frame_count: usize) {
2130 frames.extend((0..idle_frame_count).map(|_| Vec::new()));
2131 }
2132
2133 fn raw_point_for_verify_target() -> RawPoint {
2134 let verify_center = calibration_verify_target_center();
2135 distort_demo_screen_to_raw(verify_center.x as f32, verify_center.y as f32)
2136 }
2137
2138 fn assert_maps_near_corner(
2139 calibration_config: CalibrationConfig,
2140 raw_point: RawPoint,
2141 calibration_corner: CalibrationCorner,
2142 ) {
2143 let expected_screen_point = calibration_corner_center(calibration_corner);
2144 let (mapped_x, mapped_y) = calibration_config.map_raw_to_screen(raw_point.x, raw_point.y);
2145 assert!(
2146 (mapped_x - expected_screen_point.x as f32).abs() <= 1.0,
2147 "mapped_x={mapped_x} expected_x={}",
2148 expected_screen_point.x
2149 );
2150 assert!(
2151 (mapped_y - expected_screen_point.y as f32).abs() <= 1.0,
2152 "mapped_y={mapped_y} expected_y={}",
2153 expected_screen_point.y
2154 );
2155 }
2156
2157 const fn capture_ack_extra_idle_frames() -> usize {
2158 CAPTURE_ACK_FRAME_COUNT
2164 }
2165
2166 const fn rejected_restart_idle_frames() -> usize {
2167 REJECTED_FRAME_COUNT
2168 }
2169
2170 const fn verify_timeout_extra_idle_frames() -> usize {
2171 VERIFY_TIMEOUT_FRAMES.saturating_sub(1)
2172 }
2173}