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