use core::{convert::Infallible, fmt};
use embassy_time::{Duration, Timer};
use embedded_graphics_core::{
Pixel,
draw_target::DrawTarget,
geometry::{OriginDimensions, Size},
pixelcolor::{IntoStorage, Rgb565},
primitives::Rectangle,
};
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_hal::{
Async,
gpio::{Level, Output, OutputConfig},
spi::master::{AnySpi, Spi, SpiDma, SpiMasterDmaChannel},
};
use mipidsi::interface::SpiInterface;
use crate::{
Backlight, DisplayPins, Error, HEIGHT, WIDTH,
display::{PANEL_OFFSET, backlight, init_panel, spi_config},
};
const W: usize = WIDTH as usize;
const H: usize = HEIGHT as usize;
#[derive(Clone, Copy)]
struct Span {
first: u8,
last: u8,
}
impl Span {
const CLEAN: Self = Self {
first: u8::MAX,
last: 0,
};
#[expect(clippy::cast_possible_truncation, reason = "the width (240) fits")]
const FULL: Self = Self {
first: 0,
last: (W - 1) as u8,
};
const fn is_clean(self) -> bool {
self.first > self.last
}
#[expect(clippy::cast_possible_truncation, reason = "x < WIDTH (240)")]
fn with(self, x: usize) -> Self {
let x = x as u8;
Self {
first: self.first.min(x),
last: self.last.max(x),
}
}
fn union(self, other: Self) -> Self {
Self {
first: self.first.min(other.first),
last: self.last.max(other.last),
}
}
}
fn row_hash(row: &[u16]) -> u32 {
let (pairs, _) = row.as_chunks::<2>();
pairs.iter().fold(0x811C_9DC5, |hash, &[a, b]| {
(hash ^ (u32::from(a) | u32::from(b) << 16))
.wrapping_mul(0x0100_0193)
.rotate_left(5)
})
}
pub struct Frame {
pixels: [u16; W * H],
written: [Span; H],
sent: [u32; H],
stale: bool,
}
impl Frame {
#[must_use]
#[expect(
clippy::large_stack_arrays,
reason = "made at compile time, into a static (see above)"
)]
pub const fn new() -> Self {
Self {
pixels: [0; W * H],
written: [Span::FULL; H],
sent: [0; H],
stale: true,
}
}
pub const fn invalidate(&mut self) {
self.written = [Span::FULL; H];
self.stale = true;
}
fn changed(&mut self, y: usize) -> Option<u32> {
if self.written[y].is_clean() {
return None;
}
let hash = row_hash(&self.pixels[y * W..(y + 1) * W]);
if !self.stale && hash == self.sent[y] {
self.written[y] = Span::CLEAN;
return None;
}
Some(hash)
}
fn raw(color: Rgb565) -> u16 {
u16::from_ne_bytes(color.into_storage().to_be_bytes())
}
fn fill_row(&mut self, y: usize, x0: usize, x1: usize, raw: u16) {
let row = &mut self.pixels[y * W + x0..=y * W + x1];
let (head, words, tail) = bytemuck::pod_align_to_mut::<u16, u32>(row);
head.fill(raw);
words.fill(u32::from(raw) * 0x0001_0001);
tail.fill(raw);
self.written[y] = self.written[y].with(x0).with(x1);
}
}
impl Default for Frame {
fn default() -> Self {
Self::new()
}
}
impl OriginDimensions for Frame {
fn size(&self) -> Size {
Size::new(WIDTH.into(), HEIGHT.into())
}
}
impl DrawTarget for Frame {
type Color = Rgb565;
type Error = Infallible;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Infallible>
where
I: IntoIterator<Item = Pixel<Rgb565>>,
{
for Pixel(point, color) in pixels {
if let (Ok(x), Ok(y)) = (usize::try_from(point.x), usize::try_from(point.y))
&& x < W
&& y < H
{
self.pixels[y * W + x] = Self::raw(color);
self.written[y] = self.written[y].with(x);
}
}
Ok(())
}
fn fill_solid(&mut self, area: &Rectangle, color: Rgb565) -> Result<(), Infallible> {
let clip = |start: i32, length: u32, limit: usize| {
let end = start.saturating_add_unsigned(length);
let clamp = |v: i32| usize::try_from(v).unwrap_or(0).min(limit);
(clamp(start), clamp(end))
};
let (x0, x1) = clip(area.top_left.x, area.size.width, W);
let (y0, y1) = clip(area.top_left.y, area.size.height, H);
if x0 >= x1 {
return Ok(());
}
let raw = Self::raw(color);
for y in y0..y1 {
self.fill_row(y, x0, x1 - 1, raw);
}
Ok(())
}
}
#[derive(Debug)]
pub struct FrameDisplay {
spi: SpiDma<'static, Async>,
cs: Output<'static>,
dc: Output<'static>,
_reset: Output<'static>,
}
const MIN_DMA_TRANSFER: usize = 64;
const SLPIN: u8 = 0x10;
const SLPOUT: u8 = 0x11;
const CASET: u8 = 0x2A;
const RASET: u8 = 0x2B;
const RAMWR: u8 = 0x2C;
const COLUMN_OFFSET: u16 = PANEL_OFFSET.1;
const ROW_OFFSET: u16 = 240 - (HEIGHT + PANEL_OFFSET.0);
impl FrameDisplay {
pub async fn flush(&mut self, frame: &mut Frame) -> Result<(), Error> {
self.cs.set_low();
let sent = self.send(frame).await;
self.cs.set_high();
sent
}
async fn send(&mut self, frame: &mut Frame) -> Result<(), Error> {
let mut hashes = [0; H];
let mut y = 0;
while y < H {
let top = y;
let mut span = Span::CLEAN;
while y < H
&& let Some(hash) = frame.changed(y)
{
hashes[y] = hash;
span = span.union(frame.written[y]);
y += 1;
}
if span.is_clean() {
y += 1;
continue;
}
let (first, last) = (usize::from(span.first), usize::from(span.last));
self.window(first, top, last, y - 1).await?;
if first == 0 && last == W - 1 {
self.write(bytemuck::cast_slice(&frame.pixels[top * W..y * W]))
.await?;
} else {
for row in top..y {
let pixels = &frame.pixels[row * W + first..=row * W + last];
self.write(bytemuck::cast_slice(pixels)).await?;
}
}
frame.sent[top..y].copy_from_slice(&hashes[top..y]);
frame.written[top..y].fill(Span::CLEAN);
}
frame.stale = false;
Ok(())
}
pub async fn set_asleep(&mut self, asleep: bool) -> Result<(), Error> {
self.cs.set_low();
let sent = self.command(if asleep { SLPIN } else { SLPOUT }, &[]).await;
self.cs.set_high();
Timer::after(Duration::from_millis(5)).await;
sent
}
#[expect(clippy::cast_possible_truncation, reason = "inside the 240×135 frame")]
async fn window(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) -> Result<(), Error> {
let range = |from: usize, to: usize, offset: u16| {
let [a, b] = (from as u16 + offset).to_be_bytes();
let [c, d] = (to as u16 + offset).to_be_bytes();
[a, b, c, d]
};
self.command(CASET, &range(x0, x1, COLUMN_OFFSET)).await?;
self.command(RASET, &range(y0, y1, ROW_OFFSET)).await?;
self.command(RAMWR, &[]).await
}
async fn command(&mut self, command: u8, args: &[u8]) -> Result<(), Error> {
self.dc.set_low();
self.write(&[command]).await?;
self.dc.set_high();
self.write(args).await
}
async fn write(&mut self, bytes: &[u8]) -> Result<(), Error> {
self.spi
.write_async(bytes)
.await
.map_err(|_| Error::DisplayWrite)
}
}
pub fn frame_display(
pins: DisplayPins,
dma: impl SpiMasterDmaChannel<'static, AnySpi<'static>>,
) -> Result<(FrameDisplay, Backlight), Error> {
let backlight = backlight(pins.ledc, pins.backlight)?;
let mut dc = Output::new(pins.dc, Level::Low, OutputConfig::default());
let mut reset = Output::new(pins.reset, Level::High, OutputConfig::default());
let mut cs = Output::new(pins.cs, Level::High, OutputConfig::default());
let mut spi = Spi::new(
pins.spi,
spi_config().with_min_async_transfer_size(MIN_DMA_TRANSFER),
)
.map_err(|_| Error::DisplaySpi)?
.with_sck(pins.sck)
.with_mosi(pins.mosi)
.with_dma(dma);
let mut buffer = [0; 64];
let device = ExclusiveDevice::new_no_delay(&mut spi, &mut cs).map_err(|_| Error::DisplaySpi)?;
init_panel(SpiInterface::new(device, &mut dc, &mut buffer), &mut reset)?;
Ok((
FrameDisplay {
spi: spi.into_async(),
cs,
dc,
_reset: reset,
},
backlight,
))
}
impl fmt::Debug for Frame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let written = self.written.iter().filter(|span| !span.is_clean()).count();
f.debug_struct("Frame")
.field("rows_written", &written)
.field("stale", &self.stale)
.finish_non_exhaustive()
}
}