extern crate alloc;
use embedded_graphics::pixelcolor::{Gray8, Rgb565, Rgb888};
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{PrimitiveStyle, Rectangle};
use st77916::commands;
use st77916::{
ColorMode, ControllerInterface, DisplaySize, Framebuffer, ResetInterface, St77916,
St77916Color, framebuffer_size,
};
use std::cell::RefCell;
#[derive(Debug, Clone, PartialEq)]
enum Call {
Command(u8),
CommandWithData(u8, Vec<u8>),
Pixels(Vec<u8>),
}
type CallLog = std::rc::Rc<RefCell<Vec<Call>>>;
struct MockInterface {
calls: CallLog,
}
impl MockInterface {
fn new() -> Self {
Self {
calls: std::rc::Rc::new(RefCell::new(Vec::new())),
}
}
fn with_log(log: &CallLog) -> Self {
Self {
calls: std::rc::Rc::clone(log),
}
}
}
impl ControllerInterface for MockInterface {
type Error = ();
fn send_command(&mut self, cmd: u8) -> Result<(), ()> {
self.calls.borrow_mut().push(Call::Command(cmd));
Ok(())
}
fn send_command_with_data(&mut self, cmd: u8, data: &[u8]) -> Result<(), ()> {
self.calls
.borrow_mut()
.push(Call::CommandWithData(cmd, data.to_vec()));
Ok(())
}
fn send_pixels(&mut self, pixels: &[u8]) -> Result<(), ()> {
self.calls.borrow_mut().push(Call::Pixels(pixels.to_vec()));
Ok(())
}
}
struct MockReset;
impl ResetInterface for MockReset {
type Error = ();
fn reset(&mut self) -> Result<(), ()> {
Ok(())
}
}
struct NoDelay;
impl embedded_hal::delay::DelayNs for NoDelay {
fn delay_ns(&mut self, _ns: u32) {}
}
#[test]
fn framebuffer_size_rgb565() {
let size = DisplaySize::new(360, 360);
assert_eq!(framebuffer_size(size, ColorMode::Rgb565), 360 * 360 * 2);
}
#[test]
fn framebuffer_size_rgb666() {
let size = DisplaySize::new(360, 390);
assert_eq!(framebuffer_size(size, ColorMode::Rgb666), 360 * 390 * 3);
}
#[test]
fn colmod_params() {
assert_eq!(ColorMode::Rgb565.colmod_param(), 0x55);
assert_eq!(ColorMode::Rgb666.colmod_param(), 0x66);
assert_eq!(ColorMode::Rgb565.bytes_per_pixel(), 2);
assert_eq!(ColorMode::Rgb666.bytes_per_pixel(), 3);
}
#[test]
fn rgb565_encode() {
let color = Rgb565::new(0x1F, 0x3F, 0x1F); let mut buf = [0u8; 2];
color.encode(&mut buf);
assert_eq!(buf, [0xFF, 0xFF]);
}
#[test]
fn rgb565_encode_red() {
let color = Rgb565::new(0x1F, 0, 0);
let mut buf = [0u8; 2];
color.encode(&mut buf);
assert_eq!(buf, [0xF8, 0x00]);
}
#[test]
fn rgb565_encode_black() {
let color = Rgb565::new(0, 0, 0);
let mut buf = [0u8; 2];
color.encode(&mut buf);
assert_eq!(buf, [0x00, 0x00]);
}
#[test]
fn rgb888_encode() {
let color = Rgb888::new(0xAA, 0xBB, 0xCC);
let mut buf = [0u8; 3];
color.encode(&mut buf);
assert_eq!(buf, [0xAA, 0xBB, 0xCC]);
}
#[test]
fn gray8_encode() {
let color = Gray8::new(0x42);
let mut buf = [0u8; 1];
color.encode(&mut buf);
assert_eq!(buf, [0x42]);
}
#[test]
fn rgb565_fill_buf_uniform() {
let mut buf = [0xFFu8; 10];
Rgb565::fill_buf(Rgb565::new(0, 0, 0), &mut buf);
assert!(buf.iter().all(|&b| b == 0x00));
}
#[test]
fn rgb565_fill_buf_non_uniform() {
let color = Rgb565::new(0x1F, 0, 0); let mut buf = [0u8; 6];
Rgb565::fill_buf(color, &mut buf);
assert_eq!(buf, [0xF8, 0x00, 0xF8, 0x00, 0xF8, 0x00]);
}
#[test]
fn rgb888_fill_buf_uniform_gray() {
let mut buf = [0u8; 9];
Rgb888::fill_buf(Rgb888::new(0x55, 0x55, 0x55), &mut buf);
assert!(buf.iter().all(|&b| b == 0x55));
}
#[test]
fn gray8_fill_buf() {
let mut buf = [0u8; 4];
Gray8::fill_buf(Gray8::new(0xAB), &mut buf);
assert!(buf.iter().all(|&b| b == 0xAB));
}
#[test]
fn unbuffered_default_init() {
let size = DisplaySize::new(4, 4);
let display = St77916::builder(MockInterface::new(), MockReset, size)
.build(ColorMode::Rgb565, &mut NoDelay);
assert!(display.is_ok());
}
#[test]
fn unbuffered_custom_init() {
let size = DisplaySize::new(4, 4);
static INIT: &[(u8, &[u8], u16)] = &[
(0xF0, &[0x28], 0),
(0x11, &[], 120),
(0x29, &[], 0),
(0x3A, &[0x55], 5),
];
let display = St77916::builder(MockInterface::new(), MockReset, size)
.with_init_commands(INIT)
.build(ColorMode::Rgb565, &mut NoDelay);
assert!(display.is_ok());
}
#[test]
fn unbuffered_send_pixels() {
let size = DisplaySize::new(2, 2);
let mut display = St77916::builder(MockInterface::new(), MockReset, size)
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
let pixels = [0xF8, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xF8, 0x00];
display.set_window(0, 0, 1, 1).unwrap();
display.send_pixels(&pixels).unwrap();
}
#[test]
fn unbuffered_flush_pixels() {
let size = DisplaySize::new(2, 2);
let mut display = St77916::builder(MockInterface::new(), MockReset, size)
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
let pixels = [0u8; 2 * 2 * 2];
display.flush_pixels(&pixels).unwrap();
}
#[test]
fn unbuffered_size() {
let size = DisplaySize::new(360, 360);
let display = St77916::builder(MockInterface::new(), MockReset, size)
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
let s = display.size();
assert_eq!(s.width, 360);
assert_eq!(s.height, 360);
}
#[test]
fn buffered_build() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let display = St77916::builder(MockInterface::new(), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay);
assert!(display.is_ok());
}
#[test]
fn buffered_custom_init() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
static INIT: &[(u8, &[u8], u16)] = &[
(0xF0, &[0x01], 0),
(0x11, &[], 10),
(0x29, &[], 0),
(0x3A, &[0x55], 0),
];
let display = St77916::builder(MockInterface::new(), MockReset, size)
.with_init_commands(INIT)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay);
assert!(display.is_ok());
}
#[test]
fn buffered_flush() {
let size = DisplaySize::new(2, 2);
const FB: usize = 2 * 2 * 2;
let mut display = St77916::builder(MockInterface::new(), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display.flush().unwrap();
}
#[test]
fn buffered_hard_reset() {
let size = DisplaySize::new(2, 2);
const FB: usize = 2 * 2 * 2;
let mut display = St77916::builder(MockInterface::new(), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display.hard_reset().unwrap();
}
#[test]
fn buffered_set_window() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let mut display = St77916::builder(MockInterface::new(), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display.set_window(0, 0, 3, 3).unwrap();
}
#[test]
fn standard_command_values() {
assert_eq!(commands::SLPIN, 0x10);
assert_eq!(commands::SLPOUT, 0x11);
assert_eq!(commands::INVON, 0x21);
assert_eq!(commands::DISPOFF, 0x28);
assert_eq!(commands::DISPON, 0x29);
assert_eq!(commands::CASET, 0x2A);
assert_eq!(commands::RASET, 0x2B);
assert_eq!(commands::RAMWR, 0x2C);
assert_eq!(commands::RAMWRC, 0x3C);
assert_eq!(commands::MADCTL, 0x36);
assert_eq!(commands::COLMOD, 0x3A);
}
#[test]
fn page_select_values() {
assert_eq!(commands::CSC1, 0xF0);
assert_eq!(commands::CSC2, 0xF1);
assert_eq!(commands::CSC3, 0xF2);
assert_eq!(commands::CSC4, 0xF3);
}
#[test]
fn mcs_command_values() {
assert_eq!(commands::VRHPS, 0xB0);
assert_eq!(commands::VRHNS, 0xB1);
assert_eq!(commands::VCOMS, 0xB2);
assert_eq!(commands::GAMCTRP1, 0xE0);
assert_eq!(commands::GAMCTRN1, 0xE1);
assert_eq!(commands::RESSET1, 0xD0);
}
#[test]
fn qspi_opcode_values() {
assert_eq!(commands::QSPI_CMD_WRITE, 0x02);
assert_eq!(commands::QSPI_CMD_WRITE_4O, 0x32);
assert_eq!(commands::QSPI_CMD_READ, 0x0B);
}
#[test]
fn unbuffered_draw_target_build() {
let size = DisplaySize::new(4, 4);
let display = St77916::builder(MockInterface::new(), MockReset, size)
.unbuffered::<Rgb565>()
.build(ColorMode::Rgb565, &mut NoDelay);
assert!(display.is_ok());
}
#[test]
fn unbuffered_draw_target_fill_solid() {
let size = DisplaySize::new(4, 4);
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.unbuffered::<Rgb565>()
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
let area = Rectangle::new(Point::new(1, 1), Size::new(2, 2));
display.fill_solid(&area, Rgb565::RED).unwrap();
let calls = log.borrow();
assert!(
calls.len() >= 3,
"expected at least 3 calls, got {}",
calls.len()
);
assert!(matches!(&calls[0], Call::CommandWithData(0x2A, _)));
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, _)));
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 4));
assert!(matches!(&calls[3], Call::Pixels(p) if p.len() == 4));
if let Call::Pixels(ref data) = calls[2] {
assert_eq!(data, &[0xF8, 0x00, 0xF8, 0x00]);
}
}
#[test]
fn unbuffered_draw_target_clear() {
let size = DisplaySize::new(2, 2);
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.unbuffered::<Rgb565>()
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
display.clear(Rgb565::BLACK).unwrap();
let calls = log.borrow();
assert_eq!(calls.len(), 4);
if let Call::Pixels(ref data) = calls[2] {
assert_eq!(data, &[0x00, 0x00, 0x00, 0x00]);
}
}
#[test]
fn unbuffered_draw_target_draw_iter_single_pixel() {
let size = DisplaySize::new(4, 4);
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.unbuffered::<Rgb565>()
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
let pixel = Pixel(Point::new(2, 3), Rgb565::BLUE);
display.draw_iter([pixel]).unwrap();
let calls = log.borrow();
assert_eq!(calls.len(), 3);
if let Call::CommandWithData(0x2A, ref data) = calls[0] {
assert_eq!(data, &[0x00, 0x02, 0x00, 0x02]);
} else {
panic!("expected CASET");
}
if let Call::CommandWithData(0x2B, ref data) = calls[1] {
assert_eq!(data, &[0x00, 0x03, 0x00, 0x03]);
} else {
panic!("expected RASET");
}
if let Call::Pixels(ref data) = calls[2] {
assert_eq!(data, &[0x00, 0x1F]);
}
}
#[test]
fn unbuffered_draw_target_skips_out_of_bounds() {
let size = DisplaySize::new(4, 4);
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.unbuffered::<Rgb565>()
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
let pixel = Pixel(Point::new(10, 10), Rgb565::RED);
display.draw_iter([pixel]).unwrap();
assert!(
log.borrow().is_empty(),
"out-of-bounds pixel should not send anything"
);
}
#[test]
fn unbuffered_draw_target_fill_contiguous() {
let size = DisplaySize::new(4, 4);
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.unbuffered::<Rgb565>()
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
let area = Rectangle::new(Point::new(0, 0), Size::new(2, 2));
let colors = [Rgb565::RED, Rgb565::GREEN, Rgb565::BLUE, Rgb565::WHITE];
display.fill_contiguous(&area, colors).unwrap();
let calls = log.borrow();
assert_eq!(calls.len(), 4);
if let Call::Pixels(ref data) = calls[2] {
assert_eq!(data.len(), 4); assert_eq!(data[0..2], [0xF8, 0x00]);
assert_eq!(data[2..4], [0x07, 0xE0]);
}
if let Call::Pixels(ref data) = calls[3] {
assert_eq!(data[0..2], [0x00, 0x1F]); assert_eq!(data[2..4], [0xFF, 0xFF]); }
}
#[test]
fn unbuffered_draw_target_embedded_graphics_rect() {
let size = DisplaySize::new(8, 8);
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.unbuffered::<Rgb565>()
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
Rectangle::new(Point::new(2, 2), Size::new(3, 3))
.into_styled(PrimitiveStyle::with_fill(Rgb565::RED))
.draw(&mut display)
.unwrap();
let calls = log.borrow();
assert!(calls.len() >= 3);
assert!(matches!(&calls[0], Call::CommandWithData(0x2A, _)));
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, _)));
}
#[test]
fn flush_skips_when_clean() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
display.flush().unwrap();
assert!(
log.borrow().is_empty(),
"flush on clean display should be a no-op"
);
}
#[test]
fn flush_sends_only_dirty_band() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
let area = Rectangle::new(Point::new(1, 1), Size::new(2, 2));
display.fill_solid(&area, Rgb565::RED).unwrap();
log.borrow_mut().clear();
display.flush().unwrap();
let calls = log.borrow();
assert!(matches!(&calls[0], Call::CommandWithData(0x2A, d) if d == &[0, 0, 0, 3]));
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, d) if d == &[0, 1, 0, 2]));
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 16));
}
#[test]
fn flush_resets_dirty_after_flush() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display
.fill_solid(
&Rectangle::new(Point::new(0, 0), Size::new(2, 2)),
Rgb565::RED,
)
.unwrap();
display.flush().unwrap();
log.borrow_mut().clear();
display.flush().unwrap();
assert!(
log.borrow().is_empty(),
"second flush should be a no-op after reset"
);
}
#[test]
fn clear_marks_entire_display_dirty() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display.clear(Rgb565::BLACK).unwrap();
log.borrow_mut().clear();
display.flush().unwrap();
let calls = log.borrow();
assert!(matches!(&calls[0], Call::CommandWithData(0x2A, d) if d == &[0, 0, 0, 3]));
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, d) if d == &[0, 0, 0, 3]));
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 32));
}
#[test]
fn draw_iter_tracks_dirty_rows() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display
.draw_iter([
Pixel(Point::new(0, 1), Rgb565::RED),
Pixel(Point::new(0, 3), Rgb565::BLUE),
])
.unwrap();
log.borrow_mut().clear();
display.flush().unwrap();
let calls = log.borrow();
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, d) if d == &[0, 1, 0, 3]));
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 24));
}
#[test]
fn multiple_draws_expand_dirty_band() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display
.fill_solid(
&Rectangle::new(Point::new(0, 0), Size::new(1, 1)),
Rgb565::RED,
)
.unwrap();
display
.fill_solid(
&Rectangle::new(Point::new(0, 3), Size::new(1, 1)),
Rgb565::BLUE,
)
.unwrap();
log.borrow_mut().clear();
display.flush().unwrap();
let calls = log.borrow();
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, d) if d == &[0, 0, 0, 3]));
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 32));
}
#[test]
fn full_flush_sends_everything() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
display
.fill_solid(
&Rectangle::new(Point::new(0, 2), Size::new(1, 1)),
Rgb565::RED,
)
.unwrap();
log.borrow_mut().clear();
display.full_flush().unwrap();
let calls = log.borrow();
assert!(matches!(&calls[0], Call::CommandWithData(0x2A, d) if d == &[0, 0, 0, 3]));
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, d) if d == &[0, 0, 0, 3]));
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 32));
}
#[test]
fn partial_flush_sends_rows_directly() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
log.borrow_mut().clear();
display.partial_flush(1, 2, 1, 2).unwrap();
let calls = log.borrow();
assert_eq!(calls.len(), 4);
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 4));
assert!(matches!(&calls[3], Call::Pixels(p) if p.len() == 4));
}
#[test]
fn mark_dirty_for_direct_framebuffer_writes() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let log: CallLog = std::rc::Rc::new(RefCell::new(Vec::new()));
let mut display = St77916::builder(MockInterface::with_log(&log), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
let fb = display.framebuffer_mut();
fb[0] = 0xFF;
display.mark_dirty(2, 3);
log.borrow_mut().clear();
display.flush().unwrap();
let calls = log.borrow();
assert!(matches!(&calls[1], Call::CommandWithData(0x2B, d) if d == &[0, 2, 0, 3]));
assert!(matches!(&calls[2], Call::Pixels(p) if p.len() == 16));
}
#[test]
fn is_clean_reflects_state() {
let size = DisplaySize::new(4, 4);
const FB: usize = 4 * 4 * 2;
let mut display = St77916::builder(MockInterface::new(), MockReset, size)
.buffered::<Rgb565>(Framebuffer::heap::<FB>())
.build(ColorMode::Rgb565, &mut NoDelay)
.unwrap();
assert!(display.is_clean());
display
.fill_solid(
&Rectangle::new(Point::new(0, 0), Size::new(1, 1)),
Rgb565::RED,
)
.unwrap();
assert!(!display.is_clean());
display.flush().unwrap();
assert!(display.is_clean());
}