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