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}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct LcdConfig {
20    pub width: u16,
21    pub height: u16,
22    pub pixel_format: PixelFormat,
23    pub fps: u16,
24    pub interface: InterfaceType,
25    pub orientation: u16,
26    pub vsync: bool,
27    pub buffering: BufferingMode,
28    pub backlight: bool,
29    pub tearing_effect: bool,
30    pub bus_hz: u32,
31    pub controller: ControllerModel,
32}
33
34impl Default for LcdConfig {
35    fn default() -> Self {
36        Self {
37            width: 320,
38            height: 240,
39            pixel_format: PixelFormat::Rgb565,
40            fps: 30,
41            interface: InterfaceType::Spi4Wire,
42            orientation: 0,
43            vsync: true,
44            buffering: BufferingMode::Double,
45            backlight: true,
46            tearing_effect: false,
47            bus_hz: 8_000_000,
48            controller: ControllerModel::Ili9341,
49        }
50    }
51}
52
53impl LcdConfig {
54    fn validate(&self) -> Result<()> {
55        if self.width == 0 || self.height == 0 {
56            return Err(LcdError::InvalidConfig("display dimensions must be non-zero"));
57        }
58
59        if self.fps == 0 {
60            return Err(LcdError::InvalidConfig("fps must be non-zero"));
61        }
62
63        if self.bus_hz == 0 {
64            return Err(LcdError::InvalidConfig("bus_hz must be non-zero"));
65        }
66
67        Ok(())
68    }
69
70    pub fn frame_interval(&self) -> Duration {
71        Duration::from_secs_f64(1.0 / self.fps as f64)
72    }
73
74    pub fn full_frame_bytes(&self) -> usize {
75        self.width as usize * self.height as usize * self.pixel_format.bytes_per_pixel()
76    }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum PixelFormat {
81    Mono1,
82    Gray8,
83    Rgb565,
84    Rgb888,
85}
86
87impl PixelFormat {
88    pub fn bytes_per_pixel(self) -> usize {
89        match self {
90            Self::Mono1 | Self::Gray8 => 1,
91            Self::Rgb565 => 2,
92            Self::Rgb888 => 3,
93        }
94    }
95
96    fn decode_color(self, bytes: &[u8]) -> Color {
97        match self {
98            Self::Mono1 => {
99                if bytes[0] == 0 {
100                    Color::BLACK
101                } else {
102                    Color::WHITE
103                }
104            }
105            Self::Gray8 => Color::rgb(bytes[0], bytes[0], bytes[0]),
106            Self::Rgb565 => {
107                let value = u16::from_be_bytes([bytes[0], bytes[1]]);
108                Color::from_rgb565(value)
109            }
110            Self::Rgb888 => Color::rgb(bytes[0], bytes[1], bytes[2]),
111        }
112    }
113}
114
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub enum InterfaceType {
117    Spi4Wire,
118    Spi3Wire,
119    Parallel8080,
120    MemoryMapped,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum BufferingMode {
125    Single,
126    Double,
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub struct DrawWindow {
131    pub x: u16,
132    pub y: u16,
133    pub width: u16,
134    pub height: u16,
135}
136
137impl DrawWindow {
138    pub fn full(config: &LcdConfig) -> Self {
139        Self {
140            x: 0,
141            y: 0,
142            width: config.width,
143            height: config.height,
144        }
145    }
146
147    pub fn from_origin(x: u16, y: u16, width: u16, height: u16, config: &LcdConfig) -> Result<Self> {
148        if width == 0 || height == 0 {
149            return Err(LcdError::InvalidWindow);
150        }
151
152        let x_end = x
153            .checked_add(width - 1)
154            .ok_or(LcdError::OutOfBounds)?;
155        let y_end = y
156            .checked_add(height - 1)
157            .ok_or(LcdError::OutOfBounds)?;
158
159        if x_end >= config.width || y_end >= config.height {
160            return Err(LcdError::OutOfBounds);
161        }
162
163        Ok(Self {
164            x,
165            y,
166            width,
167            height,
168        })
169    }
170
171    pub fn from_inclusive(x0: u16, y0: u16, x1: u16, y1: u16, config: &LcdConfig) -> Result<Self> {
172        if x1 < x0 || y1 < y0 {
173            return Err(LcdError::InvalidWindow);
174        }
175
176        Self::from_origin(x0, y0, x1 - x0 + 1, y1 - y0 + 1, config)
177    }
178
179    pub fn area(self) -> usize {
180        self.width as usize * self.height as usize
181    }
182}
183
184#[derive(Clone, Debug)]
185pub struct LcdState {
186    pub initialized: bool,
187    pub sleeping: bool,
188    pub display_on: bool,
189    pub backlight: u8,
190    pub current_window: DrawWindow,
191    pub current_command: Option<u8>,
192    column_range: (u16, u16),
193    row_range: (u16, u16),
194}
195
196impl LcdState {
197    fn new(config: &LcdConfig) -> Self {
198        let full = DrawWindow::full(config);
199        Self {
200            initialized: false,
201            sleeping: true,
202            display_on: false,
203            backlight: if config.backlight { 100 } else { 0 },
204            current_window: full,
205            current_command: None,
206            column_range: (0, config.width - 1),
207            row_range: (0, config.height - 1),
208        }
209    }
210
211    fn set_column_range(&mut self, start: u16, end: u16) {
212        self.column_range = (start, end);
213        self.sync_window();
214    }
215
216    fn set_row_range(&mut self, start: u16, end: u16) {
217        self.row_range = (start, end);
218        self.sync_window();
219    }
220
221    fn sync_window(&mut self) {
222        self.current_window = DrawWindow {
223            x: self.column_range.0,
224            y: self.row_range.0,
225            width: self.column_range.1 - self.column_range.0 + 1,
226            height: self.row_range.1 - self.row_range.0 + 1,
227        };
228    }
229}
230
231#[derive(Debug)]
232enum ControllerRuntime {
233    Generic,
234    Ili9341(Ili9341State),
235}
236
237impl ControllerRuntime {
238    fn new(model: ControllerModel, config: &LcdConfig) -> Self {
239        match model {
240            ControllerModel::GenericMipiDcs => Self::Generic,
241            ControllerModel::Ili9341 => Self::Ili9341(Ili9341State::new(config)),
242        }
243    }
244
245    fn reset(&mut self, config: &LcdConfig) {
246        if let Self::Ili9341(state) = self {
247            *state = Ili9341State::new(config);
248        }
249    }
250
251    fn visible_bytes_per_pixel(&self, fallback: PixelFormat) -> usize {
252        match self {
253            Self::Generic => fallback.bytes_per_pixel(),
254            Self::Ili9341(state) => state.interface_pixel_format().bytes_per_pixel(),
255        }
256    }
257}
258
259#[derive(Debug)]
260struct Ili9341State {
261    madctl: u8,
262    colmod: u8,
263    inversion_on: bool,
264    tearing_enabled: bool,
265    tearing_mode: u8,
266    brightness: u8,
267    control_display: u8,
268    scroll: VerticalScrollState,
269    interface_control: [u8; 3],
270    raw_registers: BTreeMap<u8, Vec<u8>>,
271}
272
273impl Ili9341State {
274    const MADCTL_MY: u8 = 0x80;
275    const MADCTL_MX: u8 = 0x40;
276    const MADCTL_MV: u8 = 0x20;
277    const MADCTL_BGR: u8 = 0x08;
278
279    fn new(config: &LcdConfig) -> Self {
280        Self {
281            madctl: 0x00,
282            colmod: 0x66,
283            inversion_on: false,
284            tearing_enabled: config.tearing_effect,
285            tearing_mode: 0x00,
286            brightness: if config.backlight { 0xFF } else { 0x00 },
287            control_display: 0x24,
288            scroll: VerticalScrollState::new(config.height),
289            interface_control: [0x01, 0x00, 0x00],
290            raw_registers: BTreeMap::new(),
291        }
292    }
293
294    fn interface_pixel_format(&self) -> PixelFormat {
295        match self.colmod & 0x07 {
296            0x05 => PixelFormat::Rgb565,
297            0x06 => PixelFormat::Rgb888,
298            _ => PixelFormat::Rgb565,
299        }
300    }
301
302    fn decode_interface_color(&self, bytes: &[u8]) -> Color {
303        match self.interface_pixel_format() {
304            PixelFormat::Rgb565 => PixelFormat::Rgb565.decode_color(bytes),
305            PixelFormat::Rgb888 => {
306                let expand = |value: u8| (value << 2) | (value >> 4);
307                Color::rgb(expand(bytes[0]), expand(bytes[1]), expand(bytes[2]))
308            }
309            other => other.decode_color(bytes),
310        }
311    }
312
313    fn map_logical_to_memory(&self, x: u16, y: u16, config: &LcdConfig) -> Result<(u16, u16)> {
314        let width = config.width;
315        let height = config.height;
316
317        let logical_y = self.scroll.map_visible_row(y, height);
318        let mx = self.madctl & Self::MADCTL_MX != 0;
319        let my = self.madctl & Self::MADCTL_MY != 0;
320        let mv = self.madctl & Self::MADCTL_MV != 0;
321
322        let (mem_x, mem_y) = if mv {
323            let mem_x = if mx {
324                width
325                    .checked_sub(logical_y + 1)
326                    .ok_or(LcdError::OutOfBounds)?
327            } else {
328                logical_y
329            };
330            let mem_y = if my {
331                height.checked_sub(x + 1).ok_or(LcdError::OutOfBounds)?
332            } else {
333                x
334            };
335            (mem_x, mem_y)
336        } else {
337            let mem_x = if mx {
338                width.checked_sub(x + 1).ok_or(LcdError::OutOfBounds)?
339            } else {
340                x
341            };
342            let mem_y = if my {
343                height
344                    .checked_sub(logical_y + 1)
345                    .ok_or(LcdError::OutOfBounds)?
346            } else {
347                logical_y
348            };
349            (mem_x, mem_y)
350        };
351
352        if mem_x >= width || mem_y >= height {
353            return Err(LcdError::OutOfBounds);
354        }
355
356        Ok((mem_x, mem_y))
357    }
358
359    fn write_pixel_coords(
360        &self,
361        window: DrawWindow,
362        next_pixel: usize,
363        config: &LcdConfig,
364    ) -> Result<(u16, u16)> {
365        let dx = (next_pixel % window.width as usize) as u16;
366        let dy = (next_pixel / window.width as usize) as u16;
367        self.map_logical_to_memory(window.x + dx, window.y + dy, config)
368    }
369
370    fn apply_visible_transform(
371        &self,
372        memory: &Framebuffer,
373        visible: &mut Framebuffer,
374        state: &LcdState,
375        config: &LcdConfig,
376    ) -> Result<()> {
377        if !state.display_on || state.sleeping || state.backlight == 0 || self.brightness == 0 {
378            visible.clear(Color::BLACK);
379            return Ok(());
380        }
381
382        for y in 0..config.height {
383            for x in 0..config.width {
384                let (mem_x, mem_y) = self.map_logical_to_memory(x, y, config)?;
385                let mut color = memory.get_pixel(mem_x, mem_y).unwrap_or(Color::BLACK);
386                if self.madctl & Self::MADCTL_BGR != 0 {
387                    color = Color::rgb(color.b, color.g, color.r);
388                }
389                visible.set_pixel(x, y, color)?;
390            }
391        }
392
393        Ok(())
394    }
395
396    fn power_mode(&self, state: &LcdState) -> u8 {
397        let mut mode = 0u8;
398        if !state.sleeping {
399            mode |= 0x08;
400        }
401        if state.display_on {
402            mode |= 0x04;
403        }
404        if self.interface_pixel_format() == PixelFormat::Rgb565 {
405            mode |= 0x02;
406        }
407        if state.initialized {
408            mode |= 0x80;
409        }
410        mode
411    }
412}
413
414#[derive(Debug)]
415struct VerticalScrollState {
416    top_fixed_area: u16,
417    scroll_area: u16,
418    bottom_fixed_area: u16,
419    start_address: u16,
420}
421
422impl VerticalScrollState {
423    fn new(height: u16) -> Self {
424        Self {
425            top_fixed_area: 0,
426            scroll_area: height,
427            bottom_fixed_area: 0,
428            start_address: 0,
429        }
430    }
431
432    fn map_visible_row(&self, row: u16, total_height: u16) -> u16 {
433        if self.top_fixed_area + self.scroll_area + self.bottom_fixed_area != total_height {
434            return row;
435        }
436
437        if row < self.top_fixed_area {
438            return row;
439        }
440
441        if row >= self.top_fixed_area + self.scroll_area {
442            return row;
443        }
444
445        if self.scroll_area == 0 {
446            return row;
447        }
448
449        let offset = row - self.top_fixed_area;
450        self.top_fixed_area + ((offset + self.start_address) % self.scroll_area)
451    }
452}
453
454#[derive(Debug)]
455struct RegisterWrite {
456    register: RegisterKind,
457    allowed_lengths: &'static [usize],
458}
459
460#[derive(Debug, Clone, Copy)]
461enum RegisterKind {
462    Madctl,
463    Colmod,
464    VerticalScrollDefinition,
465    VerticalScrollStart,
466    Brightness,
467    ControlDisplay,
468    InterfaceControl,
469    Raw(u8),
470}
471
472#[derive(Clone, Debug)]
473pub struct Framebuffer {
474    width: u16,
475    height: u16,
476    pixels: Vec<Color>,
477}
478
479impl Framebuffer {
480    pub fn new(width: u16, height: u16) -> Self {
481        Self {
482            width,
483            height,
484            pixels: vec![Color::BLACK; width as usize * height as usize],
485        }
486    }
487
488    pub fn width(&self) -> u16 {
489        self.width
490    }
491
492    pub fn height(&self) -> u16 {
493        self.height
494    }
495
496    pub fn pixels(&self) -> &[Color] {
497        &self.pixels
498    }
499
500    pub fn clear(&mut self, color: Color) {
501        self.pixels.fill(color);
502    }
503
504    pub fn copy_from(&mut self, other: &Self) {
505        self.pixels.clone_from_slice(&other.pixels);
506    }
507
508    pub fn get_pixel(&self, x: u16, y: u16) -> Option<Color> {
509        let index = self.index_of(x, y)?;
510        Some(self.pixels[index])
511    }
512
513    pub fn set_pixel(&mut self, x: u16, y: u16, color: Color) -> Result<()> {
514        let index = self.index_of(x, y).ok_or(LcdError::OutOfBounds)?;
515        self.pixels[index] = color;
516        Ok(())
517    }
518
519    pub fn fill_rect(&mut self, window: DrawWindow, color: Color) -> Result<()> {
520        for y in window.y..window.y + window.height {
521            for x in window.x..window.x + window.width {
522                self.set_pixel(x, y, color)?;
523            }
524        }
525        Ok(())
526    }
527
528    fn index_of(&self, x: u16, y: u16) -> Option<usize> {
529        if x >= self.width || y >= self.height {
530            return None;
531        }
532
533        Some(y as usize * self.width as usize + x as usize)
534    }
535}
536
537#[derive(Clone, Debug)]
538pub struct PinBank {
539    levels: [bool; 9],
540}
541
542impl Default for PinBank {
543    fn default() -> Self {
544        let mut levels = [false; 9];
545        levels[PinId::Cs.index()] = true;
546        levels[PinId::Rst.index()] = true;
547        levels[PinId::Wr.index()] = true;
548        levels[PinId::Rd.index()] = true;
549        levels[PinId::Bl.index()] = true;
550        Self { levels }
551    }
552}
553
554impl PinBank {
555    pub fn level(&self, pin: PinId) -> bool {
556        self.levels[pin.index()]
557    }
558
559    fn set(&mut self, pin: PinId, value: bool) {
560        self.levels[pin.index()] = value;
561    }
562}
563
564#[derive(Debug)]
565struct TimingEngine {
566    frame_interval: Duration,
567    bus_hz: u32,
568    last_visible_at: Instant,
569    pending_ready_at: Option<Instant>,
570}
571
572impl TimingEngine {
573    fn new(config: &LcdConfig) -> Self {
574        let frame_interval = config.frame_interval();
575        Self {
576            frame_interval,
577            bus_hz: config.bus_hz,
578            last_visible_at: Instant::now() - frame_interval,
579            pending_ready_at: None,
580        }
581    }
582
583    fn schedule_transfer(&mut self, bytes: usize, vsync: bool) -> Result<Instant> {
584        let now = Instant::now();
585
586        if let Some(ready_at) = self.pending_ready_at {
587            if ready_at > now {
588                return Err(LcdError::FrameRateExceeded);
589            }
590        }
591
592        let transfer_secs = (bytes as f64 * 8.0) / self.bus_hz as f64;
593        let bus_time = Duration::from_secs_f64(transfer_secs.max(0.0));
594        let earliest = if vsync {
595            self.last_visible_at + self.frame_interval
596        } else {
597            now
598        };
599        let ready_at = max_instant(now + bus_time, earliest);
600
601        self.pending_ready_at = Some(ready_at);
602        Ok(ready_at)
603    }
604
605    fn tick(&mut self) -> bool {
606        match self.pending_ready_at {
607            Some(ready_at) if Instant::now() >= ready_at => {
608                self.last_visible_at = ready_at;
609                self.pending_ready_at = None;
610                true
611            }
612            _ => false,
613        }
614    }
615
616    fn time_until_ready(&self) -> Option<Duration> {
617        self.pending_ready_at.map(|ready_at| ready_at.saturating_duration_since(Instant::now()))
618    }
619
620    fn clear_pending(&mut self) {
621        self.pending_ready_at = None;
622    }
623}
624
625#[derive(Debug)]
626enum PendingWrite {
627    None,
628    Column(AddressAccumulator),
629    Row(AddressAccumulator),
630    Register(RegisterWrite),
631    MemoryWrite(MemoryWriteProgress),
632}
633
634#[derive(Debug)]
635struct AddressAccumulator {
636    bytes: [u8; 4],
637    len: usize,
638}
639
640impl AddressAccumulator {
641    fn new() -> Self {
642        Self {
643            bytes: [0; 4],
644            len: 0,
645        }
646    }
647
648    fn push(&mut self, data: &[u8]) -> usize {
649        let available = 4 - self.len;
650        let take = available.min(data.len());
651        self.bytes[self.len..self.len + take].copy_from_slice(&data[..take]);
652        self.len += take;
653        take
654    }
655
656    fn complete(&self) -> bool {
657        self.len == 4
658    }
659
660    fn decode(&self) -> (u16, u16) {
661        let start = u16::from_be_bytes([self.bytes[0], self.bytes[1]]);
662        let end = u16::from_be_bytes([self.bytes[2], self.bytes[3]]);
663        (start, end)
664    }
665}
666
667#[derive(Debug)]
668struct MemoryWriteProgress {
669    window: DrawWindow,
670    next_pixel: usize,
671    partial_pixel: [u8; 3],
672    partial_len: usize,
673    transferred_bytes: usize,
674}
675
676impl MemoryWriteProgress {
677    fn new(window: DrawWindow) -> Self {
678        Self {
679            window,
680            next_pixel: 0,
681            partial_pixel: [0; 3],
682            partial_len: 0,
683            transferred_bytes: 0,
684        }
685    }
686
687    fn total_pixels(&self) -> usize {
688        self.window.area()
689    }
690
691    fn remaining_bytes(&self, bytes_per_pixel: usize) -> usize {
692        (self.total_pixels() - self.next_pixel) * bytes_per_pixel - self.partial_len
693    }
694
695    fn finished(&self) -> bool {
696        self.next_pixel == self.total_pixels() && self.partial_len == 0
697    }
698
699    fn current_coords(&self) -> (u16, u16) {
700        let dx = (self.next_pixel % self.window.width as usize) as u16;
701        let dy = (self.next_pixel / self.window.width as usize) as u16;
702        (self.window.x + dx, self.window.y + dy)
703    }
704}
705
706#[derive(Debug)]
707pub struct VirtualLcd {
708    config: LcdConfig,
709    state: LcdState,
710    controller: ControllerRuntime,
711    front_buffer: Framebuffer,
712    back_buffer: Framebuffer,
713    pins: PinBank,
714    timing: TimingEngine,
715    pending_write: PendingWrite,
716}
717
718impl VirtualLcd {
719    pub fn new(config: LcdConfig) -> Result<Self> {
720        config.validate()?;
721
722        let front_buffer = Framebuffer::new(config.width, config.height);
723        let back_buffer = Framebuffer::new(config.width, config.height);
724        let state = LcdState::new(&config);
725        let controller = ControllerRuntime::new(config.controller, &config);
726        let timing = TimingEngine::new(&config);
727
728        Ok(Self {
729            config,
730            state,
731            controller,
732            front_buffer,
733            back_buffer,
734            pins: PinBank::default(),
735            timing,
736            pending_write: PendingWrite::None,
737        })
738    }
739
740    pub fn config(&self) -> &LcdConfig {
741        &self.config
742    }
743
744    pub fn state(&self) -> &LcdState {
745        &self.state
746    }
747
748    pub fn pins(&self) -> &PinBank {
749        &self.pins
750    }
751
752    pub fn visible_frame(&self) -> &Framebuffer {
753        &self.front_buffer
754    }
755
756    pub fn working_frame(&self) -> &Framebuffer {
757        &self.back_buffer
758    }
759
760    pub fn controller_model(&self) -> ControllerModel {
761        self.config.controller
762    }
763
764    pub fn set_window(&mut self, x: u16, y: u16, width: u16, height: u16) -> Result<()> {
765        self.ensure_ready_for_graphics()?;
766        let window = DrawWindow::from_origin(x, y, width, height, &self.config)?;
767        self.state.set_column_range(window.x, window.x + window.width - 1);
768        self.state.set_row_range(window.y, window.y + window.height - 1);
769        Ok(())
770    }
771
772    pub fn set_address_window(&mut self, x0: u16, y0: u16, x1: u16, y1: u16) -> Result<()> {
773        self.ensure_ready_for_graphics()?;
774        let window = DrawWindow::from_inclusive(x0, y0, x1, y1, &self.config)?;
775        self.state.set_column_range(window.x, window.x + window.width - 1);
776        self.state.set_row_range(window.y, window.y + window.height - 1);
777        Ok(())
778    }
779
780    pub fn write_pixels(&mut self, pixels: &[Color]) -> Result<()> {
781        self.ensure_ready_for_graphics()?;
782        let expected = self.state.current_window.area();
783        if pixels.len() != expected {
784            return Err(LcdError::InvalidDataLength {
785                expected,
786                got: pixels.len(),
787            });
788        }
789
790        let window = self.state.current_window;
791        for (index, color) in pixels.iter().copied().enumerate() {
792            let dx = (index % window.width as usize) as u16;
793            let dy = (index / window.width as usize) as u16;
794            self.back_buffer
795                .set_pixel(window.x + dx, window.y + dy, color)?;
796        }
797
798        self.schedule_visible_update(expected * self.config.pixel_format.bytes_per_pixel())
799    }
800
801    pub fn tick(&mut self) -> bool {
802        if self.timing.tick() {
803            let _ = self.rebuild_visible_frame();
804            if self.config.buffering == BufferingMode::Single
805                && matches!(self.controller, ControllerRuntime::Generic)
806            {
807                self.back_buffer.copy_from(&self.front_buffer);
808            }
809            return true;
810        }
811
812        false
813    }
814
815    pub fn time_until_ready(&self) -> Option<Duration> {
816        self.timing.time_until_ready()
817    }
818
819    pub fn has_pending_frame(&self) -> bool {
820        self.timing.pending_ready_at.is_some()
821    }
822
823    fn hardware_reset(&mut self) {
824        self.front_buffer.clear(Color::BLACK);
825        self.back_buffer.clear(Color::BLACK);
826        self.state = LcdState::new(&self.config);
827        self.controller.reset(&self.config);
828        self.pending_write = PendingWrite::None;
829        self.timing.clear_pending();
830    }
831
832    fn ensure_ready_for_graphics(&self) -> Result<()> {
833        if !self.state.initialized {
834            return Err(LcdError::NotInitialized);
835        }
836
837        if self.state.sleeping {
838            return Err(LcdError::SleepMode);
839        }
840
841        if !self.state.display_on {
842            return Err(LcdError::DisplayOff);
843        }
844
845        Ok(())
846    }
847
848    fn ensure_memory_access(&self) -> Result<()> {
849        if !self.state.initialized {
850            return Err(LcdError::NotInitialized);
851        }
852
853        if self.state.sleeping {
854            return Err(LcdError::SleepMode);
855        }
856
857        Ok(())
858    }
859
860    fn validate_bus_access(&self) -> Result<()> {
861        if self.pins.level(PinId::Cs) {
862            return Err(LcdError::BusViolation("cannot access bus while CS is high"));
863        }
864
865        if !self.pins.level(PinId::Rst) {
866            return Err(LcdError::BusViolation("cannot access bus while reset is asserted"));
867        }
868
869        Ok(())
870    }
871
872    fn schedule_visible_update(&mut self, bytes: usize) -> Result<()> {
873        self.timing.schedule_transfer(bytes, self.config.vsync)?;
874        Ok(())
875    }
876
877    fn rebuild_visible_frame(&mut self) -> Result<()> {
878        match &self.controller {
879            ControllerRuntime::Generic => {
880                if self.state.display_on && !self.state.sleeping && self.state.backlight > 0 {
881                    self.front_buffer.copy_from(&self.back_buffer);
882                } else {
883                    self.front_buffer.clear(Color::BLACK);
884                }
885            }
886            ControllerRuntime::Ili9341(controller) => {
887                controller.apply_visible_transform(
888                    &self.back_buffer,
889                    &mut self.front_buffer,
890                    &self.state,
891                    &self.config,
892                )?;
893            }
894        }
895        Ok(())
896    }
897
898    fn process_address_data(&mut self, accumulator: &mut AddressAccumulator, data: &[u8], is_column: bool) -> Result<usize> {
899        let consumed = accumulator.push(data);
900        if accumulator.complete() {
901            let (start, end) = accumulator.decode();
902            let window = if is_column {
903                DrawWindow::from_inclusive(
904                    start,
905                    self.state.current_window.y,
906                    end,
907                    self.state.current_window.y + self.state.current_window.height - 1,
908                    &self.config,
909                )?
910            } else {
911                DrawWindow::from_inclusive(
912                    self.state.current_window.x,
913                    start,
914                    self.state.current_window.x + self.state.current_window.width - 1,
915                    end,
916                    &self.config,
917                )?
918            };
919
920            self.state.set_column_range(window.x, window.x + window.width - 1);
921            self.state.set_row_range(window.y, window.y + window.height - 1);
922        }
923
924        Ok(consumed)
925    }
926
927    fn process_memory_write(&mut self, progress: &mut MemoryWriteProgress, data: &[u8]) -> Result<usize> {
928        self.ensure_memory_access()?;
929
930        let bytes_per_pixel = self.controller.visible_bytes_per_pixel(self.config.pixel_format);
931        if data.len() > progress.remaining_bytes(bytes_per_pixel) {
932            return Err(LcdError::InvalidDataLength {
933                expected: progress.remaining_bytes(bytes_per_pixel),
934                got: data.len(),
935            });
936        }
937
938        for byte in data.iter().copied() {
939            progress.partial_pixel[progress.partial_len] = byte;
940            progress.partial_len += 1;
941            progress.transferred_bytes += 1;
942
943            if progress.partial_len == bytes_per_pixel {
944                let color = match &self.controller {
945                    ControllerRuntime::Generic => self
946                        .config
947                        .pixel_format
948                        .decode_color(&progress.partial_pixel[..bytes_per_pixel]),
949                    ControllerRuntime::Ili9341(controller) => {
950                        controller.decode_interface_color(&progress.partial_pixel[..bytes_per_pixel])
951                    }
952                };
953                let (x, y) = match &self.controller {
954                    ControllerRuntime::Generic => progress.current_coords(),
955                    ControllerRuntime::Ili9341(controller) => {
956                        controller.write_pixel_coords(progress.window, progress.next_pixel, &self.config)?
957                    }
958                };
959                self.back_buffer.set_pixel(x, y, color)?;
960                progress.partial_len = 0;
961                progress.next_pixel += 1;
962            }
963        }
964
965        Ok(data.len())
966    }
967
968    fn process_register_write(&mut self, write: RegisterWrite, data: &[u8]) -> Result<()> {
969        if !write.allowed_lengths.contains(&data.len()) {
970            return Err(LcdError::InvalidDataLength {
971                expected: *write.allowed_lengths.first().unwrap_or(&0),
972                got: data.len(),
973            });
974        }
975
976        let mut refresh_visible = false;
977        match (&mut self.controller, write.register) {
978            (ControllerRuntime::Generic, RegisterKind::Raw(_)) => {}
979            (ControllerRuntime::Ili9341(controller), RegisterKind::Madctl) => {
980                controller.madctl = data[0];
981                refresh_visible = true;
982            }
983            (ControllerRuntime::Ili9341(controller), RegisterKind::Colmod) => {
984                controller.colmod = data[0];
985            }
986            (ControllerRuntime::Ili9341(controller), RegisterKind::VerticalScrollDefinition) => {
987                controller.scroll.top_fixed_area = u16::from_be_bytes([data[0], data[1]]);
988                controller.scroll.scroll_area = u16::from_be_bytes([data[2], data[3]]);
989                controller.scroll.bottom_fixed_area = u16::from_be_bytes([data[4], data[5]]);
990                refresh_visible = true;
991            }
992            (ControllerRuntime::Ili9341(controller), RegisterKind::VerticalScrollStart) => {
993                controller.scroll.start_address =
994                    u16::from_be_bytes([data[0], data[1]]) % controller.scroll.scroll_area.max(1);
995                refresh_visible = true;
996            }
997            (ControllerRuntime::Ili9341(controller), RegisterKind::Brightness) => {
998                controller.brightness = data[0];
999                refresh_visible = true;
1000            }
1001            (ControllerRuntime::Ili9341(controller), RegisterKind::ControlDisplay) => {
1002                controller.control_display = data[0];
1003            }
1004            (ControllerRuntime::Ili9341(controller), RegisterKind::InterfaceControl) => {
1005                controller.interface_control.copy_from_slice(&data[..3]);
1006            }
1007            (ControllerRuntime::Ili9341(controller), RegisterKind::Raw(cmd)) => {
1008                controller.raw_registers.insert(cmd, data.to_vec());
1009            }
1010            (ControllerRuntime::Generic, _) => {}
1011        }
1012
1013        if refresh_visible {
1014            self.rebuild_visible_frame()?;
1015        }
1016
1017        Ok(())
1018    }
1019}
1020
1021impl Lcd for VirtualLcd {
1022    type Error = LcdError;
1023
1024    fn init(&mut self) -> Result<()> {
1025        self.hardware_reset();
1026        self.state.initialized = true;
1027        self.state.sleeping = false;
1028        self.state.display_on = true;
1029        self.state.backlight = if self.config.backlight { 100 } else { 0 };
1030        if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1031            controller.brightness = if self.config.backlight { 0xFF } else { 0x00 };
1032        }
1033        self.rebuild_visible_frame()?;
1034        Ok(())
1035    }
1036
1037    fn clear(&mut self, color: Color) -> Result<()> {
1038        self.ensure_ready_for_graphics()?;
1039        self.back_buffer.clear(color);
1040        Ok(())
1041    }
1042
1043    fn draw_pixel(&mut self, x: u16, y: u16, color: Color) -> Result<()> {
1044        self.ensure_ready_for_graphics()?;
1045        self.back_buffer.set_pixel(x, y, color)
1046    }
1047
1048    fn fill_rect(&mut self, x: u16, y: u16, width: u16, height: u16, color: Color) -> Result<()> {
1049        self.ensure_ready_for_graphics()?;
1050        let window = DrawWindow::from_origin(x, y, width, height, &self.config)?;
1051        self.back_buffer.fill_rect(window, color)
1052    }
1053
1054    fn present(&mut self) -> Result<()> {
1055        self.ensure_ready_for_graphics()?;
1056
1057        if !matches!(self.pending_write, PendingWrite::None) {
1058            return Err(LcdError::BusViolation("cannot present while a bus transaction is active"));
1059        }
1060
1061        self.schedule_visible_update(self.config.full_frame_bytes())
1062    }
1063}
1064
1065impl LcdBus for VirtualLcd {
1066    type Error = LcdError;
1067
1068    fn set_pin(&mut self, pin: PinId, value: bool) -> Result<()> {
1069        self.pins.set(pin, value);
1070
1071        match pin {
1072            PinId::Rst if !value => self.hardware_reset(),
1073            PinId::Bl => {
1074                self.state.backlight = if value { 100 } else { 0 };
1075                self.rebuild_visible_frame()?;
1076            }
1077            _ => {}
1078        }
1079
1080        Ok(())
1081    }
1082
1083    fn write_command(&mut self, cmd: u8) -> Result<()> {
1084        self.validate_bus_access()?;
1085
1086        if !matches!(self.pending_write, PendingWrite::None) {
1087            return Err(LcdError::BusViolation("cannot start a new command before finishing data phase"));
1088        }
1089
1090        self.state.current_command = Some(cmd);
1091
1092        match self.config.controller {
1093            ControllerModel::GenericMipiDcs => match cmd {
1094                0x01 => {
1095                    self.hardware_reset();
1096                    self.state.current_command = Some(cmd);
1097                }
1098                0x11 => {
1099                    self.state.initialized = true;
1100                    self.state.sleeping = false;
1101                }
1102                0x28 => {
1103                    self.ensure_initialized_only()?;
1104                    self.state.display_on = false;
1105                }
1106                0x29 => {
1107                    self.ensure_initialized_only()?;
1108                    self.state.display_on = true;
1109                }
1110                0x2A => {
1111                    self.ensure_initialized_only()?;
1112                    self.pending_write = PendingWrite::Column(AddressAccumulator::new());
1113                }
1114                0x2B => {
1115                    self.ensure_initialized_only()?;
1116                    self.pending_write = PendingWrite::Row(AddressAccumulator::new());
1117                }
1118                0x2C => {
1119                    self.ensure_memory_access()?;
1120                    self.pending_write =
1121                        PendingWrite::MemoryWrite(MemoryWriteProgress::new(self.state.current_window));
1122                }
1123                _ => return Err(LcdError::InvalidCommand(cmd)),
1124            },
1125            ControllerModel::Ili9341 => match cmd {
1126                0x01 => {
1127                    self.hardware_reset();
1128                    self.state.current_command = Some(cmd);
1129                }
1130                0x04 | 0x09 | 0x0A | 0x0B | 0x0C | 0x0F | 0x2E | 0x45 | 0x52 | 0x54 | 0xDA
1131                | 0xDB | 0xDC => {}
1132                0x10 => {
1133                    self.ensure_initialized_only()?;
1134                    self.state.sleeping = true;
1135                    self.rebuild_visible_frame()?;
1136                }
1137                0x11 => {
1138                    self.state.initialized = true;
1139                    self.state.sleeping = false;
1140                    self.rebuild_visible_frame()?;
1141                }
1142                0x13 => {
1143                    self.state.initialized = true;
1144                }
1145                0x20 => {
1146                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1147                        controller.inversion_on = false;
1148                    }
1149                }
1150                0x21 => {
1151                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1152                        controller.inversion_on = true;
1153                    }
1154                }
1155                0x28 => {
1156                    self.ensure_initialized_only()?;
1157                    self.state.display_on = false;
1158                    self.rebuild_visible_frame()?;
1159                }
1160                0x29 => {
1161                    self.ensure_initialized_only()?;
1162                    self.state.display_on = true;
1163                    self.rebuild_visible_frame()?;
1164                }
1165                0x2A => {
1166                    self.ensure_initialized_only()?;
1167                    self.pending_write = PendingWrite::Column(AddressAccumulator::new());
1168                }
1169                0x2B => {
1170                    self.ensure_initialized_only()?;
1171                    self.pending_write = PendingWrite::Row(AddressAccumulator::new());
1172                }
1173                0x2C => {
1174                    self.ensure_memory_access()?;
1175                    self.pending_write =
1176                        PendingWrite::MemoryWrite(MemoryWriteProgress::new(self.state.current_window));
1177                }
1178                0x34 => {
1179                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1180                        controller.tearing_enabled = false;
1181                    }
1182                }
1183                0x35 => {
1184                    if let ControllerRuntime::Ili9341(controller) = &mut self.controller {
1185                        controller.tearing_enabled = true;
1186                        controller.tearing_mode = 0x00;
1187                    }
1188                }
1189                other => {
1190                    if let Some(write) = self.ili9341_register_write_for_command(other) {
1191                        self.pending_write = PendingWrite::Register(write);
1192                    } else {
1193                        return Err(LcdError::InvalidCommand(other));
1194                    }
1195                }
1196            },
1197        }
1198
1199        Ok(())
1200    }
1201
1202    fn write_data(&mut self, data: &[u8]) -> Result<()> {
1203        self.validate_bus_access()?;
1204
1205        let pending = std::mem::replace(&mut self.pending_write, PendingWrite::None);
1206        match pending {
1207            PendingWrite::None => Err(LcdError::BusViolation("data write without an active command")),
1208            PendingWrite::Column(mut accumulator) => {
1209                let consumed = self.process_address_data(&mut accumulator, data, true)?;
1210                if consumed != data.len() {
1211                    return Err(LcdError::InvalidDataLength {
1212                        expected: 4 - accumulator.len,
1213                        got: data.len() - consumed,
1214                    });
1215                }
1216
1217                if !accumulator.complete() {
1218                    self.pending_write = PendingWrite::Column(accumulator);
1219                }
1220
1221                Ok(())
1222            }
1223            PendingWrite::Row(mut accumulator) => {
1224                let consumed = self.process_address_data(&mut accumulator, data, false)?;
1225                if consumed != data.len() {
1226                    return Err(LcdError::InvalidDataLength {
1227                        expected: 4 - accumulator.len,
1228                        got: data.len() - consumed,
1229                    });
1230                }
1231
1232                if !accumulator.complete() {
1233                    self.pending_write = PendingWrite::Row(accumulator);
1234                }
1235
1236                Ok(())
1237            }
1238            PendingWrite::Register(write) => self.process_register_write(write, data),
1239            PendingWrite::MemoryWrite(mut progress) => {
1240                self.process_memory_write(&mut progress, data)?;
1241                if progress.finished() {
1242                    self.schedule_visible_update(progress.transferred_bytes)?;
1243                } else {
1244                    self.pending_write = PendingWrite::MemoryWrite(progress);
1245                }
1246                Ok(())
1247            }
1248        }
1249    }
1250
1251    fn read_data(&mut self, len: usize) -> Result<Vec<u8>> {
1252        self.validate_bus_access()?;
1253        self.build_read_response(len)
1254    }
1255}
1256
1257impl VirtualLcd {
1258    fn ili9341_register_write_for_command(&self, cmd: u8) -> Option<RegisterWrite> {
1259        let allowed_lengths: &'static [usize] = match cmd {
1260            0x26 | 0x36 | 0x3A | 0x51 | 0x53 | 0x55 | 0x56 | 0xB0 | 0xB7 | 0xC0 | 0xC1
1261            | 0xC7 | 0xF2 | 0xF7 => &[1],
1262            0x37 | 0x44 | 0xB1 | 0xC5 | 0xEA => &[2],
1263            0xE8 | 0xF6 => &[3],
1264            0xB5 | 0xED => &[4],
1265            0xCB => &[5],
1266            0x33 => &[6],
1267            0xCF => &[3],
1268            0xB6 => &[3, 4],
1269            0xE0 | 0xE1 => &[15],
1270            _ => return None,
1271        };
1272
1273        let register = match cmd {
1274            0x36 => RegisterKind::Madctl,
1275            0x3A => RegisterKind::Colmod,
1276            0x33 => RegisterKind::VerticalScrollDefinition,
1277            0x37 => RegisterKind::VerticalScrollStart,
1278            0x51 => RegisterKind::Brightness,
1279            0x53 => RegisterKind::ControlDisplay,
1280            0xF6 => RegisterKind::InterfaceControl,
1281            other => RegisterKind::Raw(other),
1282        };
1283
1284        Some(RegisterWrite {
1285            register,
1286            allowed_lengths,
1287        })
1288    }
1289
1290    fn build_read_response(&self, len: usize) -> Result<Vec<u8>> {
1291        let mut response = match (&self.controller, self.state.current_command) {
1292            (_, Some(0x04)) => vec![0x00, 0x00, 0x93, 0x41],
1293            (ControllerRuntime::Ili9341(controller), Some(0x09)) => {
1294                vec![0x00, 0x00, controller.power_mode(&self.state), controller.madctl, controller.colmod]
1295            }
1296            (ControllerRuntime::Ili9341(controller), Some(0x0A)) => {
1297                vec![0x00, controller.power_mode(&self.state)]
1298            }
1299            (ControllerRuntime::Ili9341(controller), Some(0x0B)) => vec![0x00, controller.madctl],
1300            (ControllerRuntime::Ili9341(controller), Some(0x0C)) => vec![0x00, controller.colmod],
1301            (ControllerRuntime::Ili9341(_), Some(0x0F)) => vec![0x00, 0xC0],
1302            (ControllerRuntime::Ili9341(_), Some(0x45)) => vec![0x00, 0x00, 0x00],
1303            (ControllerRuntime::Ili9341(controller), Some(0x52)) => vec![0x00, controller.brightness],
1304            (ControllerRuntime::Ili9341(controller), Some(0x54)) => {
1305                vec![0x00, controller.control_display]
1306            }
1307            (_, Some(0xDA)) => vec![0x00],
1308            (_, Some(0xDB)) => vec![0x93],
1309            (_, Some(0xDC)) => vec![0x41],
1310            (ControllerRuntime::Ili9341(controller), Some(0x2E)) => {
1311                self.build_ili9341_memory_read(controller, len)
1312            }
1313            _ => vec![0x00; len],
1314        };
1315
1316        response.resize(len, 0x00);
1317        Ok(response)
1318    }
1319
1320    fn build_ili9341_memory_read(&self, controller: &Ili9341State, len: usize) -> Vec<u8> {
1321        let window = self.state.current_window;
1322        let bytes_per_pixel = controller.interface_pixel_format().bytes_per_pixel();
1323        let mut out = Vec::with_capacity(len.max(1));
1324        out.push(0x00);
1325
1326        for index in 0..window.area() {
1327            if out.len() >= len {
1328                break;
1329            }
1330
1331            if let Ok((x, y)) = controller.write_pixel_coords(window, index, &self.config) {
1332                let color = self.back_buffer.get_pixel(x, y).unwrap_or(Color::BLACK);
1333                match controller.interface_pixel_format() {
1334                    PixelFormat::Rgb565 => {
1335                        let bytes = color.to_rgb565().to_be_bytes();
1336                        out.extend_from_slice(&bytes);
1337                    }
1338                    PixelFormat::Rgb888 => {
1339                        out.push(color.r & 0xFC);
1340                        out.push(color.g & 0xFC);
1341                        out.push(color.b & 0xFC);
1342                    }
1343                    format => {
1344                        let mut raw = [0u8; 3];
1345                        raw[..format.bytes_per_pixel()]
1346                            .copy_from_slice(&[color.r, color.g, color.b][..format.bytes_per_pixel()]);
1347                        out.extend_from_slice(&raw[..bytes_per_pixel]);
1348                    }
1349                }
1350            }
1351        }
1352
1353        out.truncate(len);
1354        out
1355    }
1356
1357    fn ensure_initialized_only(&self) -> Result<()> {
1358        if !self.state.initialized {
1359            return Err(LcdError::NotInitialized);
1360        }
1361
1362        Ok(())
1363    }
1364}
1365
1366#[derive(Debug, Clone, PartialEq, Eq)]
1367pub enum LcdError {
1368    InvalidConfig(&'static str),
1369    NotInitialized,
1370    DisplayOff,
1371    SleepMode,
1372    InvalidWindow,
1373    OutOfBounds,
1374    InvalidCommand(u8),
1375    InvalidDataLength { expected: usize, got: usize },
1376    BusViolation(&'static str),
1377    FrameRateExceeded,
1378}
1379
1380impl Display for LcdError {
1381    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1382        match self {
1383            Self::InvalidConfig(message) => write!(f, "invalid config: {message}"),
1384            Self::NotInitialized => f.write_str("display is not initialized"),
1385            Self::DisplayOff => f.write_str("display is off"),
1386            Self::SleepMode => f.write_str("display is in sleep mode"),
1387            Self::InvalidWindow => f.write_str("invalid address window"),
1388            Self::OutOfBounds => f.write_str("coordinates are out of bounds"),
1389            Self::InvalidCommand(cmd) => write!(f, "invalid command 0x{cmd:02X}"),
1390            Self::InvalidDataLength { expected, got } => {
1391                write!(f, "invalid data length: expected {expected} bytes, got {got}")
1392            }
1393            Self::BusViolation(message) => write!(f, "bus violation: {message}"),
1394            Self::FrameRateExceeded => f.write_str("frame submitted before the previous transfer completed"),
1395        }
1396    }
1397}
1398
1399impl Error for LcdError {}
1400
1401fn max_instant(left: Instant, right: Instant) -> Instant {
1402    if left >= right { left } else { right }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408    use std::thread;
1409
1410    fn fast_config() -> LcdConfig {
1411        LcdConfig {
1412            width: 4,
1413            height: 4,
1414            pixel_format: PixelFormat::Rgb565,
1415            fps: 1_000,
1416            interface: InterfaceType::Spi4Wire,
1417            orientation: 0,
1418            vsync: false,
1419            buffering: BufferingMode::Double,
1420            backlight: true,
1421            tearing_effect: false,
1422            bus_hz: 32_000_000,
1423            controller: ControllerModel::Ili9341,
1424        }
1425    }
1426
1427    fn wait_until_visible(lcd: &mut VirtualLcd) {
1428        for _ in 0..16 {
1429            if lcd.tick() {
1430                return;
1431            }
1432            thread::sleep(Duration::from_millis(1));
1433        }
1434    }
1435
1436    fn bus_ready_ili9341() -> VirtualLcd {
1437        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
1438        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
1439        lcd.write_command(0x11).expect("sleep out should succeed");
1440        lcd.write_command(0x29).expect("display on should succeed");
1441        lcd
1442    }
1443
1444    fn write_command_with_data(lcd: &mut VirtualLcd, cmd: u8, data: &[u8]) {
1445        lcd.write_command(cmd).expect("command should succeed");
1446        lcd.write_data(data)
1447            .unwrap_or_else(|error| panic!("data for command 0x{cmd:02X} should succeed: {error:?}"));
1448    }
1449
1450    #[test]
1451    fn high_level_draw_requires_present() {
1452        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
1453        lcd.init().expect("init should succeed");
1454        lcd.draw_pixel(1, 2, Color::WHITE)
1455            .expect("pixel draw should succeed");
1456
1457        assert_eq!(lcd.visible_frame().get_pixel(1, 2), Some(Color::BLACK));
1458
1459        lcd.present().expect("present should schedule a frame");
1460        wait_until_visible(&mut lcd);
1461
1462        assert_eq!(lcd.visible_frame().get_pixel(1, 2), Some(Color::WHITE));
1463    }
1464
1465    #[test]
1466    fn low_level_memory_write_updates_window() {
1467        let mut lcd = bus_ready_ili9341();
1468        write_command_with_data(&mut lcd, 0x3A, &[0x55]);
1469
1470        lcd.write_command(0x2A).expect("column command should succeed");
1471        lcd.write_data(&[0x00, 0x00, 0x00, 0x01])
1472            .expect("column data should succeed");
1473        lcd.write_command(0x2B).expect("row command should succeed");
1474        lcd.write_data(&[0x00, 0x00, 0x00, 0x00])
1475            .expect("row data should succeed");
1476        lcd.write_command(0x2C).expect("memory write should start");
1477
1478        let red = Color::RED.to_rgb565().to_be_bytes();
1479        let green = Color::GREEN.to_rgb565().to_be_bytes();
1480        let mut pixels = Vec::new();
1481        pixels.extend_from_slice(&red);
1482        pixels.extend_from_slice(&green);
1483        lcd.write_data(&pixels).expect("pixel payload should succeed");
1484
1485        wait_until_visible(&mut lcd);
1486
1487        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::RED));
1488        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::GREEN));
1489    }
1490
1491    #[test]
1492    fn ili9341_common_init_sequence_is_accepted() {
1493        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
1494        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
1495
1496        write_command_with_data(&mut lcd, 0xCB, &[0x39, 0x2C, 0x00, 0x34, 0x02]);
1497        write_command_with_data(&mut lcd, 0xCF, &[0x00, 0xC1, 0x30]);
1498        write_command_with_data(&mut lcd, 0xE8, &[0x85, 0x00, 0x78]);
1499        write_command_with_data(&mut lcd, 0xEA, &[0x00, 0x00]);
1500        write_command_with_data(&mut lcd, 0xED, &[0x64, 0x03, 0x12, 0x81]);
1501        write_command_with_data(&mut lcd, 0xF7, &[0x20]);
1502        write_command_with_data(&mut lcd, 0xC0, &[0x23]);
1503        write_command_with_data(&mut lcd, 0xC1, &[0x10]);
1504        write_command_with_data(&mut lcd, 0xC5, &[0x3E, 0x28]);
1505        write_command_with_data(&mut lcd, 0xC7, &[0x86]);
1506        write_command_with_data(&mut lcd, 0xB1, &[0x00, 0x18]);
1507        write_command_with_data(&mut lcd, 0xB6, &[0x08, 0x82, 0x27]);
1508        write_command_with_data(&mut lcd, 0xF2, &[0x00]);
1509        write_command_with_data(&mut lcd, 0x26, &[0x01]);
1510        write_command_with_data(
1511            &mut lcd,
1512            0xE0,
1513            &[0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00],
1514        );
1515        write_command_with_data(
1516            &mut lcd,
1517            0xE1,
1518            &[0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F],
1519        );
1520        lcd.write_command(0x11).expect("sleep out should succeed");
1521        write_command_with_data(&mut lcd, 0x3A, &[0x55]);
1522        write_command_with_data(&mut lcd, 0x36, &[0x48]);
1523        lcd.write_command(0x29).expect("display on should succeed");
1524    }
1525
1526    #[test]
1527    fn ili9341_read_commands_expose_id_and_pixel_format() {
1528        let mut lcd = bus_ready_ili9341();
1529        write_command_with_data(&mut lcd, 0x3A, &[0x55]);
1530
1531        lcd.write_command(0x04).expect("read id command should succeed");
1532        assert_eq!(lcd.read_data(4).expect("id read should succeed"), vec![0x00, 0x00, 0x93, 0x41]);
1533
1534        lcd.write_command(0x0C).expect("read colmod should succeed");
1535        assert_eq!(lcd.read_data(2).expect("colmod read should succeed"), vec![0x00, 0x55]);
1536    }
1537
1538    #[test]
1539    fn ili9341_madctl_rotation_changes_visible_mapping() {
1540        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
1541        lcd.init().expect("init should succeed");
1542        lcd.draw_pixel(1, 0, Color::RED).expect("pixel draw should succeed");
1543        lcd.present().expect("present should succeed");
1544        wait_until_visible(&mut lcd);
1545
1546        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::RED));
1547
1548        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
1549        write_command_with_data(&mut lcd, 0x36, &[0x20]);
1550
1551        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::BLACK));
1552        assert_eq!(lcd.visible_frame().get_pixel(0, 1), Some(Color::RED));
1553    }
1554
1555    #[test]
1556    fn ili9341_vertical_scroll_repositions_visible_rows() {
1557        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
1558        lcd.init().expect("init should succeed");
1559
1560        lcd.fill_rect(0, 0, 4, 1, Color::RED).expect("row 0");
1561        lcd.fill_rect(0, 1, 4, 1, Color::GREEN).expect("row 1");
1562        lcd.fill_rect(0, 2, 4, 1, Color::BLUE).expect("row 2");
1563        lcd.fill_rect(0, 3, 4, 1, Color::WHITE).expect("row 3");
1564        lcd.present().expect("present should succeed");
1565        wait_until_visible(&mut lcd);
1566
1567        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
1568        write_command_with_data(&mut lcd, 0x33, &[0x00, 0x00, 0x00, 0x04, 0x00, 0x00]);
1569        write_command_with_data(&mut lcd, 0x37, &[0x00, 0x01]);
1570
1571        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::GREEN));
1572        assert_eq!(lcd.visible_frame().get_pixel(0, 1), Some(Color::BLUE));
1573        assert_eq!(lcd.visible_frame().get_pixel(0, 2), Some(Color::WHITE));
1574        assert_eq!(lcd.visible_frame().get_pixel(0, 3), Some(Color::RED));
1575    }
1576
1577    #[test]
1578    fn invalid_config_rejects_zero_dimensions() {
1579        let mut config = fast_config();
1580        config.width = 0;
1581
1582        assert!(matches!(
1583            VirtualLcd::new(config),
1584            Err(LcdError::InvalidConfig("display dimensions must be non-zero"))
1585        ));
1586    }
1587
1588    #[test]
1589    fn present_rejects_new_frame_while_previous_one_is_pending() {
1590        let mut config = fast_config();
1591        config.bus_hz = 1;
1592
1593        let mut lcd = VirtualLcd::new(config).expect("config should be valid");
1594        lcd.init().expect("init should succeed");
1595        lcd.present().expect("first frame should be scheduled");
1596
1597        assert!(lcd.has_pending_frame());
1598        assert_eq!(lcd.present(), Err(LcdError::FrameRateExceeded));
1599    }
1600
1601    #[test]
1602    fn write_data_without_command_reports_bus_violation() {
1603        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
1604        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
1605
1606        assert_eq!(
1607            lcd.write_data(&[0x12]),
1608            Err(LcdError::BusViolation("data write without an active command"))
1609        );
1610    }
1611
1612    #[test]
1613    fn write_pixels_requires_window_sized_payload() {
1614        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
1615        lcd.init().expect("init should succeed");
1616        lcd.set_window(0, 0, 2, 2).expect("window should be valid");
1617
1618        assert_eq!(
1619            lcd.write_pixels(&[Color::WHITE; 3]),
1620            Err(LcdError::InvalidDataLength {
1621                expected: 4,
1622                got: 3,
1623            })
1624        );
1625    }
1626}