diff --git a/Cargo.lock b/Cargo.lock index 4892c03..f6ffd16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + [[package]] name = "base64" version = "0.13.1" @@ -170,6 +176,7 @@ dependencies = [ "embassy-net", "embassy-sync 0.7.2", "embassy-time", + "embedded-graphics", "embedded-hal 1.0.0", "embedded-hal-async", "embedded-io 0.7.1", @@ -567,6 +574,29 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "embedded-graphics" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" +dependencies = [ + "az", + "byteorder", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -1123,6 +1153,15 @@ dependencies = [ "vcell", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1426,6 +1465,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + [[package]] name = "nb" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 0bf8313..258d906 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ embedded-sdmmc = { version = "0.9.0", features = ["defmt-log"], default-features embassy-embedded-hal = "0.5.0" embassy-sync = "0.7.2" embedded-hal-async = "1.0.0" +embedded-graphics = "0.8.1" [profile.dev] diff --git a/src/eink_display/frame.rs b/src/eink_display/frame.rs new file mode 100644 index 0000000..36fa370 --- /dev/null +++ b/src/eink_display/frame.rs @@ -0,0 +1,111 @@ +use core::ops::{Deref, Range, RangeInclusive}; + +use embedded_graphics::{ + Pixel, + pixelcolor::BinaryColor, + prelude::{DrawTarget, OriginDimensions, Point, Size}, +}; + +use crate::eink_display; + +enum Orientation { + Portrait, + Landscape, +} + +pub(crate) struct Frame { + buffer: [u8; Self::BUFFER_SIZE], + /// The orientation is an experimental idea to allow for different display orientations. + orientation: Orientation, +} + +impl Frame { + // The display is in portrait mode by default + const WIDTH: u16 = eink_display::DISPLAY_WIDTH; + const HEIGHT: u16 = eink_display::DISPLAY_HEIGHT; + + /// Each bit in a byte represents a pixel (0 = off, 1 = on) + const WIDTH_BYTES: usize = { + // There is no div_exact yet + assert!( + Self::WIDTH % 8 == 0, + "Display width must be a multiple of 8" + ); + + Self::WIDTH.strict_div(8) as usize + }; + pub(crate) const BUFFER_SIZE: usize = Self::WIDTH_BYTES.strict_mul(Self::HEIGHT as usize); +} + +impl Default for Frame { + fn default() -> Self { + Frame { + buffer: [0x00; Self::BUFFER_SIZE], + orientation: Orientation::Portrait, + } + } +} + +impl Deref for Frame { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.buffer + } +} + +impl OriginDimensions for Frame { + fn size(&self) -> Size { + Size::new(u32::from(Self::WIDTH), u32::from(Self::HEIGHT)) + } +} + +enum DrawError { + /// If more details about the error are needed at runtime, then add them + OutOfBounds, +} + +impl DrawTarget for Frame { + type Color = BinaryColor; + + type Error = DrawError; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + const X_RANGE: Range = 0..Frame::WIDTH; + const Y_RANGE: Range = 0..Frame::HEIGHT; + + for Pixel(point, color) in pixels { + let x = u16::try_from(point.x).map_err(|_| DrawError::OutOfBounds)?; + let y = u16::try_from(point.y).map_err(|_| DrawError::OutOfBounds)?; + + if !X_RANGE.contains(&x) || !Y_RANGE.contains(&y) { + return Err(DrawError::OutOfBounds); + } + + // Map to pixel on hardware + let x_hardware = usize::from(y); + // Display is inverted + let y_hardware = usize::from(eink_display::DISPLAY_HEIGHT - x); + // Make it zero-indexed + let y_index = y_hardware - 1; + + let row_start = y_index * Frame::WIDTH_BYTES; + // Locate the byte that contains the pixel. This is a floor division + let row_pixel_index = x_hardware / 8; + let index = row_start + row_pixel_index; + // The remainder defines the bit index within the byte. The part that is left over from finding the pixel index in the row (x_hardware / 8) + let bit_index = 7 - x_hardware % 8; + + self.buffer[index] = match color { + // E-Ink light is not charged = white + BinaryColor::On => self.buffer[index] & !(1 << bit_index), + // E-Ink dark is charged = black + BinaryColor::Off => self.buffer[index] | (1 << bit_index), + }; + } + Ok(()) + } +} diff --git a/src/eink_display/mod.rs b/src/eink_display/mod.rs index 5bb26c5..0ef6a7b 100644 --- a/src/eink_display/mod.rs +++ b/src/eink_display/mod.rs @@ -1,10 +1,14 @@ +use core::{convert::Infallible, ops::RangeInclusive}; + pub(crate) use crate::eink_display::error::*; +use crate::eink_display::frame::Frame; use defmt::info; use embassy_time::{Duration, Timer, with_timeout}; use embedded_hal_async::spi::SpiDevice; use esp_hal::gpio::{Input, InputConfig, InputPin, Level, Output, OutputConfig, OutputPin}; mod error; +mod frame; #[derive(Debug, defmt::Format)] #[repr(u8)] @@ -49,7 +53,10 @@ enum ControlMode { BypassRed = 0x40, } -pub(super) struct EinkDisplay<'d, SPI: SpiDevice> { +pub(super) struct EinkDisplay<'d, SPI> +where + SPI: SpiDevice, +{ spi: SPI, reset: Output<'d>, /// Based on usage this pin is used to select between data and command mode. @@ -69,17 +76,6 @@ pub(super) enum RefreshMode { const DISPLAY_WIDTH: u16 = 800; const DISPLAY_HEIGHT: u16 = 480; -const DISPLAY_WIDTH_BYTES: usize = { - // There is no div_exact yet - assert!( - DISPLAY_WIDTH % 8 == 0, - "Display width must be a multiple of 8" - ); - - DISPLAY_WIDTH.strict_div(8) as usize -}; - -pub(crate) const BUFFER_SIZE: usize = DISPLAY_WIDTH_BYTES.strict_mul(DISPLAY_HEIGHT as usize); impl<'d, SPI: SpiDevice> EinkDisplay<'d, SPI> { fn new( @@ -358,7 +354,7 @@ impl<'d, SPI: SpiDevice> EinkDisplay<'d, SPI> { pub(crate) async fn display( &mut self, mut refresh_mode: RefreshMode, - frame_buffer: &[u8], + frame: &Frame, ) -> Result<(), DisplayError> { if !self.is_screen_on { // Force half refresh if screen is off @@ -375,15 +371,15 @@ impl<'d, SPI: SpiDevice> EinkDisplay<'d, SPI> { self.send_command(Command::WriteBwRam).await?; self.data_command.set_high(); - self.send_data(frame_buffer).await?; + self.send_data(&frame).await?; } RefreshMode::HalfRefresh | RefreshMode::Full => { // For full refresh, write to both buffers before refresh self.send_command(Command::WriteBwRam).await?; - self.send_data(frame_buffer).await?; + self.send_data(&frame).await?; self.send_command(Command::WriteRedRam).await?; - self.send_data(frame_buffer).await?; + self.send_data(&frame).await?; } }