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