// Manages display hardware use crate::dither::DitheredImage; use anyhow::Result; use epd_waveshare::{epd7in3f::Epd7in3f, prelude::WaveshareDisplay}; use linux_embedded_hal::gpio_cdev::{Chip, LineRequestFlags}; use linux_embedded_hal::spidev::SpiModeFlags; use linux_embedded_hal::spidev::SpidevOptions; use linux_embedded_hal::{CdevPin, Delay, SpidevDevice}; use std::sync::mpsc; use std::thread; use tracing::instrument; use tracing::span; use tracing::{debug, error, info, warn, Level}; pub trait EInkPanel { fn display(&mut self, buf: &DitheredImage) -> Result<()>; } pub struct Wrapper { spi: SpidevDevice, delay: Delay, panel: Epd7in3f, } impl Wrapper { 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(Self { spi, delay, panel, }) } pub fn test(&mut self) -> Result<()> { self.panel.show_7block(&mut self.spi, &mut self.delay)?; self.panel.sleep(&mut self.spi, &mut self.delay)?; Ok(()) } } impl EInkPanel for Wrapper { #[instrument(skip_all)] fn display(&mut self, img: &DitheredImage) -> Result<()> { let buf = img.into_display_buffer(); self.panel .update_and_display_frame(&mut self.spi, &buf, &mut self.delay)?; debug!("Finished updating frame"); self.panel.sleep(&mut self.spi, &mut self.delay)?; debug!("Display entered sleep mode"); Ok(()) } } /// A Fake EInk display for testing purposes. /// Saves the output as `display.bmp` pub struct FakeEInk(); impl EInkPanel for FakeEInk { fn display(&mut self, img: &DitheredImage) -> Result<()> { warn!("Fake display was called: saving to display.bmp"); img.into_rgbimage().save("display.bmp")?; Ok(()) } } #[instrument] pub fn get_display() -> Box { match Wrapper::new() { Ok(w) => { info!("Found real hardware, using it"); Box::new(w) } Err(e) => { warn!("Error opening display SPI interface: {e}"); warn!("Falling back to fake display"); Box::new(FakeEInk {}) } } } /// Create a thread that can take dithered images and display them. This allows the display to be /// used in an async context since updating the display can take ~30 seconds. #[must_use] #[instrument(skip_all)] pub fn create_display_thread( mut display: Box, ) -> (thread::JoinHandle<()>, mpsc::Sender>) { let (tx, rx) = mpsc::channel::>(); let handle = thread::spawn(move || { let span = span!(Level::INFO, "display_thread"); let _enter = span.enter(); loop { let res = rx.recv(); match res { Ok(img) => { info!("Received an image to display"); if let Err(e) = display.display(&img) { error!("Error displaying image: {e}"); return; } info!("Successfully set display image"); } Err(e) => { error!("Error reading image from channel: {e}"); return; } } } }); (handle, tx) }