diff --git a/Cargo.toml b/Cargo.toml index 5cd9cd3..d5bfd3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,17 @@ 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 } +image = "0.25.1" +linux-embedded-hal = { version = "0.4.0", optional = true } +num-traits = "0.2.19" +palette = "0.7.6" +rayon = "1.10.0" +rgb = "0.8.40" diff --git a/src/imageproc.rs b/src/imageproc.rs new file mode 100644 index 0000000..6328c01 --- /dev/null +++ b/src/imageproc.rs @@ -0,0 +1,130 @@ +use image::ColorType::Rgb8; + +use image::RgbImage; + +use palette::{cast::FromComponents, color_difference::EuclideanDistance, FromColor, IntoColor, Oklab, Srgb}; + +/// Palette used on the display; pixels can be one of these colors. +/// +/// The RGB values are slightly adjusted to improve accuracy. +const DISPLAY_PALETTE: [Srgb; 7] = [ + Srgb::new(0.047, 0.047, 0.055), // Black + Srgb::new(0.824, 0.824, 0.816), // White + Srgb::new(0.118, 0.376, 0.122), // Green + Srgb::new(0.114, 0.118, 0.667), // Blue + Srgb::new(0.549, 0.106, 0.114), // Red + Srgb::new(0.827, 0.788, 0.239), // Yellow + Srgb::new(0.757, 0.443, 0.165), // Orange +]; + +// fn octcolor_rgb(color: &OctColor) -> &Srgb { +// match color { +// OctColor::Black => &DISPLAY_PALETTE[0], +// OctColor::White => &DISPLAY_PALETTE[1], +// OctColor::Green => &DISPLAY_PALETTE[2], +// OctColor::Blue => &DISPLAY_PALETTE[3], +// OctColor::Red => &DISPLAY_PALETTE[4], +// OctColor::Yellow => &DISPLAY_PALETTE[5], +// OctColor::Orange => &DISPLAY_PALETTE[6], +// OctColor::HiZ => &DISPLAY_PALETTE[1], +// } +// } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisplayColor { + Black, + White, + Green, + Blue, + Red, + Yellow, + Orange, +} + +impl Into for DisplayColor { + fn into(self) -> Srgb { + DISPLAY_PALETTE[self as usize] + } +} + +impl DisplayColor { + fn from_u8(value: u8) -> Self { + match value { + 0 => Self::Black, + 1 => Self::White, + 2 => Self::Green, + 3 => Self::Blue, + 4 => Self::Red, + 5 => Self::Yellow, + 6 => Self::Orange, + _ => panic!("unexpected DisplayColor {}", value), + } + } +} + +/// Buffer to be sent to the EInk display. +#[derive(Debug)] +pub struct EInkBuffer { + data: Vec, + width: usize, + height: usize, +} + +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 trait Ditherer { + fn dither(&self, img: &RgbImage) -> EInkBuffer; +} + +// fn color_distance(c1: LinSrgb, c2: LinSrgb) -> f32 { +// let r2 = (c2.red - c1.red).powf(2.0); +// let g2 = (c2.green - c1.green).powf(2.0); +// let b2 = (c2.blue - c1.blue).powf(2.0); +// +// (r2 + g2 + b2).sqrt() +// } + +/// 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 (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())) + }) + .min_by(|(_, a), (_, b)| a.total_cmp(b)) + .unwrap(); + (DisplayColor::from_u8(nearest as u8), dist) +} + +pub struct NNDither(); + +impl Ditherer for NNDither { + fn dither(&self, img: &RgbImage) -> 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()); + // }); + let srgb = <&[Srgb]>::from_components(&**img); + buf + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..8efaeeb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,61 @@ -fn main() { - println!("Hello, world!"); +use clap::{Args, Parser, Subcommand}; +use epd_waveshare::{epd7in3f::Epd7in3f, prelude::WaveshareDisplay}; + +pub mod imageproc; + +/// The main +#[derive(Debug, Parser)] +#[command(version, about)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Load a single image + Load, + /// 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 { }; + + let mut panel = Epd7in3f::new(&mut spi, busy_pin, dc_pin, rst_pin, &mut delay, None)?; + panel.show_7block(&mut spi, &mut delay)?; + + } + + Ok(()) }