Skip to main content

virtual_lcd_core/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5use std::time::Duration;
6use std::collections::BTreeMap;
7use instant::Instant;
8
9pub use virtual_lcd_sdk::{Color, Lcd, LcdBus, PinId};
10
11pub type Result<T> = std::result::Result<T, LcdError>;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ControllerModel {
15    GenericMipiDcs,
16    Ili9341,
17    Ssd1306,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct LcdConfig {
22    pub width: u16,
23    pub height: u16,
24    pub pixel_format: PixelFormat,
25    pub fps: u16,
26    pub interface: InterfaceType,
27    pub orientation: u16,
28    pub vsync: bool,
29    pub buffering: BufferingMode,
30    pub backlight: bool,
31    pub tearing_effect: bool,
32    pub bus_hz: u32,
33    pub controller: ControllerModel,
34}
35
36impl Default for LcdConfig {
37    fn default() -> Self {
38        Self {
39            width: 320,
40            height: 240,
41            pixel_format: PixelFormat::Rgb565,
42            fps: 30,
43            interface: InterfaceType::Spi4Wire,
44            orientation: 0,
45            vsync: true,
46            buffering: BufferingMode::Double,
47            backlight: true,
48            tearing_effect: false,
49            bus_hz: 8_000_000,
50            controller: ControllerModel::Ili9341,
51        }
52    }
53}
54
55impl LcdConfig {
56    fn validate(&self) -> Result<()> {
57        if self.width == 0 || self.height == 0 {
58            return Err(LcdError::InvalidConfig("display dimensions must be non-zero"));
59        }
60
61        if self.fps == 0 {
62            return Err(LcdError::InvalidConfig("fps must be non-zero"));
63        }
64
65        if self.bus_hz == 0 {
66            return Err(LcdError::InvalidConfig("bus_hz must be non-zero"));
67        }
68
69        if matches!(self.controller, ControllerModel::Ssd1306) {
70            if self.width > 128 {
71                return Err(LcdError::InvalidConfig("ssd1306 width must be 128 pixels or smaller"));
72            }
73
74            if self.height > 64 {
75                return Err(LcdError::InvalidConfig("ssd1306 height must be 64 pixels or smaller"));
76            }
77
78            if self.height % 8 != 0 {
79                return Err(LcdError::InvalidConfig("ssd1306 height must be a multiple of 8"));
80            }
81        }
82
83        Ok(())
84    }
85
86    pub fn frame_interval(&self) -> Duration {
87        Duration::from_secs_f64(1.0 / self.fps as f64)
88    }
89
90    pub fn full_frame_bytes(&self) -> usize {
91        self.width as usize * self.height as usize * self.pixel_format.bytes_per_pixel()
92    }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum PixelFormat {
97    Mono1,
98    Gray8,
99    Rgb565,
100    Rgb888,
101}
102
103impl PixelFormat {
104    pub fn bytes_per_pixel(self) -> usize {
105        match self {
106            Self::Mono1 | Self::Gray8 => 1,
107            Self::Rgb565 => 2,
108            Self::Rgb888 => 3,
109        }
110    }
111
112    fn decode_color(self, bytes: &[u8]) -> Color {
113        match self {
114            Self::Mono1 => {
115                if bytes[0] == 0 {
116                    Color::BLACK
117                } else {
118                    Color::WHITE
119                }
120            }
121            Self::Gray8 => Color::rgb(bytes[0], bytes[0], bytes[0]),
122            Self::Rgb565 => {
123                let value = u16::from_be_bytes([bytes[0], bytes[1]]);
124                Color::from_rgb565(value)
125            }
126            Self::Rgb888 => Color::rgb(bytes[0], bytes[1], bytes[2]),
127        }
128    }
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub enum InterfaceType {
133    Spi4Wire,
134    Spi3Wire,
135    Parallel8080,
136    MemoryMapped,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum BufferingMode {
141    Single,
142    Double,
143}
144
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub struct DrawWindow {
147    pub x: u16,
148    pub y: u16,
149    pub width: u16,
150    pub height: u16,
151}
152
153impl DrawWindow {
154    pub fn full(config: &LcdConfig) -> Self {
155        Self {
156            x: 0,
157            y: 0,
158            width: config.width,
159            height: config.height,
160        }
161    }
162
163    pub fn from_origin(x: u16, y: u16, width: u16, height: u16, config: &LcdConfig) -> Result<Self> {
164        if width == 0 || height == 0 {
165            return Err(LcdError::InvalidWindow);
166        }
167
168        let x_end = x
169            .checked_add(width - 1)
170            .ok_or(LcdError::OutOfBounds)?;
171        let y_end = y
172            .checked_add(height - 1)
173            .ok_or(LcdError::OutOfBounds)?;
174
175        if x_end >= config.width || y_end >= config.height {
176            return Err(LcdError::OutOfBounds);
177        }
178
179        Ok(Self {
180            x,
181            y,
182            width,
183            height,
184        })
185    }
186
187    pub fn from_inclusive(x0: u16, y0: u16, x1: u16, y1: u16, config: &LcdConfig) -> Result<Self> {
188        if x1 < x0 || y1 < y0 {
189            return Err(LcdError::InvalidWindow);
190        }
191
192        Self::from_origin(x0, y0, x1 - x0 + 1, y1 - y0 + 1, config)
193    }
194
195    pub fn area(self) -> usize {
196        self.width as usize * self.height as usize
197    }
198}
199
200#[derive(Clone, Debug)]
201pub struct LcdState {
202    pub initialized: bool,
203    pub sleeping: bool,
204    pub display_on: bool,
205    pub backlight: u8,
206    pub current_window: DrawWindow,
207    pub current_command: Option<u8>,
208    column_range: (u16, u16),
209    row_range: (u16, u16),
210}
211
212impl LcdState {
213    fn new(config: &LcdConfig) -> Self {
214        let full = DrawWindow::full(config);
215        Self {
216            initialized: false,
217            sleeping: true,
218            display_on: false,
219            backlight: if config.backlight { 100 } else { 0 },
220            current_window: full,
221            current_command: None,
222            column_range: (0, config.width - 1),
223            row_range: (0, config.height - 1),
224        }
225    }
226
227    fn set_column_range(&mut self, start: u16, end: u16) {
228        self.column_range = (start, end);
229        self.sync_window();
230    }
231
232    fn set_row_range(&mut self, start: u16, end: u16) {
233        self.row_range = (start, end);
234        self.sync_window();
235    }
236
237    fn sync_window(&mut self) {
238        self.current_window = DrawWindow {
239            x: self.column_range.0,
240            y: self.row_range.0,
241            width: self.column_range.1 - self.column_range.0 + 1,
242            height: self.row_range.1 - self.row_range.0 + 1,
243        };
244    }
245}
246
247#[derive(Debug)]
248enum ControllerRuntime {
249    Generic,
250    Ili9341(Ili9341State),
251    Ssd1306(Ssd1306State),
252}
253
254impl ControllerRuntime {
255    fn new(model: ControllerModel, config: &LcdConfig) -> Self {
256        match model {
257            ControllerModel::GenericMipiDcs => Self::Generic,
258            ControllerModel::Ili9341 => Self::Ili9341(Ili9341State::new(config)),
259            ControllerModel::Ssd1306 => Self::Ssd1306(Ssd1306State::new(config)),
260        }
261    }
262
263    fn reset(&mut self, config: &LcdConfig) {
264        match self {
265            Self::Generic => {}
266            Self::Ili9341(state) => *state = Ili9341State::new(config),
267            Self::Ssd1306(state) => *state = Ssd1306State::new(config),
268        }
269    }
270
271    fn visible_bytes_per_pixel(&self, fallback: PixelFormat) -> usize {
272        match self {
273            Self::Generic => fallback.bytes_per_pixel(),
274            Self::Ili9341(state) => state.interface_pixel_format().bytes_per_pixel(),
275            Self::Ssd1306(_) => PixelFormat::Mono1.bytes_per_pixel(),
276        }
277    }
278
279    fn native_frame_bytes(&self, config: &LcdConfig) -> usize {
280        match self {
281            Self::Generic | Self::Ili9341(_) => config.full_frame_bytes(),
282            Self::Ssd1306(state) => state.gddram.len(),
283        }
284    }
285}
286
287#[derive(Debug)]
288struct Ili9341State {
289    madctl: u8,
290    colmod: u8,
291    inversion_on: bool,
292    tearing_enabled: bool,
293    tearing_mode: u8,
294    brightness: u8,
295    control_display: u8,
296    scroll: VerticalScrollState,
297    interface_control: [u8; 3],
298    raw_registers: BTreeMap<u8, Vec<u8>>,
299}
300
301impl Ili9341State {
302    const MADCTL_MY: u8 = 0x80;
303    const MADCTL_MX: u8 = 0x40;
304    const MADCTL_MV: u8 = 0x20;
305    const MADCTL_BGR: u8 = 0x08;
306
307    fn new(config: &LcdConfig) -> Self {
308        Self {
309            madctl: 0x00,
310            colmod: 0x66,
311            inversion_on: false,
312            tearing_enabled: config.tearing_effect,
313            tearing_mode: 0x00,
314            brightness: if config.backlight { 0xFF } else { 0x00 },
315            control_display: 0x24,
316            scroll: VerticalScrollState::new(config.height),
317            interface_control: [0x01, 0x00, 0x00],
318            raw_registers: BTreeMap::new(),
319        }
320    }
321
322    fn interface_pixel_format(&self) -> PixelFormat {
323        match self.colmod & 0x07 {
324            0x05 => PixelFormat::Rgb565,
325            0x06 => PixelFormat::Rgb888,
326            _ => PixelFormat::Rgb565,
327        }
328    }
329
330    fn decode_interface_color(&self, bytes: &[u8]) -> Color {
331        match self.interface_pixel_format() {
332            PixelFormat::Rgb565 => PixelFormat::Rgb565.decode_color(bytes),
333            PixelFormat::Rgb888 => {
334                let expand = |value: u8| (value << 2) | (value >> 4);
335                Color::rgb(expand(bytes[0]), expand(bytes[1]), expand(bytes[2]))
336            }
337            other => other.decode_color(bytes),
338        }
339    }
340
341    fn map_logical_to_memory(&self, x: u16, y: u16, config: &LcdConfig) -> Result<(u16, u16)> {
342        let width = config.width;
343        let height = config.height;
344
345        let logical_y = self.scroll.map_visible_row(y, height);
346        let mx = self.madctl & Self::MADCTL_MX != 0;
347        let my = self.madctl & Self::MADCTL_MY != 0;
348        let mv = self.madctl & Self::MADCTL_MV != 0;
349
350        let (mem_x, mem_y) = if mv {
351            let mem_x = if mx {
352                width
353                    .checked_sub(logical_y + 1)
354                    .ok_or(LcdError::OutOfBounds)?
355            } else {
356                logical_y
357            };
358            let mem_y = if my {
359                height.checked_sub(x + 1).ok_or(LcdError::OutOfBounds)?
360            } else {
361                x
362            };
363            (mem_x, mem_y)
364        } else {
365            let mem_x = if mx {
366                width.checked_sub(x + 1).ok_or(LcdError::OutOfBounds)?
367            } else {
368                x
369            };
370            let mem_y = if my {
371                height
372                    .checked_sub(logical_y + 1)
373                    .ok_or(LcdError::OutOfBounds)?
374            } else {
375                logical_y
376            };
377            (mem_x, mem_y)
378        };
379
380        if mem_x >= width || mem_y >= height {
381            return Err(LcdError::OutOfBounds);
382        }
383
384        Ok((mem_x, mem_y))
385    }
386
387    fn write_pixel_coords(
388        &self,
389        window: DrawWindow,
390        next_pixel: usize,
391        config: &LcdConfig,
392    ) -> Result<(u16, u16)> {
393        let dx = (next_pixel % window.width as usize) as u16;
394        let dy = (next_pixel / window.width as usize) as u16;
395        self.map_logical_to_memory(window.x + dx, window.y + dy, config)
396    }
397
398    fn apply_visible_transform(
399        &self,
400        memory: &Framebuffer,
401        visible: &mut Framebuffer,
402        state: &LcdState,
403        config: &LcdConfig,
404    ) -> Result<()> {
405        if !state.display_on || state.sleeping || state.backlight == 0 || self.brightness == 0 {
406            visible.clear(Color::BLACK);
407            return Ok(());
408        }
409
410        for y in 0..config.height {
411            for x in 0..config.width {
412                let (mem_x, mem_y) = self.map_logical_to_memory(x, y, config)?;
413                let mut color = memory.get_pixel(mem_x, mem_y).unwrap_or(Color::BLACK);
414                if self.madctl & Self::MADCTL_BGR != 0 {
415                    color = Color::rgb(color.b, color.g, color.r);
416                }
417                visible.set_pixel(x, y, color)?;
418            }
419        }
420
421        Ok(())
422    }
423
424    fn power_mode(&self, state: &LcdState) -> u8 {
425        let mut mode = 0u8;
426        if !state.sleeping {
427            mode |= 0x08;
428        }
429        if state.display_on {
430            mode |= 0x04;
431        }
432        if self.interface_pixel_format() == PixelFormat::Rgb565 {
433            mode |= 0x02;
434        }
435        if state.initialized {
436            mode |= 0x80;
437        }
438        mode
439    }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443enum Ssd1306AddressingMode {
444    Horizontal,
445    Vertical,
446    Page,
447}
448
449#[derive(Debug)]
450struct Ssd1306State {
451    gddram: Vec<u8>,
452    memory_mode: Ssd1306AddressingMode,
453    column_start: u8,
454    column_end: u8,
455    page_start: u8,
456    page_end: u8,
457    column: u8,
458    page: u8,
459    start_line: u8,
460    display_offset: u8,
461    contrast: u8,
462    multiplex_ratio: u8,
463    clock_div: u8,
464    precharge: u8,
465    com_pins: u8,
466    vcomh: u8,
467    charge_pump: u8,
468    segment_remap: bool,
469    com_scan_reverse: bool,
470    entire_display_on: bool,
471    inverse_display: bool,
472    scroll_enabled: bool,
473    raw_registers: BTreeMap<u8, Vec<u8>>,
474}
475
476impl Ssd1306State {
477    fn new(config: &LcdConfig) -> Self {
478        let pages = (config.height / 8).max(1);
479        Self {
480            gddram: vec![0x00; config.width as usize * pages as usize],
481            memory_mode: Ssd1306AddressingMode::Page,
482            column_start: 0,
483            column_end: config.width.saturating_sub(1) as u8,
484            page_start: 0,
485            page_end: pages.saturating_sub(1) as u8,
486            column: 0,
487            page: 0,
488            start_line: 0,
489            display_offset: 0,
490            contrast: 0x7F,
491            multiplex_ratio: config.height.saturating_sub(1) as u8,
492            clock_div: 0x80,
493            precharge: 0xF1,
494            com_pins: if config.height > 32 { 0x12 } else { 0x02 },
495            vcomh: 0x20,
496            charge_pump: 0x14,
497            segment_remap: false,
498            com_scan_reverse: false,
499            entire_display_on: false,
500            inverse_display: false,
501            scroll_enabled: false,
502            raw_registers: BTreeMap::new(),
503        }
504    }
505
506    fn normalize_color(&self, color: Color) -> Color {
507        if color.luminance() >= 128 {
508            Color::WHITE
509        } else {
510            Color::BLACK
511        }
512    }
513
514    fn pages(&self, config: &LcdConfig) -> u8 {
515        (config.height / 8).max(1) as u8
516    }
517
518    fn clamp_column(&self, column: u8, config: &LcdConfig) -> u8 {
519        column.min(config.width.saturating_sub(1) as u8)
520    }
521
522    fn clamp_page(&self, page: u8, config: &LcdConfig) -> u8 {
523        page.min(self.pages(config).saturating_sub(1))
524    }
525
526    fn gddram_index(&self, x: u16, page: u8, config: &LcdConfig) -> Option<usize> {
527        if x >= config.width || page >= self.pages(config) {
528            return None;
529        }
530
531        Some(page as usize * config.width as usize + x as usize)
532    }
533
534    fn sync_gddram_byte_to_frame(
535        &self,
536        frame: &mut Framebuffer,
537        column: u8,
538        page: u8,
539        config: &LcdConfig,
540    ) -> Result<()> {
541        let x = column as u16;
542        let Some(index) = self.gddram_index(x, page, config) else {
543            return Ok(());
544        };
545        let byte = self.gddram[index];
546        let base_y = page as u16 * 8;
547
548        for bit in 0..8u16 {
549            let y = base_y + bit;
550            if y >= config.height {
551                break;
552            }
553
554            let color = if (byte >> bit) & 0x01 != 0 {
555                Color::WHITE
556            } else {
557                Color::BLACK
558            };
559            frame.set_pixel(x, y, color)?;
560        }
561
562        Ok(())
563    }
564
565    fn set_native_pixel(&mut self, x: u16, y: u16, on: bool, config: &LcdConfig) -> Result<()> {
566        let page = (y / 8) as u8;
567        let bit = (y % 8) as u8;
568        let index = self
569            .gddram_index(x, page, config)
570            .ok_or(LcdError::OutOfBounds)?;
571
572        if on {
573            self.gddram[index] |= 1 << bit;
574        } else {
575            self.gddram[index] &= !(1 << bit);
576        }
577
578        Ok(())
579    }
580
581    fn sync_pixel_from_color(
582        &mut self,
583        frame: &mut Framebuffer,
584        x: u16,
585        y: u16,
586        color: Color,
587        config: &LcdConfig,
588    ) -> Result<()> {
589        let mono = self.normalize_color(color);
590        frame.set_pixel(x, y, mono)?;
591        self.set_native_pixel(x, y, mono == Color::WHITE, config)
592    }
593
594    fn sync_window_from_frame(
595        &mut self,
596        frame: &mut Framebuffer,
597        window: DrawWindow,
598        config: &LcdConfig,
599    ) -> Result<()> {
600        for y in window.y..window.y + window.height {
601            for x in window.x..window.x + window.width {
602                let color = frame.get_pixel(x, y).unwrap_or(Color::BLACK);
603                self.sync_pixel_from_color(frame, x, y, color, config)?;
604            }
605        }
606
607        Ok(())
608    }
609
610    fn set_column_address(&mut self, start: u8, end: u8, config: &LcdConfig) {
611        self.column_start = self.clamp_column(start, config);
612        self.column_end = self.clamp_column(end, config).max(self.column_start);
613        self.column = self.column_start;
614    }
615
616    fn set_page_address(&mut self, start: u8, end: u8, config: &LcdConfig) {
617        self.page_start = self.clamp_page(start, config);
618        self.page_end = self.clamp_page(end, config).max(self.page_start);
619        self.page = self.page_start;
620    }
621
622    fn set_page_mode_page(&mut self, page: u8, config: &LcdConfig) {
623        self.page = self.clamp_page(page, config);
624    }
625
626    fn set_page_mode_lower_column(&mut self, lower: u8, config: &LcdConfig) {
627        self.column = self.clamp_column((self.column & 0xF0) | (lower & 0x0F), config);
628    }
629
630    fn set_page_mode_upper_column(&mut self, upper: u8, config: &LcdConfig) {
631        self.column = self.clamp_column((self.column & 0x0F) | ((upper & 0x0F) << 4), config);
632    }
633
634    fn advance_address(&mut self, config: &LcdConfig) {
635        match self.memory_mode {
636            Ssd1306AddressingMode::Horizontal => {
637                if self.column >= self.column_end {
638                    self.column = self.column_start;
639                    if self.page >= self.page_end {
640                        self.page = self.page_start;
641                    } else {
642                        self.page += 1;
643                    }
644                } else {
645                    self.column += 1;
646                }
647            }
648            Ssd1306AddressingMode::Vertical => {
649                if self.page >= self.page_end {
650                    self.page = self.page_start;
651                    if self.column >= self.column_end {
652                        self.column = self.column_start;
653                    } else {
654                        self.column += 1;
655                    }
656                } else {
657                    self.page += 1;
658                }
659            }
660            Ssd1306AddressingMode::Page => {
661                let max_column = config.width.saturating_sub(1) as u8;
662                if self.column >= max_column {
663                    self.column = 0;
664                } else {
665                    self.column += 1;
666                }
667            }
668        }
669    }
670
671    fn write_ram_bytes(
672        &mut self,
673        frame: &mut Framebuffer,
674        data: &[u8],
675        config: &LcdConfig,
676    ) -> Result<usize> {
677        for byte in data.iter().copied() {
678            let column = self.clamp_column(self.column, config);
679            let page = self.clamp_page(self.page, config);
680            if let Some(index) = self.gddram_index(column as u16, page, config) {
681                self.gddram[index] = byte;
682                self.sync_gddram_byte_to_frame(frame, column, page, config)?;
683            }
684            self.advance_address(config);
685        }
686
687        Ok(data.len())
688    }
689
690    fn apply_visible_transform(
691        &self,
692        visible: &mut Framebuffer,
693        state: &LcdState,
694        config: &LcdConfig,
695    ) -> Result<()> {
696        if !state.display_on || state.backlight == 0 {
697            visible.clear(Color::BLACK);
698            return Ok(());
699        }
700
701        let height = config.height;
702        let width = config.width;
703
704        for y in 0..height {
705            let logical_y = if self.com_scan_reverse {
706                height - 1 - y
707            } else {
708                y
709            };
710            let memory_y =
711                (logical_y + self.start_line as u16 + self.display_offset as u16) % height.max(1);
712
713            for x in 0..width {
714                let memory_x = if self.segment_remap {
715                    width - 1 - x
716                } else {
717                    x
718                };
719
720                let pixel_on = if self.entire_display_on {
721                    true
722                } else {
723                    let page = (memory_y / 8) as u8;
724                    let bit = (memory_y % 8) as u8;
725                    let Some(index) = self.gddram_index(memory_x, page, config) else {
726                        continue;
727                    };
728                    let mut on = (self.gddram[index] >> bit) & 0x01 != 0;
729                    if self.inverse_display {
730                        on = !on;
731                    }
732                    on
733                };
734
735                visible.set_pixel(x, y, if pixel_on { Color::WHITE } else { Color::BLACK })?;
736            }
737        }
738
739        Ok(())
740    }
741}
742
743#[derive(Debug)]
744struct VerticalScrollState {
745    top_fixed_area: u16,
746    scroll_area: u16,
747    bottom_fixed_area: u16,
748    start_address: u16,
749}
750
751impl VerticalScrollState {
752    fn new(height: u16) -> Self {
753        Self {
754            top_fixed_area: 0,
755            scroll_area: height,
756            bottom_fixed_area: 0,
757            start_address: 0,
758        }
759    }
760
761    fn map_visible_row(&self, row: u16, total_height: u16) -> u16 {
762        if self.top_fixed_area + self.scroll_area + self.bottom_fixed_area != total_height {
763            return row;
764        }
765
766        if row < self.top_fixed_area {
767            return row;
768        }
769
770        if row >= self.top_fixed_area + self.scroll_area {
771            return row;
772        }
773
774        if self.scroll_area == 0 {
775            return row;
776        }
777
778        let offset = row - self.top_fixed_area;
779        self.top_fixed_area + ((offset + self.start_address) % self.scroll_area)
780    }
781}
782
783#[derive(Debug)]
784struct RegisterWrite {
785    register: RegisterKind,
786    allowed_lengths: &'static [usize],
787}
788
789#[derive(Debug, Clone, Copy)]
790enum RegisterKind {
791    Madctl,
792    Colmod,
793    VerticalScrollDefinition,
794    VerticalScrollStart,
795    Brightness,
796    ControlDisplay,
797    InterfaceControl,
798    Ssd1306MemoryMode,
799    Ssd1306ColumnAddress,
800    Ssd1306PageAddress,
801    Ssd1306Contrast,
802    Ssd1306MultiplexRatio,
803    Ssd1306DisplayOffset,
804    Ssd1306ClockDiv,
805    Ssd1306Precharge,
806    Ssd1306Compins,
807    Ssd1306Vcomh,
808    Ssd1306ChargePump,
809    Raw(u8),
810}
811
812#[derive(Clone, Debug)]
813pub struct Framebuffer {
814    width: u16,
815    height: u16,
816    pixels: Vec<Color>,
817}
818
819impl Framebuffer {
820    pub fn new(width: u16, height: u16) -> Self {
821        Self {
822            width,
823            height,
824            pixels: vec![Color::BLACK; width as usize * height as usize],
825        }
826    }
827
828    pub fn width(&self) -> u16 {
829        self.width
830    }
831
832    pub fn height(&self) -> u16 {
833        self.height
834    }
835
836    pub fn pixels(&self) -> &[Color] {
837        &self.pixels
838    }
839
840    pub fn clear(&mut self, color: Color) {
841        self.pixels.fill(color);
842    }
843
844    pub fn copy_from(&mut self, other: &Self) {
845        self.pixels.clone_from_slice(&other.pixels);
846    }
847
848    pub fn get_pixel(&self, x: u16, y: u16) -> Option<Color> {
849        let index = self.index_of(x, y)?;
850        Some(self.pixels[index])
851    }
852
853    pub fn set_pixel(&mut self, x: u16, y: u16, color: Color) -> Result<()> {
854        let index = self.index_of(x, y).ok_or(LcdError::OutOfBounds)?;
855        self.pixels[index] = color;
856        Ok(())
857    }
858
859    pub fn fill_rect(&mut self, window: DrawWindow, color: Color) -> Result<()> {
860        for y in window.y..window.y + window.height {
861            for x in window.x..window.x + window.width {
862                self.set_pixel(x, y, color)?;
863            }
864        }
865        Ok(())
866    }
867
868    fn index_of(&self, x: u16, y: u16) -> Option<usize> {
869        if x >= self.width || y >= self.height {
870            return None;
871        }
872
873        Some(y as usize * self.width as usize + x as usize)
874    }
875}
876
877#[derive(Clone, Debug)]
878pub struct PinBank {
879    levels: [bool; 9],
880}
881
882impl Default for PinBank {
883    fn default() -> Self {
884        let mut levels = [false; 9];
885        levels[PinId::Cs.index()] = true;
886        levels[PinId::Rst.index()] = true;
887        levels[PinId::Wr.index()] = true;
888        levels[PinId::Rd.index()] = true;
889        levels[PinId::Bl.index()] = true;
890        Self { levels }
891    }
892}
893
894impl PinBank {
895    pub fn level(&self, pin: PinId) -> bool {
896        self.levels[pin.index()]
897    }
898
899    fn set(&mut self, pin: PinId, value: bool) {
900        self.levels[pin.index()] = value;
901    }
902}
903
904#[derive(Debug)]
905struct TimingEngine {
906    frame_interval: Duration,
907    bus_hz: u32,
908    last_visible_at: Instant,
909    pending_ready_at: Option<Instant>,
910}
911
912impl TimingEngine {
913    fn new(config: &LcdConfig) -> Self {
914        let frame_interval = config.frame_interval();
915        Self {
916            frame_interval,
917            bus_hz: config.bus_hz,
918            last_visible_at: Instant::now() - frame_interval,
919            pending_ready_at: None,
920        }
921    }
922
923    fn schedule_transfer(&mut self, bytes: usize, vsync: bool) -> Result<Instant> {
924        let now = Instant::now();
925
926        if let Some(ready_at) = self.pending_ready_at {
927            if ready_at > now {
928                return Err(LcdError::FrameRateExceeded);
929            }
930        }
931
932        let transfer_secs = (bytes as f64 * 8.0) / self.bus_hz as f64;
933        let bus_time = Duration::from_secs_f64(transfer_secs.max(0.0));
934        let earliest = if vsync {
935            self.last_visible_at + self.frame_interval
936        } else {
937            now
938        };
939        let ready_at = max_instant(now + bus_time, earliest);
940
941        self.pending_ready_at = Some(ready_at);
942        Ok(ready_at)
943    }
944
945    fn tick(&mut self) -> bool {
946        match self.pending_ready_at {
947            Some(ready_at) if Instant::now() >= ready_at => {
948                self.last_visible_at = ready_at;
949                self.pending_ready_at = None;
950                true
951            }
952            _ => false,
953        }
954    }
955
956    fn time_until_ready(&self) -> Option<Duration> {
957        self.pending_ready_at.map(|ready_at| ready_at.saturating_duration_since(Instant::now()))
958    }
959
960    fn clear_pending(&mut self) {
961        self.pending_ready_at = None;
962    }
963}
964
965#[derive(Debug)]
966enum PendingWrite {
967    None,
968    Column(AddressAccumulator),
969    Row(AddressAccumulator),
970    Register(RegisterWrite),
971    MemoryWrite(MemoryWriteProgress),
972}
973
974#[derive(Debug)]
975struct AddressAccumulator {
976    bytes: [u8; 4],
977    len: usize,
978}
979
980impl AddressAccumulator {
981    fn new() -> Self {
982        Self {
983            bytes: [0; 4],
984            len: 0,
985        }
986    }
987
988    fn push(&mut self, data: &[u8]) -> usize {
989        let available = 4 - self.len;
990        let take = available.min(data.len());
991        self.bytes[self.len..self.len + take].copy_from_slice(&data[..take]);
992        self.len += take;
993        take
994    }
995
996    fn complete(&self) -> bool {
997        self.len == 4
998    }
999
1000    fn decode(&self) -> (u16, u16) {
1001        let start = u16::from_be_bytes([self.bytes[0], self.bytes[1]]);
1002        let end = u16::from_be_bytes([self.bytes[2], self.bytes[3]]);
1003        (start, end)
1004    }
1005}
1006
1007#[derive(Debug)]
1008struct MemoryWriteProgress {
1009    window: DrawWindow,
1010    next_pixel: usize,
1011    partial_pixel: [u8; 3],
1012    partial_len: usize,
1013    transferred_bytes: usize,
1014}
1015
1016impl MemoryWriteProgress {
1017    fn new(window: DrawWindow) -> Self {
1018        Self {
1019            window,
1020            next_pixel: 0,
1021            partial_pixel: [0; 3],
1022            partial_len: 0,
1023            transferred_bytes: 0,
1024        }
1025    }
1026
1027    fn total_pixels(&self) -> usize {
1028        self.window.area()
1029    }
1030
1031    fn remaining_bytes(&self, bytes_per_pixel: usize) -> usize {
1032        (self.total_pixels() - self.next_pixel) * bytes_per_pixel - self.partial_len
1033    }
1034
1035    fn finished(&self) -> bool {
1036        self.next_pixel == self.total_pixels() && self.partial_len == 0
1037    }
1038
1039    fn current_coords(&self) -> (u16, u16) {
1040        let dx = (self.next_pixel % self.window.width as usize) as u16;
1041        let dy = (self.next_pixel / self.window.width as usize) as u16;
1042        (self.window.x + dx, self.window.y + dy)
1043    }
1044}
1045
1046#[derive(Debug)]
1047pub struct VirtualLcd {
1048    config: LcdConfig,
1049    state: LcdState,
1050    controller: ControllerRuntime,
1051    front_buffer: Framebuffer,
1052    back_buffer: Framebuffer,
1053    pins: PinBank,
1054    timing: TimingEngine,
1055    pending_write: PendingWrite,
1056}
1057
1058impl VirtualLcd {
1059    pub fn new(config: LcdConfig) -> Result<Self> {
1060        config.validate()?;
1061
1062        let front_buffer = Framebuffer::new(config.width, config.height);
1063        let back_buffer = Framebuffer::new(config.width, config.height);
1064        let state = LcdState::new(&config);
1065        let controller = ControllerRuntime::new(config.controller, &config);
1066        let timing = TimingEngine::new(&config);
1067
1068        Ok(Self {
1069            config,
1070            state,
1071            controller,
1072            front_buffer,
1073            back_buffer,
1074            pins: PinBank::default(),
1075            timing,
1076            pending_write: PendingWrite::None,
1077        })
1078    }
1079
1080    pub fn config(&self) -> &LcdConfig {
1081        &self.config
1082    }
1083
1084    pub fn state(&self) -> &LcdState {
1085        &self.state
1086    }
1087
1088    pub fn pins(&self) -> &PinBank {
1089        &self.pins
1090    }
1091
1092    pub fn visible_frame(&self) -> &Framebuffer {
1093        &self.front_buffer
1094    }
1095
1096    pub fn working_frame(&self) -> &Framebuffer {
1097        &self.back_buffer
1098    }
1099
1100    pub fn controller_model(&self) -> ControllerModel {
1101        self.config.controller
1102    }
1103
1104    pub fn set_window(&mut self, x: u16, y: u16, width: u16, height: u16) -> Result<()> {
1105        self.ensure_ready_for_graphics()?;
1106        let window = DrawWindow::from_origin(x, y, width, height, &self.config)?;
1107        self.state.set_column_range(window.x, window.x + window.width - 1);
1108        self.state.set_row_range(window.y, window.y + window.height - 1);
1109        Ok(())
1110    }
1111
1112    pub fn set_address_window(&mut self, x0: u16, y0: u16, x1: u16, y1: u16) -> Result<()> {
1113        self.ensure_ready_for_graphics()?;
1114        let window = DrawWindow::from_inclusive(x0, y0, x1, y1, &self.config)?;
1115        self.state.set_column_range(window.x, window.x + window.width - 1);
1116        self.state.set_row_range(window.y, window.y + window.height - 1);
1117        Ok(())
1118    }
1119
1120    pub fn write_pixels(&mut self, pixels: &[Color]) -> Result<()> {
1121        self.ensure_ready_for_graphics()?;
1122        let expected = self.state.current_window.area();
1123        if pixels.len() != expected {
1124            return Err(LcdError::InvalidDataLength {
1125                expected,
1126                got: pixels.len(),
1127            });
1128        }
1129
1130        let window = self.state.current_window;
1131        for (index, color) in pixels.iter().copied().enumerate() {
1132            let dx = (index % window.width as usize) as u16;
1133            let dy = (index / window.width as usize) as u16;
1134            let color = self.normalize_high_level_color(color);
1135            self.back_buffer
1136                .set_pixel(window.x + dx, window.y + dy, color)?;
1137            self.sync_controller_pixel(window.x + dx, window.y + dy, color)?;
1138        }
1139
1140        self.schedule_visible_update(expected * self.config.pixel_format.bytes_per_pixel())
1141    }
1142
1143    pub fn tick(&mut self) -> bool {
1144        if self.timing.tick() {
1145            let _ = self.rebuild_visible_frame();
1146            if self.config.buffering == BufferingMode::Single
1147                && matches!(self.controller, ControllerRuntime::Generic)
1148            {
1149                self.back_buffer.copy_from(&self.front_buffer);
1150            }
1151            return true;
1152        }
1153
1154        false
1155    }
1156
1157    pub fn time_until_ready(&self) -> Option<Duration> {
1158        self.timing.time_until_ready()
1159    }
1160
1161    pub fn has_pending_frame(&self) -> bool {
1162        self.timing.pending_ready_at.is_some()
1163    }
1164
1165    fn hardware_reset(&mut self) {
1166        self.front_buffer.clear(Color::BLACK);
1167        self.back_buffer.clear(Color::BLACK);
1168        self.state = LcdState::new(&self.config);
1169        self.controller.reset(&self.config);
1170        self.pending_write = PendingWrite::None;
1171        self.timing.clear_pending();
1172    }
1173
1174    fn ensure_ready_for_graphics(&self) -> Result<()> {
1175        if !self.state.initialized {
1176            return Err(LcdError::NotInitialized);
1177        }
1178
1179        if self.state.sleeping {
1180            return Err(LcdError::SleepMode);
1181        }
1182
1183        if !self.state.display_on {
1184            return Err(LcdError::DisplayOff);
1185        }
1186
1187        Ok(())
1188    }
1189
1190    fn ensure_memory_access(&self) -> Result<()> {
1191        if !self.state.initialized {
1192            return Err(LcdError::NotInitialized);
1193        }
1194
1195        if self.state.sleeping {
1196            return Err(LcdError::SleepMode);
1197        }
1198
1199        Ok(())
1200    }
1201
1202    fn validate_bus_access(&self) -> Result<()> {
1203        if self.pins.level(PinId::Cs) {
1204            return Err(LcdError::BusViolation("cannot access bus while CS is high"));
1205        }
1206
1207        if !self.pins.level(PinId::Rst) {
1208            return Err(LcdError::BusViolation("cannot access bus while reset is asserted"));
1209        }
1210
1211        Ok(())
1212    }
1213
1214    fn schedule_visible_update(&mut self, bytes: usize) -> Result<()> {
1215        self.timing.schedule_transfer(bytes, self.config.vsync)?;
1216        Ok(())
1217    }
1218
1219    fn rebuild_visible_frame(&mut self) -> Result<()> {
1220        match &self.controller {
1221            ControllerRuntime::Generic => {
1222                if self.state.display_on && !self.state.sleeping && self.state.backlight > 0 {
1223                    self.front_buffer.copy_from(&self.back_buffer);
1224                } else {
1225                    self.front_buffer.clear(Color::BLACK);
1226                }
1227            }
1228            ControllerRuntime::Ili9341(controller) => {
1229                controller.apply_visible_transform(
1230                    &self.back_buffer,
1231                    &mut self.front_buffer,
1232                    &self.state,
1233                    &self.config,
1234                )?;
1235            }
1236            ControllerRuntime::Ssd1306(controller) => {
1237                controller.apply_visible_transform(
1238                    &mut self.front_buffer,
1239                    &self.state,
1240                    &self.config,
1241                )?;
1242            }
1243        }
1244        Ok(())
1245    }
1246
1247    fn normalize_high_level_color(&self, color: Color) -> Color {
1248        match &self.controller {
1249            ControllerRuntime::Ssd1306(controller) => controller.normalize_color(color),
1250            _ => color,
1251        }
1252    }
1253
1254    fn sync_controller_pixel(&mut self, x: u16, y: u16, color: Color) -> Result<()> {
1255        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1256            controller.sync_pixel_from_color(&mut self.back_buffer, x, y, color, &self.config)?;
1257        }
1258        Ok(())
1259    }
1260
1261    fn sync_controller_window(&mut self, window: DrawWindow) -> Result<()> {
1262        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1263            controller.sync_window_from_frame(&mut self.back_buffer, window, &self.config)?;
1264        }
1265        Ok(())
1266    }
1267
1268    fn process_address_data(&mut self, accumulator: &mut AddressAccumulator, data: &[u8], is_column: bool) -> Result<usize> {
1269        let consumed = accumulator.push(data);
1270        if accumulator.complete() {
1271            let (start, end) = accumulator.decode();
1272            let window = if is_column {
1273                DrawWindow::from_inclusive(
1274                    start,
1275                    self.state.current_window.y,
1276                    end,
1277                    self.state.current_window.y + self.state.current_window.height - 1,
1278                    &self.config,
1279                )?
1280            } else {
1281                DrawWindow::from_inclusive(
1282                    self.state.current_window.x,
1283                    start,
1284                    self.state.current_window.x + self.state.current_window.width - 1,
1285                    end,
1286                    &self.config,
1287                )?
1288            };
1289
1290            self.state.set_column_range(window.x, window.x + window.width - 1);
1291            self.state.set_row_range(window.y, window.y + window.height - 1);
1292        }
1293
1294        Ok(consumed)
1295    }
1296
1297    fn process_memory_write(&mut self, progress: &mut MemoryWriteProgress, data: &[u8]) -> Result<usize> {
1298        self.ensure_memory_access()?;
1299
1300        let bytes_per_pixel = self.controller.visible_bytes_per_pixel(self.config.pixel_format);
1301        if data.len() > progress.remaining_bytes(bytes_per_pixel) {
1302            return Err(LcdError::InvalidDataLength {
1303                expected: progress.remaining_bytes(bytes_per_pixel),
1304                got: data.len(),
1305            });
1306        }
1307
1308        for byte in data.iter().copied() {
1309            progress.partial_pixel[progress.partial_len] = byte;
1310            progress.partial_len += 1;
1311            progress.transferred_bytes += 1;
1312
1313            if progress.partial_len == bytes_per_pixel {
1314                let color = match &self.controller {
1315                    ControllerRuntime::Generic => self
1316                        .config
1317                        .pixel_format
1318                        .decode_color(&progress.partial_pixel[..bytes_per_pixel]),
1319                    ControllerRuntime::Ili9341(controller) => {
1320                        controller.decode_interface_color(&progress.partial_pixel[..bytes_per_pixel])
1321                    }
1322                    ControllerRuntime::Ssd1306(_) => {
1323                        unreachable!("ssd1306 does not use MIPI-style memory write sequencing")
1324                    }
1325                };
1326                let (x, y) = match &self.controller {
1327                    ControllerRuntime::Generic => progress.current_coords(),
1328                    ControllerRuntime::Ili9341(controller) => {
1329                        controller.write_pixel_coords(progress.window, progress.next_pixel, &self.config)?
1330                    }
1331                    ControllerRuntime::Ssd1306(_) => {
1332                        unreachable!("ssd1306 does not use MIPI-style memory write sequencing")
1333                    }
1334                };
1335                self.back_buffer.set_pixel(x, y, color)?;
1336                progress.partial_len = 0;
1337                progress.next_pixel += 1;
1338            }
1339        }
1340
1341        Ok(data.len())
1342    }
1343
1344    fn process_ssd1306_ram_write(&mut self, data: &[u8]) -> Result<usize> {
1345        self.ensure_memory_access()?;
1346
1347        match &mut self.controller {
1348            ControllerRuntime::Ssd1306(controller) => {
1349                controller.write_ram_bytes(&mut self.back_buffer, data, &self.config)
1350            }
1351            _ => Err(LcdError::BusViolation(
1352                "ssd1306 RAM write requested for a non-ssd1306 controller",
1353            )),
1354        }
1355    }
1356
1357    fn process_register_write(&mut self, write: RegisterWrite, data: &[u8]) -> Result<()> {
1358        if !write.allowed_lengths.contains(&data.len()) {
1359            return Err(LcdError::InvalidDataLength {
1360                expected: *write.allowed_lengths.first().unwrap_or(&0),
1361                got: data.len(),
1362            });
1363        }
1364
1365        let mut refresh_visible = false;
1366        match (&mut self.controller, write.register) {
1367            (ControllerRuntime::Generic, RegisterKind::Raw(_)) => {}
1368            (ControllerRuntime::Ili9341(controller), RegisterKind::Madctl) => {
1369                controller.madctl = data[0];
1370                refresh_visible = true;
1371            }
1372            (ControllerRuntime::Ili9341(controller), RegisterKind::Colmod) => {
1373                controller.colmod = data[0];
1374            }
1375            (ControllerRuntime::Ili9341(controller), RegisterKind::VerticalScrollDefinition) => {
1376                controller.scroll.top_fixed_area = u16::from_be_bytes([data[0], data[1]]);
1377                controller.scroll.scroll_area = u16::from_be_bytes([data[2], data[3]]);
1378                controller.scroll.bottom_fixed_area = u16::from_be_bytes([data[4], data[5]]);
1379                refresh_visible = true;
1380            }
1381            (ControllerRuntime::Ili9341(controller), RegisterKind::VerticalScrollStart) => {
1382                controller.scroll.start_address =
1383                    u16::from_be_bytes([data[0], data[1]]) % controller.scroll.scroll_area.max(1);
1384                refresh_visible = true;
1385            }
1386            (ControllerRuntime::Ili9341(controller), RegisterKind::Brightness) => {
1387                controller.brightness = data[0];
1388                refresh_visible = true;
1389            }
1390            (ControllerRuntime::Ili9341(controller), RegisterKind::ControlDisplay) => {
1391                controller.control_display = data[0];
1392            }
1393            (ControllerRuntime::Ili9341(controller), RegisterKind::InterfaceControl) => {
1394                controller.interface_control.copy_from_slice(&data[..3]);
1395            }
1396            (ControllerRuntime::Ili9341(controller), RegisterKind::Raw(cmd)) => {
1397                controller.raw_registers.insert(cmd, data.to_vec());
1398            }
1399            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306MemoryMode) => {
1400                controller.memory_mode = match data[0] & 0x03 {
1401                    0x00 => Ssd1306AddressingMode::Horizontal,
1402                    0x01 => Ssd1306AddressingMode::Vertical,
1403                    _ => Ssd1306AddressingMode::Page,
1404                };
1405            }
1406            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306ColumnAddress) => {
1407                controller.set_column_address(data[0], data[1], &self.config);
1408            }
1409            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306PageAddress) => {
1410                controller.set_page_address(data[0], data[1], &self.config);
1411            }
1412            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306Contrast) => {
1413                controller.contrast = data[0];
1414            }
1415            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306MultiplexRatio) => {
1416                controller.multiplex_ratio = data[0];
1417            }
1418            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306DisplayOffset) => {
1419                controller.display_offset = data[0] & 0x3F;
1420                refresh_visible = true;
1421            }
1422            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306ClockDiv) => {
1423                controller.clock_div = data[0];
1424            }
1425            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306Precharge) => {
1426                controller.precharge = data[0];
1427            }
1428            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306Compins) => {
1429                controller.com_pins = data[0];
1430            }
1431            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306Vcomh) => {
1432                controller.vcomh = data[0];
1433            }
1434            (ControllerRuntime::Ssd1306(controller), RegisterKind::Ssd1306ChargePump) => {
1435                controller.charge_pump = data[0];
1436            }
1437            (ControllerRuntime::Ssd1306(controller), RegisterKind::Raw(cmd)) => {
1438                controller.raw_registers.insert(cmd, data.to_vec());
1439            }
1440            (ControllerRuntime::Generic, _) => {}
1441            (ControllerRuntime::Ili9341(_), _) => {}
1442            (ControllerRuntime::Ssd1306(_), _) => {}
1443        }
1444
1445        if refresh_visible {
1446            self.rebuild_visible_frame()?;
1447        }
1448
1449        Ok(())
1450    }
1451}
1452
1453impl Lcd for VirtualLcd {
1454    type Error = LcdError;
1455
1456    fn init(&mut self) -> Result<()> {
1457        self.hardware_reset();
1458        self.state.initialized = true;
1459        self.state.sleeping = false;
1460        self.state.display_on = true;
1461        self.state.backlight = if self.config.backlight { 100 } else { 0 };
1462        if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1463            controller.brightness = if self.config.backlight { 0xFF } else { 0x00 };
1464        }
1465        self.rebuild_visible_frame()?;
1466        Ok(())
1467    }
1468
1469    fn clear(&mut self, color: Color) -> Result<()> {
1470        self.ensure_ready_for_graphics()?;
1471        self.back_buffer.clear(self.normalize_high_level_color(color));
1472        self.sync_controller_window(DrawWindow::full(&self.config))?;
1473        Ok(())
1474    }
1475
1476    fn draw_pixel(&mut self, x: u16, y: u16, color: Color) -> Result<()> {
1477        self.ensure_ready_for_graphics()?;
1478        let color = self.normalize_high_level_color(color);
1479        self.back_buffer.set_pixel(x, y, color)?;
1480        self.sync_controller_pixel(x, y, color)
1481    }
1482
1483    fn fill_rect(&mut self, x: u16, y: u16, width: u16, height: u16, color: Color) -> Result<()> {
1484        self.ensure_ready_for_graphics()?;
1485        let window = DrawWindow::from_origin(x, y, width, height, &self.config)?;
1486        self.back_buffer
1487            .fill_rect(window, self.normalize_high_level_color(color))?;
1488        self.sync_controller_window(window)
1489    }
1490
1491    fn present(&mut self) -> Result<()> {
1492        self.ensure_ready_for_graphics()?;
1493
1494        if !matches!(self.pending_write, PendingWrite::None) {
1495            return Err(LcdError::BusViolation("cannot present while a bus transaction is active"));
1496        }
1497
1498        self.schedule_visible_update(self.controller.native_frame_bytes(&self.config))
1499    }
1500}
1501
1502impl LcdBus for VirtualLcd {
1503    type Error = LcdError;
1504
1505    fn set_pin(&mut self, pin: PinId, value: bool) -> Result<()> {
1506        self.pins.set(pin, value);
1507
1508        match pin {
1509            PinId::Rst if !value => self.hardware_reset(),
1510            PinId::Bl => {
1511                self.state.backlight = if value { 100 } else { 0 };
1512                self.rebuild_visible_frame()?;
1513            }
1514            _ => {}
1515        }
1516
1517        Ok(())
1518    }
1519
1520    fn write_command(&mut self, cmd: u8) -> Result<()> {
1521        self.validate_bus_access()?;
1522
1523        if !matches!(self.pending_write, PendingWrite::None) {
1524            return Err(LcdError::BusViolation("cannot start a new command before finishing data phase"));
1525        }
1526
1527        self.state.current_command = Some(cmd);
1528
1529        match self.config.controller {
1530            ControllerModel::GenericMipiDcs => match cmd {
1531                0x01 => {
1532                    self.hardware_reset();
1533                    self.state.current_command = Some(cmd);
1534                }
1535                0x11 => {
1536                    self.state.initialized = true;
1537                    self.state.sleeping = false;
1538                }
1539                0x28 => {
1540                    self.ensure_initialized_only()?;
1541                    self.state.display_on = false;
1542                }
1543                0x29 => {
1544                    self.ensure_initialized_only()?;
1545                    self.state.display_on = true;
1546                }
1547                0x2A => {
1548                    self.ensure_initialized_only()?;
1549                    self.pending_write = PendingWrite::Column(AddressAccumulator::new());
1550                }
1551                0x2B => {
1552                    self.ensure_initialized_only()?;
1553                    self.pending_write = PendingWrite::Row(AddressAccumulator::new());
1554                }
1555                0x2C => {
1556                    self.ensure_memory_access()?;
1557                    self.pending_write =
1558                        PendingWrite::MemoryWrite(MemoryWriteProgress::new(self.state.current_window));
1559                }
1560                _ => return Err(LcdError::InvalidCommand(cmd)),
1561            },
1562            ControllerModel::Ili9341 => match cmd {
1563                0x01 => {
1564                    self.hardware_reset();
1565                    self.state.current_command = Some(cmd);
1566                }
1567                0x04 | 0x09 | 0x0A | 0x0B | 0x0C | 0x0F | 0x2E | 0x45 | 0x52 | 0x54 | 0xDA
1568                | 0xDB | 0xDC => {}
1569                0x10 => {
1570                    self.ensure_initialized_only()?;
1571                    self.state.sleeping = true;
1572                    self.rebuild_visible_frame()?;
1573                }
1574                0x11 => {
1575                    self.state.initialized = true;
1576                    self.state.sleeping = false;
1577                    self.rebuild_visible_frame()?;
1578                }
1579                0x13 => {
1580                    self.state.initialized = true;
1581                }
1582                0x20 => {
1583                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1584                        controller.inversion_on = false;
1585                    }
1586                }
1587                0x21 => {
1588                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1589                        controller.inversion_on = true;
1590                    }
1591                }
1592                0x28 => {
1593                    self.ensure_initialized_only()?;
1594                    self.state.display_on = false;
1595                    self.rebuild_visible_frame()?;
1596                }
1597                0x29 => {
1598                    self.ensure_initialized_only()?;
1599                    self.state.display_on = true;
1600                    self.rebuild_visible_frame()?;
1601                }
1602                0x2A => {
1603                    self.ensure_initialized_only()?;
1604                    self.pending_write = PendingWrite::Column(AddressAccumulator::new());
1605                }
1606                0x2B => {
1607                    self.ensure_initialized_only()?;
1608                    self.pending_write = PendingWrite::Row(AddressAccumulator::new());
1609                }
1610                0x2C => {
1611                    self.ensure_memory_access()?;
1612                    self.pending_write =
1613                        PendingWrite::MemoryWrite(MemoryWriteProgress::new(self.state.current_window));
1614                }
1615                0x34 => {
1616                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1617                        controller.tearing_enabled = false;
1618                    }
1619                }
1620                0x35 => {
1621                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1622                        controller.tearing_enabled = true;
1623                        controller.tearing_mode = 0x00;
1624                    }
1625                }
1626                other => {
1627                    if let Some(write) = self.ili9341_register_write_for_command(other) {
1628                        self.pending_write = PendingWrite::Register(write);
1629                    } else {
1630                        return Err(LcdError::InvalidCommand(other));
1631                    }
1632                }
1633            },
1634            ControllerModel::Ssd1306 => {
1635                self.state.initialized = true;
1636                self.state.sleeping = false;
1637
1638                match cmd {
1639                    0x00..=0x0F => {
1640                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1641                            controller.set_page_mode_lower_column(cmd & 0x0F, &self.config);
1642                        }
1643                    }
1644                    0x10..=0x1F => {
1645                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1646                            controller.set_page_mode_upper_column(cmd & 0x0F, &self.config);
1647                        }
1648                    }
1649                    0x20 => {
1650                        self.pending_write = PendingWrite::Register(RegisterWrite {
1651                            register: RegisterKind::Ssd1306MemoryMode,
1652                            allowed_lengths: &[1],
1653                        });
1654                    }
1655                    0x21 => {
1656                        self.pending_write = PendingWrite::Register(RegisterWrite {
1657                            register: RegisterKind::Ssd1306ColumnAddress,
1658                            allowed_lengths: &[2],
1659                        });
1660                    }
1661                    0x22 => {
1662                        self.pending_write = PendingWrite::Register(RegisterWrite {
1663                            register: RegisterKind::Ssd1306PageAddress,
1664                            allowed_lengths: &[2],
1665                        });
1666                    }
1667                    0x26 | 0x27 | 0x29 | 0x2A => {
1668                        self.pending_write = PendingWrite::Register(RegisterWrite {
1669                            register: RegisterKind::Raw(cmd),
1670                            allowed_lengths: &[6],
1671                        });
1672                    }
1673                    0x2E => {
1674                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1675                            controller.scroll_enabled = false;
1676                        }
1677                    }
1678                    0x2F => {
1679                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1680                            controller.scroll_enabled = true;
1681                        }
1682                    }
1683                    0x40..=0x7F => {
1684                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1685                            controller.start_line = cmd & 0x3F;
1686                        }
1687                        self.rebuild_visible_frame()?;
1688                    }
1689                    0x81 => {
1690                        self.pending_write = PendingWrite::Register(RegisterWrite {
1691                            register: RegisterKind::Ssd1306Contrast,
1692                            allowed_lengths: &[1],
1693                        });
1694                    }
1695                    0x8D => {
1696                        self.pending_write = PendingWrite::Register(RegisterWrite {
1697                            register: RegisterKind::Ssd1306ChargePump,
1698                            allowed_lengths: &[1],
1699                        });
1700                    }
1701                    0xA0 => {
1702                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1703                            controller.segment_remap = false;
1704                        }
1705                        self.rebuild_visible_frame()?;
1706                    }
1707                    0xA1 => {
1708                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1709                            controller.segment_remap = true;
1710                        }
1711                        self.rebuild_visible_frame()?;
1712                    }
1713                    0xA3 => {
1714                        self.pending_write = PendingWrite::Register(RegisterWrite {
1715                            register: RegisterKind::Raw(cmd),
1716                            allowed_lengths: &[2],
1717                        });
1718                    }
1719                    0xA4 => {
1720                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1721                            controller.entire_display_on = false;
1722                        }
1723                        self.rebuild_visible_frame()?;
1724                    }
1725                    0xA5 => {
1726                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1727                            controller.entire_display_on = true;
1728                        }
1729                        self.rebuild_visible_frame()?;
1730                    }
1731                    0xA6 => {
1732                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1733                            controller.inverse_display = false;
1734                        }
1735                        self.rebuild_visible_frame()?;
1736                    }
1737                    0xA7 => {
1738                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1739                            controller.inverse_display = true;
1740                        }
1741                        self.rebuild_visible_frame()?;
1742                    }
1743                    0xA8 => {
1744                        self.pending_write = PendingWrite::Register(RegisterWrite {
1745                            register: RegisterKind::Ssd1306MultiplexRatio,
1746                            allowed_lengths: &[1],
1747                        });
1748                    }
1749                    0xAE => {
1750                        self.state.display_on = false;
1751                        self.rebuild_visible_frame()?;
1752                    }
1753                    0xAF => {
1754                        self.state.display_on = true;
1755                        self.rebuild_visible_frame()?;
1756                    }
1757                    0xB0..=0xB7 => {
1758                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1759                            controller.set_page_mode_page(cmd & 0x0F, &self.config);
1760                        }
1761                    }
1762                    0xC0 => {
1763                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1764                            controller.com_scan_reverse = false;
1765                        }
1766                        self.rebuild_visible_frame()?;
1767                    }
1768                    0xC8 => {
1769                        if let ControllerRuntime::Ssd1306(controller) = &mut self.controller {
1770                            controller.com_scan_reverse = true;
1771                        }
1772                        self.rebuild_visible_frame()?;
1773                    }
1774                    0xD3 => {
1775                        self.pending_write = PendingWrite::Register(RegisterWrite {
1776                            register: RegisterKind::Ssd1306DisplayOffset,
1777                            allowed_lengths: &[1],
1778                        });
1779                    }
1780                    0xD5 => {
1781                        self.pending_write = PendingWrite::Register(RegisterWrite {
1782                            register: RegisterKind::Ssd1306ClockDiv,
1783                            allowed_lengths: &[1],
1784                        });
1785                    }
1786                    0xD9 => {
1787                        self.pending_write = PendingWrite::Register(RegisterWrite {
1788                            register: RegisterKind::Ssd1306Precharge,
1789                            allowed_lengths: &[1],
1790                        });
1791                    }
1792                    0xDA => {
1793                        self.pending_write = PendingWrite::Register(RegisterWrite {
1794                            register: RegisterKind::Ssd1306Compins,
1795                            allowed_lengths: &[1],
1796                        });
1797                    }
1798                    0xDB => {
1799                        self.pending_write = PendingWrite::Register(RegisterWrite {
1800                            register: RegisterKind::Ssd1306Vcomh,
1801                            allowed_lengths: &[1],
1802                        });
1803                    }
1804                    0xE3 => {}
1805                    other => return Err(LcdError::InvalidCommand(other)),
1806                }
1807            }
1808        }
1809
1810        Ok(())
1811    }
1812
1813    fn write_data(&mut self, data: &[u8]) -> Result<()> {
1814        self.validate_bus_access()?;
1815
1816        let pending = std::mem::replace(&mut self.pending_write, PendingWrite::None);
1817        match pending {
1818            PendingWrite::None => {
1819                if matches!(self.controller, ControllerRuntime::Ssd1306(_)) {
1820                    let transferred = self.process_ssd1306_ram_write(data)?;
1821                    if !self.has_pending_frame() {
1822                        self.schedule_visible_update(transferred)?;
1823                    }
1824                    Ok(())
1825                } else {
1826                    Err(LcdError::BusViolation("data write without an active command"))
1827                }
1828            }
1829            PendingWrite::Column(mut accumulator) => {
1830                let consumed = self.process_address_data(&mut accumulator, data, true)?;
1831                if consumed != data.len() {
1832                    return Err(LcdError::InvalidDataLength {
1833                        expected: 4 - accumulator.len,
1834                        got: data.len() - consumed,
1835                    });
1836                }
1837
1838                if !accumulator.complete() {
1839                    self.pending_write = PendingWrite::Column(accumulator);
1840                }
1841
1842                Ok(())
1843            }
1844            PendingWrite::Row(mut accumulator) => {
1845                let consumed = self.process_address_data(&mut accumulator, data, false)?;
1846                if consumed != data.len() {
1847                    return Err(LcdError::InvalidDataLength {
1848                        expected: 4 - accumulator.len,
1849                        got: data.len() - consumed,
1850                    });
1851                }
1852
1853                if !accumulator.complete() {
1854                    self.pending_write = PendingWrite::Row(accumulator);
1855                }
1856
1857                Ok(())
1858            }
1859            PendingWrite::Register(write) => self.process_register_write(write, data),
1860            PendingWrite::MemoryWrite(mut progress) => {
1861                self.process_memory_write(&mut progress, data)?;
1862                if progress.finished() {
1863                    self.schedule_visible_update(progress.transferred_bytes)?;
1864                } else {
1865                    self.pending_write = PendingWrite::MemoryWrite(progress);
1866                }
1867                Ok(())
1868            }
1869        }
1870    }
1871
1872    fn read_data(&mut self, len: usize) -> Result<Vec<u8>> {
1873        self.validate_bus_access()?;
1874        self.build_read_response(len)
1875    }
1876}
1877
1878impl VirtualLcd {
1879    fn ili9341_register_write_for_command(&self, cmd: u8) -> Option<RegisterWrite> {
1880        let allowed_lengths: &'static [usize] = match cmd {
1881            0x26 | 0x36 | 0x3A | 0x51 | 0x53 | 0x55 | 0x56 | 0xB0 | 0xB7 | 0xC0 | 0xC1
1882            | 0xC7 | 0xF2 | 0xF7 => &[1],
1883            0x37 | 0x44 | 0xB1 | 0xC5 | 0xEA => &[2],
1884            0xE8 | 0xF6 => &[3],
1885            0xB5 | 0xED => &[4],
1886            0xCB => &[5],
1887            0x33 => &[6],
1888            0xCF => &[3],
1889            0xB6 => &[3, 4],
1890            0xE0 | 0xE1 => &[15],
1891            _ => return None,
1892        };
1893
1894        let register = match cmd {
1895            0x36 => RegisterKind::Madctl,
1896            0x3A => RegisterKind::Colmod,
1897            0x33 => RegisterKind::VerticalScrollDefinition,
1898            0x37 => RegisterKind::VerticalScrollStart,
1899            0x51 => RegisterKind::Brightness,
1900            0x53 => RegisterKind::ControlDisplay,
1901            0xF6 => RegisterKind::InterfaceControl,
1902            other => RegisterKind::Raw(other),
1903        };
1904
1905        Some(RegisterWrite {
1906            register,
1907            allowed_lengths,
1908        })
1909    }
1910
1911    fn build_read_response(&self, len: usize) -> Result<Vec<u8>> {
1912        let mut response = match (&self.controller, self.state.current_command) {
1913            (_, Some(0x04)) => vec![0x00, 0x00, 0x93, 0x41],
1914            (ControllerRuntime::Ili9341(controller), Some(0x09)) => {
1915                vec![0x00, 0x00, controller.power_mode(&self.state), controller.madctl, controller.colmod]
1916            }
1917            (ControllerRuntime::Ili9341(controller), Some(0x0A)) => {
1918                vec![0x00, controller.power_mode(&self.state)]
1919            }
1920            (ControllerRuntime::Ili9341(controller), Some(0x0B)) => vec![0x00, controller.madctl],
1921            (ControllerRuntime::Ili9341(controller), Some(0x0C)) => vec![0x00, controller.colmod],
1922            (ControllerRuntime::Ili9341(_), Some(0x0F)) => vec![0x00, 0xC0],
1923            (ControllerRuntime::Ili9341(_), Some(0x45)) => vec![0x00, 0x00, 0x00],
1924            (ControllerRuntime::Ili9341(controller), Some(0x52)) => vec![0x00, controller.brightness],
1925            (ControllerRuntime::Ili9341(controller), Some(0x54)) => {
1926                vec![0x00, controller.control_display]
1927            }
1928            (_, Some(0xDA)) => vec![0x00],
1929            (_, Some(0xDB)) => vec![0x93],
1930            (_, Some(0xDC)) => vec![0x41],
1931            (ControllerRuntime::Ili9341(controller), Some(0x2E)) => {
1932                self.build_ili9341_memory_read(controller, len)
1933            }
1934            _ => vec![0x00; len],
1935        };
1936
1937        response.resize(len, 0x00);
1938        Ok(response)
1939    }
1940
1941    fn build_ili9341_memory_read(&self, controller: &Ili9341State, len: usize) -> Vec<u8> {
1942        let window = self.state.current_window;
1943        let bytes_per_pixel = controller.interface_pixel_format().bytes_per_pixel();
1944        let mut out = Vec::with_capacity(len.max(1));
1945        out.push(0x00);
1946
1947        for index in 0..window.area() {
1948            if out.len() >= len {
1949                break;
1950            }
1951
1952            if let Ok((x, y)) = controller.write_pixel_coords(window, index, &self.config) {
1953                let color = self.back_buffer.get_pixel(x, y).unwrap_or(Color::BLACK);
1954                match controller.interface_pixel_format() {
1955                    PixelFormat::Rgb565 => {
1956                        let bytes = color.to_rgb565().to_be_bytes();
1957                        out.extend_from_slice(&bytes);
1958                    }
1959                    PixelFormat::Rgb888 => {
1960                        out.push(color.r & 0xFC);
1961                        out.push(color.g & 0xFC);
1962                        out.push(color.b & 0xFC);
1963                    }
1964                    format => {
1965                        let mut raw = [0u8; 3];
1966                        raw[..format.bytes_per_pixel()]
1967                            .copy_from_slice(&[color.r, color.g, color.b][..format.bytes_per_pixel()]);
1968                        out.extend_from_slice(&raw[..bytes_per_pixel]);
1969                    }
1970                }
1971            }
1972        }
1973
1974        out.truncate(len);
1975        out
1976    }
1977
1978    fn ensure_initialized_only(&self) -> Result<()> {
1979        if !self.state.initialized {
1980            return Err(LcdError::NotInitialized);
1981        }
1982
1983        Ok(())
1984    }
1985}
1986
1987#[derive(Debug, Clone, PartialEq, Eq)]
1988pub enum LcdError {
1989    InvalidConfig(&'static str),
1990    NotInitialized,
1991    DisplayOff,
1992    SleepMode,
1993    InvalidWindow,
1994    OutOfBounds,
1995    InvalidCommand(u8),
1996    InvalidDataLength { expected: usize, got: usize },
1997    BusViolation(&'static str),
1998    FrameRateExceeded,
1999}
2000
2001impl Display for LcdError {
2002    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2003        match self {
2004            Self::InvalidConfig(message) => write!(f, "invalid config: {message}"),
2005            Self::NotInitialized => f.write_str("display is not initialized"),
2006            Self::DisplayOff => f.write_str("display is off"),
2007            Self::SleepMode => f.write_str("display is in sleep mode"),
2008            Self::InvalidWindow => f.write_str("invalid address window"),
2009            Self::OutOfBounds => f.write_str("coordinates are out of bounds"),
2010            Self::InvalidCommand(cmd) => write!(f, "invalid command 0x{cmd:02X}"),
2011            Self::InvalidDataLength { expected, got } => {
2012                write!(f, "invalid data length: expected {expected} bytes, got {got}")
2013            }
2014            Self::BusViolation(message) => write!(f, "bus violation: {message}"),
2015            Self::FrameRateExceeded => f.write_str("frame submitted before the previous transfer completed"),
2016        }
2017    }
2018}
2019
2020impl Error for LcdError {}
2021
2022fn max_instant(left: Instant, right: Instant) -> Instant {
2023    if left >= right { left } else { right }
2024}
2025
2026#[cfg(test)]
2027mod tests {
2028    use super::*;
2029    use std::thread;
2030
2031    fn fast_config() -> LcdConfig {
2032        LcdConfig {
2033            width: 4,
2034            height: 4,
2035            pixel_format: PixelFormat::Rgb565,
2036            fps: 1_000,
2037            interface: InterfaceType::Spi4Wire,
2038            orientation: 0,
2039            vsync: false,
2040            buffering: BufferingMode::Double,
2041            backlight: true,
2042            tearing_effect: false,
2043            bus_hz: 32_000_000,
2044            controller: ControllerModel::Ili9341,
2045        }
2046    }
2047
2048    fn fast_ssd1306_config() -> LcdConfig {
2049        LcdConfig {
2050            width: 8,
2051            height: 8,
2052            pixel_format: PixelFormat::Mono1,
2053            fps: 1_000,
2054            interface: InterfaceType::Spi4Wire,
2055            orientation: 0,
2056            vsync: false,
2057            buffering: BufferingMode::Double,
2058            backlight: true,
2059            tearing_effect: false,
2060            bus_hz: 32_000_000,
2061            controller: ControllerModel::Ssd1306,
2062        }
2063    }
2064
2065    fn wait_until_visible(lcd: &mut VirtualLcd) {
2066        for _ in 0..16 {
2067            if lcd.tick() {
2068                return;
2069            }
2070            thread::sleep(Duration::from_millis(1));
2071        }
2072    }
2073
2074    fn bus_ready_ili9341() -> VirtualLcd {
2075        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
2076        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
2077        lcd.write_command(0x11).expect("sleep out should succeed");
2078        lcd.write_command(0x29).expect("display on should succeed");
2079        lcd
2080    }
2081
2082    fn bus_ready_ssd1306() -> VirtualLcd {
2083        let mut lcd = VirtualLcd::new(fast_ssd1306_config()).expect("config should be valid");
2084        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
2085        lcd.write_command(0xAE).expect("display off should succeed");
2086        write_command_with_data(&mut lcd, 0x20, &[0x02]);
2087        lcd.write_command(0xAF).expect("display on should succeed");
2088        lcd
2089    }
2090
2091    fn write_command_with_data(lcd: &mut VirtualLcd, cmd: u8, data: &[u8]) {
2092        lcd.write_command(cmd).expect("command should succeed");
2093        lcd.write_data(data)
2094            .unwrap_or_else(|error| panic!("data for command 0x{cmd:02X} should succeed: {error:?}"));
2095    }
2096
2097    #[test]
2098    fn high_level_draw_requires_present() {
2099        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
2100        lcd.init().expect("init should succeed");
2101        lcd.draw_pixel(1, 2, Color::WHITE)
2102            .expect("pixel draw should succeed");
2103
2104        assert_eq!(lcd.visible_frame().get_pixel(1, 2), Some(Color::BLACK));
2105
2106        lcd.present().expect("present should schedule a frame");
2107        wait_until_visible(&mut lcd);
2108
2109        assert_eq!(lcd.visible_frame().get_pixel(1, 2), Some(Color::WHITE));
2110    }
2111
2112    #[test]
2113    fn low_level_memory_write_updates_window() {
2114        let mut lcd = bus_ready_ili9341();
2115        write_command_with_data(&mut lcd, 0x3A, &[0x55]);
2116
2117        lcd.write_command(0x2A).expect("column command should succeed");
2118        lcd.write_data(&[0x00, 0x00, 0x00, 0x01])
2119            .expect("column data should succeed");
2120        lcd.write_command(0x2B).expect("row command should succeed");
2121        lcd.write_data(&[0x00, 0x00, 0x00, 0x00])
2122            .expect("row data should succeed");
2123        lcd.write_command(0x2C).expect("memory write should start");
2124
2125        let red = Color::RED.to_rgb565().to_be_bytes();
2126        let green = Color::GREEN.to_rgb565().to_be_bytes();
2127        let mut pixels = Vec::new();
2128        pixels.extend_from_slice(&red);
2129        pixels.extend_from_slice(&green);
2130        lcd.write_data(&pixels).expect("pixel payload should succeed");
2131
2132        wait_until_visible(&mut lcd);
2133
2134        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::RED));
2135        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::GREEN));
2136    }
2137
2138    #[test]
2139    fn ili9341_common_init_sequence_is_accepted() {
2140        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
2141        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
2142
2143        write_command_with_data(&mut lcd, 0xCB, &[0x39, 0x2C, 0x00, 0x34, 0x02]);
2144        write_command_with_data(&mut lcd, 0xCF, &[0x00, 0xC1, 0x30]);
2145        write_command_with_data(&mut lcd, 0xE8, &[0x85, 0x00, 0x78]);
2146        write_command_with_data(&mut lcd, 0xEA, &[0x00, 0x00]);
2147        write_command_with_data(&mut lcd, 0xED, &[0x64, 0x03, 0x12, 0x81]);
2148        write_command_with_data(&mut lcd, 0xF7, &[0x20]);
2149        write_command_with_data(&mut lcd, 0xC0, &[0x23]);
2150        write_command_with_data(&mut lcd, 0xC1, &[0x10]);
2151        write_command_with_data(&mut lcd, 0xC5, &[0x3E, 0x28]);
2152        write_command_with_data(&mut lcd, 0xC7, &[0x86]);
2153        write_command_with_data(&mut lcd, 0xB1, &[0x00, 0x18]);
2154        write_command_with_data(&mut lcd, 0xB6, &[0x08, 0x82, 0x27]);
2155        write_command_with_data(&mut lcd, 0xF2, &[0x00]);
2156        write_command_with_data(&mut lcd, 0x26, &[0x01]);
2157        write_command_with_data(
2158            &mut lcd,
2159            0xE0,
2160            &[0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00],
2161        );
2162        write_command_with_data(
2163            &mut lcd,
2164            0xE1,
2165            &[0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F],
2166        );
2167        lcd.write_command(0x11).expect("sleep out should succeed");
2168        write_command_with_data(&mut lcd, 0x3A, &[0x55]);
2169        write_command_with_data(&mut lcd, 0x36, &[0x48]);
2170        lcd.write_command(0x29).expect("display on should succeed");
2171    }
2172
2173    #[test]
2174    fn ili9341_read_commands_expose_id_and_pixel_format() {
2175        let mut lcd = bus_ready_ili9341();
2176        write_command_with_data(&mut lcd, 0x3A, &[0x55]);
2177
2178        lcd.write_command(0x04).expect("read id command should succeed");
2179        assert_eq!(lcd.read_data(4).expect("id read should succeed"), vec![0x00, 0x00, 0x93, 0x41]);
2180
2181        lcd.write_command(0x0C).expect("read colmod should succeed");
2182        assert_eq!(lcd.read_data(2).expect("colmod read should succeed"), vec![0x00, 0x55]);
2183    }
2184
2185    #[test]
2186    fn ili9341_madctl_rotation_changes_visible_mapping() {
2187        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
2188        lcd.init().expect("init should succeed");
2189        lcd.draw_pixel(1, 0, Color::RED).expect("pixel draw should succeed");
2190        lcd.present().expect("present should succeed");
2191        wait_until_visible(&mut lcd);
2192
2193        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::RED));
2194
2195        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
2196        write_command_with_data(&mut lcd, 0x36, &[0x20]);
2197
2198        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::BLACK));
2199        assert_eq!(lcd.visible_frame().get_pixel(0, 1), Some(Color::RED));
2200    }
2201
2202    #[test]
2203    fn ili9341_vertical_scroll_repositions_visible_rows() {
2204        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
2205        lcd.init().expect("init should succeed");
2206
2207        lcd.fill_rect(0, 0, 4, 1, Color::RED).expect("row 0");
2208        lcd.fill_rect(0, 1, 4, 1, Color::GREEN).expect("row 1");
2209        lcd.fill_rect(0, 2, 4, 1, Color::BLUE).expect("row 2");
2210        lcd.fill_rect(0, 3, 4, 1, Color::WHITE).expect("row 3");
2211        lcd.present().expect("present should succeed");
2212        wait_until_visible(&mut lcd);
2213
2214        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
2215        write_command_with_data(&mut lcd, 0x33, &[0x00, 0x00, 0x00, 0x04, 0x00, 0x00]);
2216        write_command_with_data(&mut lcd, 0x37, &[0x00, 0x01]);
2217
2218        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::GREEN));
2219        assert_eq!(lcd.visible_frame().get_pixel(0, 1), Some(Color::BLUE));
2220        assert_eq!(lcd.visible_frame().get_pixel(0, 2), Some(Color::WHITE));
2221        assert_eq!(lcd.visible_frame().get_pixel(0, 3), Some(Color::RED));
2222    }
2223
2224    #[test]
2225    fn ssd1306_common_init_sequence_is_accepted() {
2226        let mut lcd = VirtualLcd::new(fast_ssd1306_config()).expect("config should be valid");
2227        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
2228
2229        lcd.write_command(0xAE).expect("display off should succeed");
2230        write_command_with_data(&mut lcd, 0xD5, &[0x80]);
2231        write_command_with_data(&mut lcd, 0xA8, &[0x3F]);
2232        write_command_with_data(&mut lcd, 0xD3, &[0x00]);
2233        lcd.write_command(0x40).expect("start line should succeed");
2234        write_command_with_data(&mut lcd, 0x8D, &[0x14]);
2235        write_command_with_data(&mut lcd, 0x20, &[0x00]);
2236        lcd.write_command(0xA1).expect("segment remap should succeed");
2237        lcd.write_command(0xC8).expect("com scan reverse should succeed");
2238        write_command_with_data(&mut lcd, 0xDA, &[0x12]);
2239        write_command_with_data(&mut lcd, 0x81, &[0xCF]);
2240        write_command_with_data(&mut lcd, 0xD9, &[0xF1]);
2241        write_command_with_data(&mut lcd, 0xDB, &[0x40]);
2242        lcd.write_command(0xA4).expect("display follow ram should succeed");
2243        lcd.write_command(0xA6).expect("normal display should succeed");
2244        lcd.write_command(0xAF).expect("display on should succeed");
2245    }
2246
2247    #[test]
2248    fn ssd1306_page_writes_update_mono_pixels() {
2249        let mut lcd = bus_ready_ssd1306();
2250
2251        lcd.write_command(0xB0).expect("page select should succeed");
2252        lcd.write_command(0x00).expect("lower column should succeed");
2253        lcd.write_command(0x10).expect("upper column should succeed");
2254        lcd.write_data(&[0b0000_0011, 0b0000_0100])
2255            .expect("gddram write should succeed");
2256
2257        wait_until_visible(&mut lcd);
2258
2259        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::WHITE));
2260        assert_eq!(lcd.visible_frame().get_pixel(0, 1), Some(Color::WHITE));
2261        assert_eq!(lcd.visible_frame().get_pixel(1, 2), Some(Color::WHITE));
2262        assert_eq!(lcd.visible_frame().get_pixel(1, 1), Some(Color::BLACK));
2263    }
2264
2265    #[test]
2266    fn ssd1306_high_level_drawing_quantizes_to_monochrome() {
2267        let mut lcd = VirtualLcd::new(fast_ssd1306_config()).expect("config should be valid");
2268        lcd.init().expect("init should succeed");
2269
2270        lcd.draw_pixel(0, 0, Color::rgb(240, 240, 240))
2271            .expect("bright pixel should succeed");
2272        lcd.draw_pixel(1, 0, Color::rgb(20, 20, 20))
2273            .expect("dark pixel should succeed");
2274        lcd.present().expect("present should succeed");
2275        wait_until_visible(&mut lcd);
2276
2277        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::WHITE));
2278        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::BLACK));
2279    }
2280
2281    #[test]
2282    fn ssd1306_display_start_line_and_remap_affect_visible_output() {
2283        let mut lcd = bus_ready_ssd1306();
2284        lcd.write_command(0xB0).expect("page select should succeed");
2285        lcd.write_command(0x00).expect("lower column should succeed");
2286        lcd.write_command(0x10).expect("upper column should succeed");
2287        lcd.write_data(&[0b0000_0001]).expect("gddram write should succeed");
2288        wait_until_visible(&mut lcd);
2289
2290        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::WHITE));
2291
2292        lcd.write_command(0x41).expect("start line shift should succeed");
2293        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::BLACK));
2294        assert_eq!(lcd.visible_frame().get_pixel(0, 7), Some(Color::WHITE));
2295
2296        lcd.write_command(0xA1).expect("segment remap should succeed");
2297        assert_eq!(lcd.visible_frame().get_pixel(7, 7), Some(Color::WHITE));
2298    }
2299
2300    #[test]
2301    fn invalid_config_rejects_zero_dimensions() {
2302        let mut config = fast_config();
2303        config.width = 0;
2304
2305        assert!(matches!(
2306            VirtualLcd::new(config),
2307            Err(LcdError::InvalidConfig("display dimensions must be non-zero"))
2308        ));
2309    }
2310
2311    #[test]
2312    fn invalid_ssd1306_config_rejects_non_paged_height() {
2313        let mut config = fast_ssd1306_config();
2314        config.height = 7;
2315
2316        assert!(matches!(
2317            VirtualLcd::new(config),
2318            Err(LcdError::InvalidConfig("ssd1306 height must be a multiple of 8"))
2319        ));
2320    }
2321
2322    #[test]
2323    fn present_rejects_new_frame_while_previous_one_is_pending() {
2324        let mut config = fast_config();
2325        config.bus_hz = 1;
2326
2327        let mut lcd = VirtualLcd::new(config).expect("config should be valid");
2328        lcd.init().expect("init should succeed");
2329        lcd.present().expect("first frame should be scheduled");
2330
2331        assert!(lcd.has_pending_frame());
2332        assert_eq!(lcd.present(), Err(LcdError::FrameRateExceeded));
2333    }
2334
2335    #[test]
2336    fn write_data_without_command_reports_bus_violation() {
2337        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
2338        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
2339
2340        assert_eq!(
2341            lcd.write_data(&[0x12]),
2342            Err(LcdError::BusViolation("data write without an active command"))
2343        );
2344    }
2345
2346    #[test]
2347    fn write_pixels_requires_window_sized_payload() {
2348        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
2349        lcd.init().expect("init should succeed");
2350        lcd.set_window(0, 0, 2, 2).expect("window should be valid");
2351
2352        assert_eq!(
2353            lcd.write_pixels(&[Color::WHITE; 3]),
2354            Err(LcdError::InvalidDataLength {
2355                expected: 4,
2356                got: 3,
2357            })
2358        );
2359    }
2360}