diff --git a/Cargo.toml b/Cargo.toml index d5bfd3a..806b396 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,16 +3,12 @@ name = "pi-frame-server" version = "0.1.0" edition = "2021" -[features] -gui = [] -spi = ["dep:epd-waveshare", "dep:linux-embedded-hal"] - [dependencies] anyhow = "1.0.86" clap = { version = "4.5.7", features = ["derive"] } -epd-waveshare = { git = "https://github.com/caemor/epd-waveshare.git", optional = true } +epd-waveshare = { git = "https://github.com/caemor/epd-waveshare.git" } image = "0.25.1" -linux-embedded-hal = { version = "0.4.0", optional = true } +linux-embedded-hal = { version = "0.4.0" } num-traits = "0.2.19" palette = "0.7.6" rayon = "1.10.0" diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0fdbb38 --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set +x +cross build --target aarch64-unknown-linux-gnu +rsync -aczP target/aarch64-unknown-linux-gnu/debug/pi-frame-server 192.168.0.186: +# scp target/aarch64-unknown-linux-gnu/debug/pi-frame-server 192.168.0.186: +ssh 192.168.0.186 ./pi-frame-server load diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..455170b --- /dev/null +++ b/src/display.rs @@ -0,0 +1,88 @@ +use epd_waveshare::{epd7in3f::Epd7in3f, prelude::WaveshareDisplay}; +use linux_embedded_hal::spidev::SpiModeFlags; +use linux_embedded_hal::spidev::SpidevOptions; +use linux_embedded_hal::{CdevPin, Delay, SpidevBus, SpidevDevice}; + +use linux_embedded_hal::gpio_cdev::{Chip, LineRequestFlags}; +use anyhow::Result; + +pub fn demo() -> Result<()> { + let mut spi = SpidevDevice::open("/dev/spidev0.0")?; + let spi_options = SpidevOptions::new() + .bits_per_word(8) + .max_speed_hz(10_000_000) + .mode(SpiModeFlags::SPI_MODE_0) + .build(); + spi.configure(&spi_options)?; + let mut gpiochip = Chip::new("/dev/gpiochip0")?; + let busy_pin = CdevPin::new(gpiochip.get_line(24)?.request( + LineRequestFlags::INPUT, + 0, + "frametool", + )?)?; + let dc_pin = CdevPin::new(gpiochip.get_line(25)?.request( + LineRequestFlags::OUTPUT, + 0, + "frametool", + )?)?; + let rst_pin = CdevPin::new(gpiochip.get_line(17)?.request( + LineRequestFlags::OUTPUT, + 0, + "frametool", + )?)?; + let mut delay = Delay {}; + + let mut panel = Epd7in3f::new(&mut spi, busy_pin, dc_pin, rst_pin, &mut delay, None)?; + panel.show_7block(&mut spi, &mut delay)?; + Ok(()) +} + + +pub struct DisplayWrapper { + spi: SpidevDevice, + gpiochip: Chip, + delay: Delay, + panel: Epd7in3f +} + +impl DisplayWrapper { + pub fn new() -> Result { + let mut spi = SpidevDevice::open("/dev/spidev0.0")?; + let spi_options = SpidevOptions::new() + .bits_per_word(8) + .max_speed_hz(10_000_000) + .mode(SpiModeFlags::SPI_MODE_0) + .build(); + spi.configure(&spi_options)?; + + let mut gpiochip = Chip::new("/dev/gpiochip0")?; + let busy_pin = CdevPin::new(gpiochip.get_line(24)?.request( + LineRequestFlags::INPUT, + 0, + "frametool", + )?)?; + let dc_pin = CdevPin::new(gpiochip.get_line(25)?.request( + LineRequestFlags::OUTPUT, + 0, + "frametool", + )?)?; + let rst_pin = CdevPin::new(gpiochip.get_line(17)?.request( + LineRequestFlags::OUTPUT, + 0, + "frametool", + )?)?; + let mut delay = Delay {}; + let panel = Epd7in3f::new(&mut spi, busy_pin, dc_pin, rst_pin, &mut delay, None)?; + + Ok(DisplayWrapper { + spi, + gpiochip, + delay, + panel + }) + } + pub fn display(&mut self, buf: &[u8]) -> Result<()>{ + self.panel.update_and_display_frame(&mut self.spi, buf, &mut self.delay)?; + Ok(()) + } +} diff --git a/src/imageproc.rs b/src/imageproc.rs index 6328c01..9300e6a 100644 --- a/src/imageproc.rs +++ b/src/imageproc.rs @@ -1,8 +1,7 @@ -use image::ColorType::Rgb8; use image::RgbImage; -use palette::{cast::FromComponents, color_difference::EuclideanDistance, FromColor, IntoColor, Oklab, Srgb}; +use palette::{cast::FromComponents, color_difference::Ciede2000, IntoColor, Lab, Srgb}; /// Palette used on the display; pixels can be one of these colors. /// @@ -41,9 +40,9 @@ pub enum DisplayColor { Orange, } -impl Into for DisplayColor { - fn into(self) -> Srgb { - DISPLAY_PALETTE[self as usize] +impl From for Srgb { + fn from(value: DisplayColor) -> Self { + DISPLAY_PALETTE[value as usize] } } @@ -60,34 +59,62 @@ impl DisplayColor { _ => panic!("unexpected DisplayColor {}", value), } } -} + fn into_byte(color1: Self, color2: Self) -> u8 { + let upper: u8 = color1.into(); + let lower: u8 = color2.into(); + upper << 4 | lower + } +} +impl From for u8 { + fn from(value: DisplayColor) -> Self { + value as u8 + } +} /// Buffer to be sent to the EInk display. #[derive(Debug)] -pub struct EInkBuffer { - data: Vec, - width: usize, - height: usize, -} +pub struct EInkBuffer(Vec); impl EInkBuffer { - /// Converts the EInkBuffer into data that can be sent over the SPI API - /// Bin-packs the two 4-bit colors into bytes. - pub fn into_buffer(&self) -> Vec { - vec![] - } + pub fn into_display_buffer(&self) -> Vec { + let mut buf = Vec::with_capacity(self.0.len()/2); - pub fn new(width: usize, height: usize) -> EInkBuffer { - EInkBuffer { - data: vec![DisplayColor::Black; width * height], - width, - height, + for colors in self.0.chunks_exact(2) { + buf.push(DisplayColor::into_byte(colors[0], colors[1])); } + + buf + } + pub fn new(width: usize, height: usize) -> Self { + let v = vec![DisplayColor::Black; width * height]; + EInkBuffer(v) } } +// impl EInkBuffer { +// /// Converts the EInkBuffer into data that can be sent over the SPI API +// /// Bin-packs the two 4-bit colors into bytes. +// pub fn into_buffer(&self) -> Vec { +// vec![] +// } +// +// pub fn new(width: usize, height: usize) -> EInkBuffer { +// EInkBuffer { +// data: vec![DisplayColor::Black; width * height], +// width, +// height, +// } +// } +// pub fn set(&mut self, x: usize, y: usize, value: DisplayColor) { +// self.data[x + y * self.width] = value; +// } +// pub fn get(&self, x:usize, y:usize) -> DisplayColor { +// self.data[x +// } +// } + pub trait Ditherer { - fn dither(&self, img: &RgbImage) -> EInkBuffer; + fn dither(&self, img: &RgbImage, output: &mut EInkBuffer); } // fn color_distance(c1: LinSrgb, c2: LinSrgb) -> f32 { @@ -101,13 +128,14 @@ pub trait Ditherer { /// Find the closest approximate palette color to the given sRGB value. /// This uses euclidian distance in linear space. pub fn nearest_neighbor(color: Srgb) -> (DisplayColor, f32) { + let input: Lab = color.into_color(); let (nearest, dist) = DISPLAY_PALETTE .iter() .enumerate() .map(|(idx, p_color)| { - let lab: Oklab = p_color.clone().into_color(); - (idx, color.distance(p_color.into_format())) + let c: Lab = (*p_color).into_color(); + (idx, input.difference(c)) }) .min_by(|(_, a), (_, b)| a.total_cmp(b)) .unwrap(); @@ -117,14 +145,18 @@ pub fn nearest_neighbor(color: Srgb) -> (DisplayColor, f32) { pub struct NNDither(); impl Ditherer for NNDither { - fn dither(&self, img: &RgbImage) -> EInkBuffer { + fn dither(&self, img: &RgbImage, output: &mut EInkBuffer) { assert!(img.width() == 800); assert!(img.height() == 480); - let mut buf = EInkBuffer::new(800, 480); - // img.enumerate_pixels().for_each(|(x, y, pix)| { - // buf[x + y * 800] = nearest_neighbor(Srgb::from_components()); - // }); + + // sRGB view into the given image. zero copy! let srgb = <&[Srgb]>::from_components(&**img); - buf + + for (idx, pixel) in srgb.iter().enumerate() { + let (n, _) = nearest_neighbor(pixel.into_format()); + output.0[idx] = n; + } + } } + diff --git a/src/main.rs b/src/main.rs index 8efaeeb..0c6ad6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,12 @@ -use clap::{Args, Parser, Subcommand}; -use epd_waveshare::{epd7in3f::Epd7in3f, prelude::WaveshareDisplay}; - +pub mod display; pub mod imageproc; -/// The main +use crate::display::DisplayWrapper; +use crate::imageproc::{Ditherer, EInkBuffer, NNDither}; +use clap::{Parser, Subcommand}; +use image::RgbImage; + +/// Display images over #[derive(Debug, Parser)] #[command(version, about)] struct Cli { @@ -14,47 +17,25 @@ struct Cli { #[derive(Debug, Subcommand)] enum Command { /// Load a single image - Load, + Show, /// Start the HTTP sever Serve, } -use linux_embedded_hal::spidev::SpidevOptions; -use linux_embedded_hal::spidev::SpiModeFlags; -use linux_embedded_hal::{CdevPin, Delay, SpidevBus, SpidevDevice}; - -use linux_embedded_hal::gpio_cdev::{Chip, LineRequestFlags}; - fn main() -> anyhow::Result<()> { let cli = Cli::parse(); println!("CLI {cli:?}"); - if let Command::Load = cli.command { - println!("loading"); - let mut spi = SpidevDevice::open("/dev/spidev0.0")?; - let spi_options = SpidevOptions::new().bits_per_word(8).max_speed_hz(10_000_000).mode(SpiModeFlags::SPI_MODE_0).build(); - spi.configure(&spi_options)?; - let mut gpiochip = Chip::new("/dev/gpiochip0")?; - let busy_pin = CdevPin::new(gpiochip.get_line(24)?.request( - LineRequestFlags::INPUT, - 0, - "frametool", - )?)?; - let dc_pin = CdevPin::new(gpiochip.get_line(25)?.request( - LineRequestFlags::OUTPUT, - 0, - "frametool", - )?)?; - let rst_pin = CdevPin::new(gpiochip.get_line(17)?.request( - LineRequestFlags::OUTPUT, - 0, - "frametool", - )?)?; - let mut delay = Delay { }; + if let Command::Show = cli.command { + let img: RgbImage = image::io::Reader::open("myimage.png")?.decode()?.into(); + let mut display = DisplayWrapper::new()?; - let mut panel = Epd7in3f::new(&mut spi, busy_pin, dc_pin, rst_pin, &mut delay, None)?; - panel.show_7block(&mut spi, &mut delay)?; + let mut eink_buf = EInkBuffer::new(800, 480); + let dither = NNDither {}; + dither.dither(&img, &mut eink_buf); + let raw_buf = eink_buf.into_display_buffer(); + display.display(&raw_buf)?; } Ok(())