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};
6
7pub use virtual_lcd_sdk::{Color, Lcd, LcdBus, PinId};
8
9pub type Result<T> = std::result::Result<T, LcdError>;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct LcdConfig {
13    pub width: u16,
14    pub height: u16,
15    pub pixel_format: PixelFormat,
16    pub fps: u16,
17    pub interface: InterfaceType,
18    pub orientation: u16,
19    pub vsync: bool,
20    pub buffering: BufferingMode,
21    pub backlight: bool,
22    pub tearing_effect: bool,
23    pub bus_hz: u32,
24}
25
26impl Default for LcdConfig {
27    fn default() -> Self {
28        Self {
29            width: 320,
30            height: 240,
31            pixel_format: PixelFormat::Rgb565,
32            fps: 30,
33            interface: InterfaceType::Spi4Wire,
34            orientation: 0,
35            vsync: true,
36            buffering: BufferingMode::Double,
37            backlight: true,
38            tearing_effect: false,
39            bus_hz: 8_000_000,
40        }
41    }
42}
43
44impl LcdConfig {
45    fn validate(&self) -> Result<()> {
46        if self.width == 0 || self.height == 0 {
47            return Err(LcdError::InvalidConfig("display dimensions must be non-zero"));
48        }
49
50        if self.fps == 0 {
51            return Err(LcdError::InvalidConfig("fps must be non-zero"));
52        }
53
54        if self.bus_hz == 0 {
55            return Err(LcdError::InvalidConfig("bus_hz must be non-zero"));
56        }
57
58        Ok(())
59    }
60
61    pub fn frame_interval(&self) -> Duration {
62        Duration::from_secs_f64(1.0 / self.fps as f64)
63    }
64
65    pub fn full_frame_bytes(&self) -> usize {
66        self.width as usize * self.height as usize * self.pixel_format.bytes_per_pixel()
67    }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum PixelFormat {
72    Mono1,
73    Gray8,
74    Rgb565,
75    Rgb888,
76}
77
78impl PixelFormat {
79    pub fn bytes_per_pixel(self) -> usize {
80        match self {
81            Self::Mono1 | Self::Gray8 => 1,
82            Self::Rgb565 => 2,
83            Self::Rgb888 => 3,
84        }
85    }
86
87    fn decode_color(self, bytes: &[u8]) -> Color {
88        match self {
89            Self::Mono1 => {
90                if bytes[0] == 0 {
91                    Color::BLACK
92                } else {
93                    Color::WHITE
94                }
95            }
96            Self::Gray8 => Color::rgb(bytes[0], bytes[0], bytes[0]),
97            Self::Rgb565 => {
98                let value = u16::from_be_bytes([bytes[0], bytes[1]]);
99                Color::from_rgb565(value)
100            }
101            Self::Rgb888 => Color::rgb(bytes[0], bytes[1], bytes[2]),
102        }
103    }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum InterfaceType {
108    Spi4Wire,
109    Spi3Wire,
110    Parallel8080,
111    MemoryMapped,
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum BufferingMode {
116    Single,
117    Double,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub struct DrawWindow {
122    pub x: u16,
123    pub y: u16,
124    pub width: u16,
125    pub height: u16,
126}
127
128impl DrawWindow {
129    pub fn full(config: &LcdConfig) -> Self {
130        Self {
131            x: 0,
132            y: 0,
133            width: config.width,
134            height: config.height,
135        }
136    }
137
138    pub fn from_origin(x: u16, y: u16, width: u16, height: u16, config: &LcdConfig) -> Result<Self> {
139        if width == 0 || height == 0 {
140            return Err(LcdError::InvalidWindow);
141        }
142
143        let x_end = x
144            .checked_add(width - 1)
145            .ok_or(LcdError::OutOfBounds)?;
146        let y_end = y
147            .checked_add(height - 1)
148            .ok_or(LcdError::OutOfBounds)?;
149
150        if x_end >= config.width || y_end >= config.height {
151            return Err(LcdError::OutOfBounds);
152        }
153
154        Ok(Self {
155            x,
156            y,
157            width,
158            height,
159        })
160    }
161
162    pub fn from_inclusive(x0: u16, y0: u16, x1: u16, y1: u16, config: &LcdConfig) -> Result<Self> {
163        if x1 < x0 || y1 < y0 {
164            return Err(LcdError::InvalidWindow);
165        }
166
167        Self::from_origin(x0, y0, x1 - x0 + 1, y1 - y0 + 1, config)
168    }
169
170    pub fn area(self) -> usize {
171        self.width as usize * self.height as usize
172    }
173}
174
175#[derive(Clone, Debug)]
176pub struct LcdState {
177    pub initialized: bool,
178    pub sleeping: bool,
179    pub display_on: bool,
180    pub backlight: u8,
181    pub current_window: DrawWindow,
182    pub current_command: Option<u8>,
183    column_range: (u16, u16),
184    row_range: (u16, u16),
185}
186
187impl LcdState {
188    fn new(config: &LcdConfig) -> Self {
189        let full = DrawWindow::full(config);
190        Self {
191            initialized: false,
192            sleeping: true,
193            display_on: false,
194            backlight: if config.backlight { 100 } else { 0 },
195            current_window: full,
196            current_command: None,
197            column_range: (0, config.width - 1),
198            row_range: (0, config.height - 1),
199        }
200    }
201
202    fn set_column_range(&mut self, start: u16, end: u16) {
203        self.column_range = (start, end);
204        self.sync_window();
205    }
206
207    fn set_row_range(&mut self, start: u16, end: u16) {
208        self.row_range = (start, end);
209        self.sync_window();
210    }
211
212    fn sync_window(&mut self) {
213        self.current_window = DrawWindow {
214            x: self.column_range.0,
215            y: self.row_range.0,
216            width: self.column_range.1 - self.column_range.0 + 1,
217            height: self.row_range.1 - self.row_range.0 + 1,
218        };
219    }
220}
221
222#[derive(Clone, Debug)]
223pub struct Framebuffer {
224    width: u16,
225    height: u16,
226    pixels: Vec<Color>,
227}
228
229impl Framebuffer {
230    pub fn new(width: u16, height: u16) -> Self {
231        Self {
232            width,
233            height,
234            pixels: vec![Color::BLACK; width as usize * height as usize],
235        }
236    }
237
238    pub fn width(&self) -> u16 {
239        self.width
240    }
241
242    pub fn height(&self) -> u16 {
243        self.height
244    }
245
246    pub fn pixels(&self) -> &[Color] {
247        &self.pixels
248    }
249
250    pub fn clear(&mut self, color: Color) {
251        self.pixels.fill(color);
252    }
253
254    pub fn copy_from(&mut self, other: &Self) {
255        self.pixels.clone_from_slice(&other.pixels);
256    }
257
258    pub fn get_pixel(&self, x: u16, y: u16) -> Option<Color> {
259        let index = self.index_of(x, y)?;
260        Some(self.pixels[index])
261    }
262
263    pub fn set_pixel(&mut self, x: u16, y: u16, color: Color) -> Result<()> {
264        let index = self.index_of(x, y).ok_or(LcdError::OutOfBounds)?;
265        self.pixels[index] = color;
266        Ok(())
267    }
268
269    pub fn fill_rect(&mut self, window: DrawWindow, color: Color) -> Result<()> {
270        for y in window.y..window.y + window.height {
271            for x in window.x..window.x + window.width {
272                self.set_pixel(x, y, color)?;
273            }
274        }
275        Ok(())
276    }
277
278    fn index_of(&self, x: u16, y: u16) -> Option<usize> {
279        if x >= self.width || y >= self.height {
280            return None;
281        }
282
283        Some(y as usize * self.width as usize + x as usize)
284    }
285}
286
287#[derive(Clone, Debug)]
288pub struct PinBank {
289    levels: [bool; 9],
290}
291
292impl Default for PinBank {
293    fn default() -> Self {
294        let mut levels = [false; 9];
295        levels[PinId::Cs.index()] = true;
296        levels[PinId::Rst.index()] = true;
297        levels[PinId::Wr.index()] = true;
298        levels[PinId::Rd.index()] = true;
299        levels[PinId::Bl.index()] = true;
300        Self { levels }
301    }
302}
303
304impl PinBank {
305    pub fn level(&self, pin: PinId) -> bool {
306        self.levels[pin.index()]
307    }
308
309    fn set(&mut self, pin: PinId, value: bool) {
310        self.levels[pin.index()] = value;
311    }
312}
313
314#[derive(Debug)]
315struct TimingEngine {
316    frame_interval: Duration,
317    bus_hz: u32,
318    last_visible_at: Instant,
319    pending_ready_at: Option<Instant>,
320}
321
322impl TimingEngine {
323    fn new(config: &LcdConfig) -> Self {
324        let frame_interval = config.frame_interval();
325        Self {
326            frame_interval,
327            bus_hz: config.bus_hz,
328            last_visible_at: Instant::now() - frame_interval,
329            pending_ready_at: None,
330        }
331    }
332
333    fn schedule_transfer(&mut self, bytes: usize, vsync: bool) -> Result<Instant> {
334        let now = Instant::now();
335
336        if let Some(ready_at) = self.pending_ready_at {
337            if ready_at > now {
338                return Err(LcdError::FrameRateExceeded);
339            }
340        }
341
342        let transfer_secs = (bytes as f64 * 8.0) / self.bus_hz as f64;
343        let bus_time = Duration::from_secs_f64(transfer_secs.max(0.0));
344        let earliest = if vsync {
345            self.last_visible_at + self.frame_interval
346        } else {
347            now
348        };
349        let ready_at = max_instant(now + bus_time, earliest);
350
351        self.pending_ready_at = Some(ready_at);
352        Ok(ready_at)
353    }
354
355    fn tick(&mut self) -> bool {
356        match self.pending_ready_at {
357            Some(ready_at) if Instant::now() >= ready_at => {
358                self.last_visible_at = ready_at;
359                self.pending_ready_at = None;
360                true
361            }
362            _ => false,
363        }
364    }
365
366    fn time_until_ready(&self) -> Option<Duration> {
367        self.pending_ready_at.map(|ready_at| ready_at.saturating_duration_since(Instant::now()))
368    }
369
370    fn clear_pending(&mut self) {
371        self.pending_ready_at = None;
372    }
373}
374
375#[derive(Debug)]
376enum PendingWrite {
377    None,
378    Column(AddressAccumulator),
379    Row(AddressAccumulator),
380    MemoryWrite(MemoryWriteProgress),
381}
382
383#[derive(Debug)]
384struct AddressAccumulator {
385    bytes: [u8; 4],
386    len: usize,
387}
388
389impl AddressAccumulator {
390    fn new() -> Self {
391        Self {
392            bytes: [0; 4],
393            len: 0,
394        }
395    }
396
397    fn push(&mut self, data: &[u8]) -> usize {
398        let available = 4 - self.len;
399        let take = available.min(data.len());
400        self.bytes[self.len..self.len + take].copy_from_slice(&data[..take]);
401        self.len += take;
402        take
403    }
404
405    fn complete(&self) -> bool {
406        self.len == 4
407    }
408
409    fn decode(&self) -> (u16, u16) {
410        let start = u16::from_be_bytes([self.bytes[0], self.bytes[1]]);
411        let end = u16::from_be_bytes([self.bytes[2], self.bytes[3]]);
412        (start, end)
413    }
414}
415
416#[derive(Debug)]
417struct MemoryWriteProgress {
418    window: DrawWindow,
419    next_pixel: usize,
420    partial_pixel: [u8; 3],
421    partial_len: usize,
422    transferred_bytes: usize,
423}
424
425impl MemoryWriteProgress {
426    fn new(window: DrawWindow) -> Self {
427        Self {
428            window,
429            next_pixel: 0,
430            partial_pixel: [0; 3],
431            partial_len: 0,
432            transferred_bytes: 0,
433        }
434    }
435
436    fn total_pixels(&self) -> usize {
437        self.window.area()
438    }
439
440    fn remaining_bytes(&self, bytes_per_pixel: usize) -> usize {
441        (self.total_pixels() - self.next_pixel) * bytes_per_pixel - self.partial_len
442    }
443
444    fn finished(&self) -> bool {
445        self.next_pixel == self.total_pixels() && self.partial_len == 0
446    }
447
448    fn current_coords(&self) -> (u16, u16) {
449        let dx = (self.next_pixel % self.window.width as usize) as u16;
450        let dy = (self.next_pixel / self.window.width as usize) as u16;
451        (self.window.x + dx, self.window.y + dy)
452    }
453}
454
455#[derive(Debug)]
456pub struct VirtualLcd {
457    config: LcdConfig,
458    state: LcdState,
459    front_buffer: Framebuffer,
460    back_buffer: Framebuffer,
461    pins: PinBank,
462    timing: TimingEngine,
463    pending_write: PendingWrite,
464}
465
466impl VirtualLcd {
467    pub fn new(config: LcdConfig) -> Result<Self> {
468        config.validate()?;
469
470        let front_buffer = Framebuffer::new(config.width, config.height);
471        let back_buffer = Framebuffer::new(config.width, config.height);
472        let state = LcdState::new(&config);
473        let timing = TimingEngine::new(&config);
474
475        Ok(Self {
476            config,
477            state,
478            front_buffer,
479            back_buffer,
480            pins: PinBank::default(),
481            timing,
482            pending_write: PendingWrite::None,
483        })
484    }
485
486    pub fn config(&self) -> &LcdConfig {
487        &self.config
488    }
489
490    pub fn state(&self) -> &LcdState {
491        &self.state
492    }
493
494    pub fn pins(&self) -> &PinBank {
495        &self.pins
496    }
497
498    pub fn visible_frame(&self) -> &Framebuffer {
499        &self.front_buffer
500    }
501
502    pub fn working_frame(&self) -> &Framebuffer {
503        &self.back_buffer
504    }
505
506    pub fn set_window(&mut self, x: u16, y: u16, width: u16, height: u16) -> Result<()> {
507        self.ensure_ready_for_graphics()?;
508        let window = DrawWindow::from_origin(x, y, width, height, &self.config)?;
509        self.state.set_column_range(window.x, window.x + window.width - 1);
510        self.state.set_row_range(window.y, window.y + window.height - 1);
511        Ok(())
512    }
513
514    pub fn set_address_window(&mut self, x0: u16, y0: u16, x1: u16, y1: u16) -> Result<()> {
515        self.ensure_ready_for_graphics()?;
516        let window = DrawWindow::from_inclusive(x0, y0, x1, y1, &self.config)?;
517        self.state.set_column_range(window.x, window.x + window.width - 1);
518        self.state.set_row_range(window.y, window.y + window.height - 1);
519        Ok(())
520    }
521
522    pub fn write_pixels(&mut self, pixels: &[Color]) -> Result<()> {
523        self.ensure_ready_for_graphics()?;
524        let expected = self.state.current_window.area();
525        if pixels.len() != expected {
526            return Err(LcdError::InvalidDataLength {
527                expected,
528                got: pixels.len(),
529            });
530        }
531
532        let window = self.state.current_window;
533        for (index, color) in pixels.iter().copied().enumerate() {
534            let dx = (index % window.width as usize) as u16;
535            let dy = (index / window.width as usize) as u16;
536            self.back_buffer
537                .set_pixel(window.x + dx, window.y + dy, color)?;
538        }
539
540        self.schedule_visible_update(expected * self.config.pixel_format.bytes_per_pixel())
541    }
542
543    pub fn tick(&mut self) -> bool {
544        if self.timing.tick() {
545            self.front_buffer.copy_from(&self.back_buffer);
546            if self.config.buffering == BufferingMode::Single {
547                self.back_buffer.copy_from(&self.front_buffer);
548            }
549            return true;
550        }
551
552        false
553    }
554
555    pub fn time_until_ready(&self) -> Option<Duration> {
556        self.timing.time_until_ready()
557    }
558
559    pub fn has_pending_frame(&self) -> bool {
560        self.timing.pending_ready_at.is_some()
561    }
562
563    fn hardware_reset(&mut self) {
564        self.front_buffer.clear(Color::BLACK);
565        self.back_buffer.clear(Color::BLACK);
566        self.state = LcdState::new(&self.config);
567        self.pending_write = PendingWrite::None;
568        self.timing.clear_pending();
569    }
570
571    fn ensure_ready_for_graphics(&self) -> Result<()> {
572        if !self.state.initialized {
573            return Err(LcdError::NotInitialized);
574        }
575
576        if self.state.sleeping {
577            return Err(LcdError::SleepMode);
578        }
579
580        if !self.state.display_on {
581            return Err(LcdError::DisplayOff);
582        }
583
584        Ok(())
585    }
586
587    fn validate_bus_access(&self) -> Result<()> {
588        if self.pins.level(PinId::Cs) {
589            return Err(LcdError::BusViolation("cannot access bus while CS is high"));
590        }
591
592        if !self.pins.level(PinId::Rst) {
593            return Err(LcdError::BusViolation("cannot access bus while reset is asserted"));
594        }
595
596        Ok(())
597    }
598
599    fn schedule_visible_update(&mut self, bytes: usize) -> Result<()> {
600        self.timing.schedule_transfer(bytes, self.config.vsync)?;
601        Ok(())
602    }
603
604    fn process_address_data(&mut self, accumulator: &mut AddressAccumulator, data: &[u8], is_column: bool) -> Result<usize> {
605        let consumed = accumulator.push(data);
606        if accumulator.complete() {
607            let (start, end) = accumulator.decode();
608            let window = if is_column {
609                DrawWindow::from_inclusive(
610                    start,
611                    self.state.current_window.y,
612                    end,
613                    self.state.current_window.y + self.state.current_window.height - 1,
614                    &self.config,
615                )?
616            } else {
617                DrawWindow::from_inclusive(
618                    self.state.current_window.x,
619                    start,
620                    self.state.current_window.x + self.state.current_window.width - 1,
621                    end,
622                    &self.config,
623                )?
624            };
625
626            self.state.set_column_range(window.x, window.x + window.width - 1);
627            self.state.set_row_range(window.y, window.y + window.height - 1);
628        }
629
630        Ok(consumed)
631    }
632
633    fn process_memory_write(&mut self, progress: &mut MemoryWriteProgress, data: &[u8]) -> Result<usize> {
634        self.ensure_ready_for_graphics()?;
635
636        let bytes_per_pixel = self.config.pixel_format.bytes_per_pixel();
637        if data.len() > progress.remaining_bytes(bytes_per_pixel) {
638            return Err(LcdError::InvalidDataLength {
639                expected: progress.remaining_bytes(bytes_per_pixel),
640                got: data.len(),
641            });
642        }
643
644        for byte in data.iter().copied() {
645            progress.partial_pixel[progress.partial_len] = byte;
646            progress.partial_len += 1;
647            progress.transferred_bytes += 1;
648
649            if progress.partial_len == bytes_per_pixel {
650                let color = self
651                    .config
652                    .pixel_format
653                    .decode_color(&progress.partial_pixel[..bytes_per_pixel]);
654                let (x, y) = progress.current_coords();
655                self.back_buffer.set_pixel(x, y, color)?;
656                progress.partial_len = 0;
657                progress.next_pixel += 1;
658            }
659        }
660
661        Ok(data.len())
662    }
663}
664
665impl Lcd for VirtualLcd {
666    type Error = LcdError;
667
668    fn init(&mut self) -> Result<()> {
669        self.hardware_reset();
670        self.state.initialized = true;
671        self.state.sleeping = false;
672        self.state.display_on = true;
673        self.state.backlight = if self.config.backlight { 100 } else { 0 };
674        Ok(())
675    }
676
677    fn clear(&mut self, color: Color) -> Result<()> {
678        self.ensure_ready_for_graphics()?;
679        self.back_buffer.clear(color);
680        Ok(())
681    }
682
683    fn draw_pixel(&mut self, x: u16, y: u16, color: Color) -> Result<()> {
684        self.ensure_ready_for_graphics()?;
685        self.back_buffer.set_pixel(x, y, color)
686    }
687
688    fn fill_rect(&mut self, x: u16, y: u16, width: u16, height: u16, color: Color) -> Result<()> {
689        self.ensure_ready_for_graphics()?;
690        let window = DrawWindow::from_origin(x, y, width, height, &self.config)?;
691        self.back_buffer.fill_rect(window, color)
692    }
693
694    fn present(&mut self) -> Result<()> {
695        self.ensure_ready_for_graphics()?;
696
697        if !matches!(self.pending_write, PendingWrite::None) {
698            return Err(LcdError::BusViolation("cannot present while a bus transaction is active"));
699        }
700
701        self.schedule_visible_update(self.config.full_frame_bytes())
702    }
703}
704
705impl LcdBus for VirtualLcd {
706    type Error = LcdError;
707
708    fn set_pin(&mut self, pin: PinId, value: bool) -> Result<()> {
709        self.pins.set(pin, value);
710
711        match pin {
712            PinId::Rst if !value => self.hardware_reset(),
713            PinId::Bl => {
714                self.state.backlight = if value { 100 } else { 0 };
715            }
716            _ => {}
717        }
718
719        Ok(())
720    }
721
722    fn write_command(&mut self, cmd: u8) -> Result<()> {
723        self.validate_bus_access()?;
724
725        if !matches!(self.pending_write, PendingWrite::None) {
726            return Err(LcdError::BusViolation("cannot start a new command before finishing data phase"));
727        }
728
729        self.state.current_command = Some(cmd);
730
731        match cmd {
732            0x01 => {
733                self.hardware_reset();
734                self.state.current_command = Some(cmd);
735            }
736            0x11 => {
737                self.state.initialized = true;
738                self.state.sleeping = false;
739            }
740            0x28 => {
741                self.ensure_initialized_only()?;
742                self.state.display_on = false;
743            }
744            0x29 => {
745                self.ensure_initialized_only()?;
746                self.state.display_on = true;
747            }
748            0x2A => {
749                self.ensure_initialized_only()?;
750                self.pending_write = PendingWrite::Column(AddressAccumulator::new());
751            }
752            0x2B => {
753                self.ensure_initialized_only()?;
754                self.pending_write = PendingWrite::Row(AddressAccumulator::new());
755            }
756            0x2C => {
757                self.ensure_ready_for_graphics()?;
758                self.pending_write = PendingWrite::MemoryWrite(MemoryWriteProgress::new(self.state.current_window));
759            }
760            _ => return Err(LcdError::InvalidCommand(cmd)),
761        }
762
763        Ok(())
764    }
765
766    fn write_data(&mut self, data: &[u8]) -> Result<()> {
767        self.validate_bus_access()?;
768
769        let pending = std::mem::replace(&mut self.pending_write, PendingWrite::None);
770        match pending {
771            PendingWrite::None => Err(LcdError::BusViolation("data write without an active command")),
772            PendingWrite::Column(mut accumulator) => {
773                let consumed = self.process_address_data(&mut accumulator, data, true)?;
774                if consumed != data.len() {
775                    return Err(LcdError::InvalidDataLength {
776                        expected: 4 - accumulator.len,
777                        got: data.len() - consumed,
778                    });
779                }
780
781                if !accumulator.complete() {
782                    self.pending_write = PendingWrite::Column(accumulator);
783                }
784
785                Ok(())
786            }
787            PendingWrite::Row(mut accumulator) => {
788                let consumed = self.process_address_data(&mut accumulator, data, false)?;
789                if consumed != data.len() {
790                    return Err(LcdError::InvalidDataLength {
791                        expected: 4 - accumulator.len,
792                        got: data.len() - consumed,
793                    });
794                }
795
796                if !accumulator.complete() {
797                    self.pending_write = PendingWrite::Row(accumulator);
798                }
799
800                Ok(())
801            }
802            PendingWrite::MemoryWrite(mut progress) => {
803                self.process_memory_write(&mut progress, data)?;
804                if progress.finished() {
805                    self.schedule_visible_update(progress.transferred_bytes)?;
806                } else {
807                    self.pending_write = PendingWrite::MemoryWrite(progress);
808                }
809                Ok(())
810            }
811        }
812    }
813
814    fn read_data(&mut self, len: usize) -> Result<Vec<u8>> {
815        self.validate_bus_access()?;
816
817        let response = match self.state.current_command {
818            Some(0x04) => {
819                let mut id = vec![0x00, 0x93, 0x41];
820                id.resize(len, 0x00);
821                id
822            }
823            _ => vec![0x00; len],
824        };
825
826        Ok(response)
827    }
828}
829
830impl VirtualLcd {
831    fn ensure_initialized_only(&self) -> Result<()> {
832        if !self.state.initialized {
833            return Err(LcdError::NotInitialized);
834        }
835
836        Ok(())
837    }
838}
839
840#[derive(Debug, Clone, PartialEq, Eq)]
841pub enum LcdError {
842    InvalidConfig(&'static str),
843    NotInitialized,
844    DisplayOff,
845    SleepMode,
846    InvalidWindow,
847    OutOfBounds,
848    InvalidCommand(u8),
849    InvalidDataLength { expected: usize, got: usize },
850    BusViolation(&'static str),
851    FrameRateExceeded,
852}
853
854impl Display for LcdError {
855    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
856        match self {
857            Self::InvalidConfig(message) => write!(f, "invalid config: {message}"),
858            Self::NotInitialized => f.write_str("display is not initialized"),
859            Self::DisplayOff => f.write_str("display is off"),
860            Self::SleepMode => f.write_str("display is in sleep mode"),
861            Self::InvalidWindow => f.write_str("invalid address window"),
862            Self::OutOfBounds => f.write_str("coordinates are out of bounds"),
863            Self::InvalidCommand(cmd) => write!(f, "invalid command 0x{cmd:02X}"),
864            Self::InvalidDataLength { expected, got } => {
865                write!(f, "invalid data length: expected {expected} bytes, got {got}")
866            }
867            Self::BusViolation(message) => write!(f, "bus violation: {message}"),
868            Self::FrameRateExceeded => f.write_str("frame submitted before the previous transfer completed"),
869        }
870    }
871}
872
873impl Error for LcdError {}
874
875fn max_instant(left: Instant, right: Instant) -> Instant {
876    if left >= right { left } else { right }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882    use std::thread;
883
884    fn fast_config() -> LcdConfig {
885        LcdConfig {
886            width: 4,
887            height: 4,
888            pixel_format: PixelFormat::Rgb565,
889            fps: 1_000,
890            interface: InterfaceType::Spi4Wire,
891            orientation: 0,
892            vsync: false,
893            buffering: BufferingMode::Double,
894            backlight: true,
895            tearing_effect: false,
896            bus_hz: 32_000_000,
897        }
898    }
899
900    fn wait_until_visible(lcd: &mut VirtualLcd) {
901        for _ in 0..16 {
902            if lcd.tick() {
903                return;
904            }
905            thread::sleep(Duration::from_millis(1));
906        }
907    }
908
909    #[test]
910    fn high_level_draw_requires_present() {
911        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
912        lcd.init().expect("init should succeed");
913        lcd.draw_pixel(1, 2, Color::WHITE)
914            .expect("pixel draw should succeed");
915
916        assert_eq!(lcd.visible_frame().get_pixel(1, 2), Some(Color::BLACK));
917
918        lcd.present().expect("present should schedule a frame");
919        wait_until_visible(&mut lcd);
920
921        assert_eq!(lcd.visible_frame().get_pixel(1, 2), Some(Color::WHITE));
922    }
923
924    #[test]
925    fn low_level_memory_write_updates_window() {
926        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
927        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
928        lcd.write_command(0x11).expect("sleep out should succeed");
929        lcd.write_command(0x29).expect("display on should succeed");
930
931        lcd.write_command(0x2A).expect("column command should succeed");
932        lcd.write_data(&[0x00, 0x00, 0x00, 0x01])
933            .expect("column data should succeed");
934        lcd.write_command(0x2B).expect("row command should succeed");
935        lcd.write_data(&[0x00, 0x00, 0x00, 0x00])
936            .expect("row data should succeed");
937        lcd.write_command(0x2C).expect("memory write should start");
938
939        let red = Color::RED.to_rgb565().to_be_bytes();
940        let green = Color::GREEN.to_rgb565().to_be_bytes();
941        let mut pixels = Vec::new();
942        pixels.extend_from_slice(&red);
943        pixels.extend_from_slice(&green);
944        lcd.write_data(&pixels).expect("pixel payload should succeed");
945
946        wait_until_visible(&mut lcd);
947
948        assert_eq!(lcd.visible_frame().get_pixel(0, 0), Some(Color::RED));
949        assert_eq!(lcd.visible_frame().get_pixel(1, 0), Some(Color::GREEN));
950    }
951
952    #[test]
953    fn invalid_config_rejects_zero_dimensions() {
954        let mut config = fast_config();
955        config.width = 0;
956
957        assert!(matches!(
958            VirtualLcd::new(config),
959            Err(LcdError::InvalidConfig("display dimensions must be non-zero"))
960        ));
961    }
962
963    #[test]
964    fn present_rejects_new_frame_while_previous_one_is_pending() {
965        let mut config = fast_config();
966        config.bus_hz = 1;
967
968        let mut lcd = VirtualLcd::new(config).expect("config should be valid");
969        lcd.init().expect("init should succeed");
970        lcd.present().expect("first frame should be scheduled");
971
972        assert!(lcd.has_pending_frame());
973        assert_eq!(lcd.present(), Err(LcdError::FrameRateExceeded));
974    }
975
976    #[test]
977    fn write_data_without_command_reports_bus_violation() {
978        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
979        lcd.set_pin(PinId::Cs, false).expect("CS should be writable");
980
981        assert_eq!(
982            lcd.write_data(&[0x12]),
983            Err(LcdError::BusViolation("data write without an active command"))
984        );
985    }
986
987    #[test]
988    fn write_pixels_requires_window_sized_payload() {
989        let mut lcd = VirtualLcd::new(fast_config()).expect("config should be valid");
990        lcd.init().expect("init should succeed");
991        lcd.set_window(0, 0, 2, 2).expect("window should be valid");
992
993        assert_eq!(
994            lcd.write_pixels(&[Color::WHITE; 3]),
995            Err(LcdError::InvalidDataLength {
996                expected: 4,
997                got: 3,
998            })
999        );
1000    }
1001}