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