refactor(editor): extract editor + display into host-testable crates

Move the editor core out of the firmware crate (pinned to the xtensa
target, so it can't run `cargo test`) into a standalone `editor` crate,
with the panel framebuffer + geometry split into a `display` crate. Both
depend only on embedded-graphics + keymap, so the editor is now
host-buildable and unit-tested (8 characterization tests). Firmware links
them and re-exports the geometry from epd.rs; behaviour is unchanged.

The xtensa firmware build was not verified in this environment.
This commit is contained in:
Julien Calixte
2026-07-11 00:28:37 +02:00
parent 875658a661
commit e8063a2b13
7 changed files with 189 additions and 65 deletions

66
display/src/lib.rs Normal file
View File

@@ -0,0 +1,66 @@
//! The e-paper panel's geometry and an in-memory drawable frame.
//!
//! Split out of the hardware driver (`firmware/src/epd.rs`) so the driver and
//! the host-testable `editor` crate share one framebuffer definition. `Frame`
//! is a pure `embedded-graphics` [`DrawTarget`]; the `Epd` driver in firmware
//! consumes its raw bytes via [`Frame::bytes`] and never names the type, so
//! nothing here depends on esp-idf and the whole crate builds on the host.
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::prelude::*;
pub const WIDTH: u16 = 792;
pub const HEIGHT: u16 = 272;
/// Full-frame 1-bit framebuffer: 792 px = 99 bytes per row, MSB-first,
/// 1 = white, 0 = black (SSD16xx convention).
pub const FB_BYTES_W: usize = (WIDTH / 8) as usize; // 99
pub const FB_BYTES: usize = FB_BYTES_W * HEIGHT as usize; // 26928
/// In-memory 792×272 1-bit frame, drawable via `embedded-graphics`.
/// `BinaryColor::On` = black ink, `Off` = white paper.
pub struct Frame {
buf: Vec<u8>,
}
impl Frame {
pub fn new_white() -> Self {
Self { buf: vec![0xFF; FB_BYTES] }
}
pub fn new_black() -> Self {
Self { buf: vec![0x00; FB_BYTES] }
}
pub fn bytes(&self) -> &[u8] {
&self.buf
}
}
impl OriginDimensions for Frame {
fn size(&self) -> Size {
Size::new(WIDTH as u32, HEIGHT as u32)
}
}
impl DrawTarget for Frame {
type Color = BinaryColor;
type Error = core::convert::Infallible;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
for Pixel(p, color) in pixels {
if (0..WIDTH as i32).contains(&p.x) && (0..HEIGHT as i32).contains(&p.y) {
let idx = p.y as usize * FB_BYTES_W + p.x as usize / 8;
let bit = 0x80u8 >> (p.x % 8);
match color {
BinaryColor::On => self.buf[idx] &= !bit, // black ink
BinaryColor::Off => self.buf[idx] |= bit, // white paper
}
}
}
Ok(())
}
}