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}