A fork of pulp-os for the xteink4 adding custom apps
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: move structure around to somehting a bit more clear

hansmrtn 133aae52 e72c3540

+467 -351
+2 -2
src/bin/main.rs
··· 21 21 }; 22 22 23 23 use pulp_os::board::Board; 24 - use pulp_os::display::DisplayDriver; 25 - use pulp_os::input::{Event, InputDriver}; 24 + use pulp_os::drivers::display::DisplayDriver; 25 + use pulp_os::drivers::input::{Event, InputDriver}; 26 26 27 27 extern crate alloc; 28 28
-179
src/board.rs
··· 1 - //! XTEink X4 BSP 2 - //! 3 - //! This module is to map the X4's physical hardware to named subsystems. 4 - //! All the pin assignments, bus configs, and calibration consts are here. 5 - //! Goal: have no need for another part of pulp-os to know GPIO 6 - //! 7 - //! Pin Map: 8 - //! GPIO | Function | Notes 9 - //! 1 | ADC1 - Button 2 | Resistance ladder button for Right/Left/Confirm/Back 10 - //! 2 | ADC2 - Button 1 | Resistance ladder (for consistency): Volume Up/Down 11 - //! 3 | Digital - Power | Active LOW, internal pullup 12 - //! 5 | EPD RST | Reset (active low) 13 - //! 6 | EPD BUSY | Busy signal from display 14 - //! 8 | SPI2 SCK | Shared SPI clock 15 - //! 10 | SPI2 MOSI | Shared SPI data out 16 - //! 21 | EPD CS | Display chip select 17 - 18 - use esp_hal::{ 19 - analog::adc::{ Adc, AdcCalCurve, AdcPin, AdcConfig, Attenuation}, 20 - gpio::{Output, Input, Pull, InputConfig, OutputConfig, Level }, 21 - peripherals::{ Peripherals, ADC1, GPIO1, GPIO2 }, 22 - time::Rate, 23 - delay::Delay, 24 - spi, 25 - Blocking, 26 - }; 27 - use embedded_hal_bus::spi::ExclusiveDevice; 28 - use ssd1677::{Interface, Display, Rotation, Builder, Dimensions}; 29 - 30 - // Display 31 - pub const DISPLAY_WIDTH: u16 = 800; 32 - pub const DISPLAY_HEIGHT: u16 = 480; 33 - 34 - pub const FB_SIZE: usize = (DISPLAY_WIDTH as usize * DISPLAY_HEIGHT as usize) / 8; 35 - 36 - // SPI clock rate for the epaper dispaly 37 - pub const EPD_SPI_FREQ: u32 = 10; 38 - 39 - // Buttons 40 - pub const BTN_TOLERANCE: u16 = 150; 41 - 42 - // Calibrated(?) tolerance band for the resistence ladder btns 43 - pub const ROW1_THRESHOLDS: &[(u16, u16, Button)] = &[ 44 - // center (mV), move, button 45 - (3, 50, Button::Right), 46 - (1113, BTN_TOLERANCE, Button::Left), 47 - (1984, BTN_TOLERANCE, Button::Back), 48 - (2556, BTN_TOLERANCE, Button::Confirm), 49 - ]; 50 - 51 - pub const ROW2_THRESHOLDS: &[(u16, u16, Button)] = &[ 52 - (3, 50, Button::VolDown), 53 - (1659, BTN_TOLERANCE, Button::VolUp), 54 - ]; 55 - 56 - pub type SpiBus = spi::master::Spi<'static, Blocking>; 57 - 58 - pub type SpiDev = ExclusiveDevice<SpiBus, Output<'static>, Delay>; 59 - 60 - pub type EpdInterface = Interface<SpiDev, Output<'static>, Output<'static>, Input<'static>>; 61 - 62 - pub type Epd = Display<EpdInterface>; 63 - 64 - pub type EpdCs = Output<'static>; 65 - pub type EpdDc = Output<'static>; 66 - pub type EpdRst = Output<'static>; 67 - pub type EpdBusy = Input<'static>; 68 - pub type PowerButton = Input<'static>; 69 - 70 - // pub type AdcRow1 = AdcPin<P, ADC1<'static>, AdcCalCurve<ADC1<'static>>>; 71 - // pub type AdcRow2 = AdcPin<P, ADC1<'static>, AdcCalCurve<ADC1<'static>>>; 72 - 73 - #[derive(Debug, Clone, Copy, PartialEq, Eq)] 74 - pub enum Button { 75 - Right, 76 - Left, 77 - Confirm, 78 - Back, 79 - VolUp, 80 - VolDown, 81 - Power, 82 - } 83 - 84 - impl Button { 85 - pub fn name(self) -> &'static str { 86 - match self { 87 - Button::Right => "Right", 88 - Button::Left => "Left", 89 - Button::Confirm => "Confirm", 90 - Button::Back => "Back", 91 - Button::VolUp => "Vol Up", 92 - Button::VolDown => "Vol Down", 93 - Button::Power => "Power", 94 - } 95 - } 96 - } 97 - 98 - impl core::fmt::Display for Button { 99 - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 100 - f.write_str(self.name()) 101 - } 102 - } 103 - 104 - pub struct InputHw { 105 - pub adc: Adc<'static, ADC1<'static>, Blocking>, 106 - pub row1: AdcPin<GPIO1<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, 107 - pub row2: AdcPin<GPIO2<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, 108 - pub power: Input<'static>, 109 - } 110 - 111 - pub struct DisplayHw { 112 - pub epd: Epd, // type alias chain from before 113 - } 114 - 115 - pub struct Board { 116 - pub input: InputHw, 117 - pub display: DisplayHw, 118 - } 119 - 120 - impl Board{ 121 - pub fn init(p: Peripherals) -> Self { 122 - let mut adc_cfg = AdcConfig::new(); 123 - 124 - let row1 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 125 - p.GPIO1, 126 - Attenuation::_11dB, 127 - ); 128 - 129 - let row2 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 130 - p.GPIO2, 131 - Attenuation::_11dB, 132 - ); 133 - 134 - let adc = Adc::new(p.ADC1, adc_cfg); 135 - 136 - let power = Input::new(p.GPIO3, InputConfig::default().with_pull(Pull::Up)); 137 - 138 - let input = InputHw { adc, row1, row2, power }; 139 - 140 - // Display: SPI bus + EPD 141 - let cs = Output::new(p.GPIO21,Level::High, OutputConfig::default()); 142 - let dc= Output::new(p.GPIO4,Level::High, OutputConfig::default()); 143 - let rst= Output::new(p.GPIO5,Level::High, OutputConfig::default()); 144 - let busy= Input::new(p.GPIO6, InputConfig::default().with_pull(Pull::None)); 145 - 146 - let spi_cfg = spi::master::Config::default().with_frequency(Rate::from_mhz(EPD_SPI_FREQ)); 147 - let spi_bus = spi::master::Spi::new(p.SPI2, spi_cfg).unwrap().with_sck(p.GPIO8).with_mosi(p.GPIO10); 148 - 149 - let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay::new()).unwrap(); 150 - 151 - let interface = Interface::new(spi_dev, dc, rst, busy); 152 - 153 - let dims = Dimensions::new(DISPLAY_HEIGHT, DISPLAY_WIDTH).unwrap(); 154 - let cfg = Builder::new() 155 - .dimensions(dims) 156 - .rotation(Rotation::Rotate270) 157 - .build() 158 - .unwrap(); 159 - 160 - let mut epd = Display::new(interface, cfg); 161 - epd.reset(&mut Delay::new()); 162 - 163 - let display = DisplayHw { epd }; 164 - 165 - Board { input, display } 166 - } 167 - } 168 - 169 - 170 - /// Decode a millivolt reading against a threshold table. 171 - /// Used by the input subsystem to map ADC readings to buttons. 172 - pub fn decode_ladder(mv: u16, thresholds: &[(u16, u16, Button)]) -> Option<Button> { 173 - for &(center, tol, button) in thresholds { 174 - if mv >= center.saturating_sub(tol) && mv <= center.saturating_add(tol) { 175 - return Some(button); 176 - } 177 - } 178 - None 179 - }
+69
src/board/button.rs
··· 1 + //! Button definitions and ADC decoding for XTEink X4 2 + //! 3 + //! The X4 uses resistance ladder circuits for most buttons. 4 + //! Each ladder is read via ADC and decoded by comparing the 5 + //! millivolt reading against known thresholds. 6 + 7 + /// All physical buttons on the device. 8 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 + pub enum Button { 10 + // Navigation cluster (Row 1 - GPIO1) 11 + Right, 12 + Left, 13 + Confirm, 14 + Back, 15 + // Volume buttons (Row 2 - GPIO2) 16 + VolUp, 17 + VolDown, 18 + // Discrete digital button 19 + Power, 20 + } 21 + 22 + impl Button { 23 + pub const fn name(self) -> &'static str { 24 + match self { 25 + Button::Right => "Right", 26 + Button::Left => "Left", 27 + Button::Confirm => "Confirm", 28 + Button::Back => "Back", 29 + Button::VolUp => "Vol Up", 30 + Button::VolDown => "Vol Down", 31 + Button::Power => "Power", 32 + } 33 + } 34 + } 35 + 36 + impl core::fmt::Display for Button { 37 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 38 + f.write_str(self.name()) 39 + } 40 + } 41 + 42 + // ADC Threshold Tables 43 + // Each entry: (center_mv, tolerance_mv, button) 44 + // A reading matches if: center - tolerance <= reading <= center + tolerance 45 + pub const DEFAULT_TOLERANCE: u16 = 150; 46 + 47 + pub const ROW1_THRESHOLDS: &[(u16, u16, Button)] = &[ 48 + (3, 50, Button::Right), // Near ground 49 + (1113, DEFAULT_TOLERANCE, Button::Left), 50 + (1984, DEFAULT_TOLERANCE, Button::Back), 51 + (2556, DEFAULT_TOLERANCE, Button::Confirm), 52 + ]; 53 + 54 + pub const ROW2_THRESHOLDS: &[(u16, u16, Button)] = &[ 55 + (3, 50, Button::VolDown), // Near ground 56 + (1659, DEFAULT_TOLERANCE, Button::VolUp), 57 + ]; 58 + 59 + pub fn decode_ladder(mv: u16, thresholds: &[(u16, u16, Button)]) -> Option<Button> { 60 + for &(center, tolerance, button) in thresholds { 61 + let low = center.saturating_sub(tolerance); 62 + let high = center.saturating_add(tolerance); 63 + if mv >= low && mv <= high { 64 + return Some(button); 65 + } 66 + } 67 + None 68 + } 69 +
+12
src/board/display.rs
··· 1 + //! Display hardware constants for XTEink X4 2 + //! 3 + //! The X4 uses an 800x480 e-paper display driven by an SSD1677 controller. 4 + 5 + pub const WIDTH: u16 = 800; 6 + pub const HEIGHT: u16 = 480; 7 + 8 + /// Framebuffer size in bytes (1 bit per pixel, packed). 9 + pub const FRAMEBUFFER_SIZE: usize = (WIDTH as usize * HEIGHT as usize) / 8; 10 + 11 + /// The SSD1677 typically supports up to 20MHz, but 10MHz seems fine for now 12 + pub const SPI_FREQ_MHZ: u32 = 10;
+119
src/board/mod.rs
··· 1 + //! XTEink X4 Board Support Package (BSP) 2 + //! 3 + //! This module provides hardware abstraction for the XTEink X4 e-reader. 4 + //! It maps physical hardware to named subsystems so that application code 5 + //! doesn't need to know GPIO numbers or peripheral details. 6 + 7 + pub mod button; 8 + pub mod display; 9 + pub mod pins; 10 + 11 + pub use button::{decode_ladder, Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS}; 12 + 13 + use embedded_hal_bus::spi::ExclusiveDevice; 14 + use esp_hal::{ 15 + analog::adc::{Adc, AdcCalCurve, AdcConfig, AdcPin, Attenuation}, 16 + delay::Delay, 17 + gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}, 18 + peripherals::{Peripherals, ADC1, GPIO1, GPIO2}, 19 + spi, 20 + time::Rate, 21 + Blocking, 22 + }; 23 + use ssd1677::{Builder, Dimensions, Display, Interface, Rotation}; 24 + 25 + // Type Aliases 26 + pub type SpiBus = spi::master::Spi<'static, Blocking>; 27 + pub type SpiDevice = ExclusiveDevice<SpiBus, Output<'static>, Delay>; 28 + pub type EpdInterface = Interface<SpiDevice, Output<'static>, Output<'static>, Input<'static>>; 29 + pub type Epd = Display<EpdInterface>; 30 + 31 + // Hardware Bundles 32 + /// Input subsystem hardware: ADC for button ladders + power button. 33 + pub struct InputHw { 34 + pub adc: Adc<'static, ADC1<'static>, Blocking>, 35 + pub row1: AdcPin<GPIO1<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, 36 + pub row2: AdcPin<GPIO2<'static>, ADC1<'static>, AdcCalCurve<ADC1<'static>>>, 37 + pub power: Input<'static>, 38 + } 39 + 40 + /// Display subsystem hardware: initialized e-paper display. 41 + pub struct DisplayHw { 42 + pub epd: Epd, 43 + } 44 + 45 + /// Complete board hardware, ready for driver initialization. 46 + pub struct Board { 47 + pub input: InputHw, 48 + pub display: DisplayHw, 49 + } 50 + 51 + impl Board { 52 + pub fn init(p: Peripherals) -> Self { 53 + let input = Self::init_input(&p); 54 + let display = Self::init_display(p); 55 + Board { input, display } 56 + } 57 + 58 + fn init_input(p: &Peripherals) -> InputHw { 59 + let mut adc_cfg = AdcConfig::new(); 60 + 61 + // Configure both ADC channels with 11dB attenuation for full 0-3.3V range 62 + let row1 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 63 + unsafe { p.GPIO1.clone_unchecked() }, 64 + Attenuation::_11dB, 65 + ); 66 + 67 + let row2 = adc_cfg.enable_pin_with_cal::<_, AdcCalCurve<ADC1>>( 68 + unsafe { p.GPIO2.clone_unchecked() }, 69 + Attenuation::_11dB, 70 + ); 71 + 72 + let adc = Adc::new(unsafe { p.ADC1.clone_unchecked() }, adc_cfg); 73 + 74 + let power = Input::new( 75 + unsafe { p.GPIO3.clone_unchecked() }, 76 + InputConfig::default().with_pull(Pull::Up), 77 + ); 78 + 79 + InputHw { 80 + adc, 81 + row1, 82 + row2, 83 + power, 84 + } 85 + } 86 + 87 + fn init_display(p: Peripherals) -> DisplayHw { 88 + // GPIO setup 89 + let cs = Output::new(p.GPIO21, Level::High, OutputConfig::default()); 90 + let dc = Output::new(p.GPIO4, Level::High, OutputConfig::default()); 91 + let rst = Output::new(p.GPIO5, Level::High, OutputConfig::default()); 92 + let busy = Input::new(p.GPIO6, InputConfig::default().with_pull(Pull::None)); 93 + 94 + // SPI bus 95 + let spi_cfg = 96 + spi::master::Config::default().with_frequency(Rate::from_mhz(display::SPI_FREQ_MHZ)); 97 + let spi_bus = spi::master::Spi::new(p.SPI2, spi_cfg) 98 + .unwrap() 99 + .with_sck(p.GPIO8) 100 + .with_mosi(p.GPIO10); 101 + 102 + let spi_dev = ExclusiveDevice::new(spi_bus, cs, Delay::new()).unwrap(); 103 + 104 + // Display controller 105 + let interface = Interface::new(spi_dev, dc, rst, busy); 106 + 107 + let dims = Dimensions::new(display::HEIGHT, display::WIDTH).unwrap(); 108 + let cfg = Builder::new() 109 + .dimensions(dims) 110 + .rotation(Rotation::Rotate270) 111 + .build() 112 + .unwrap(); 113 + 114 + let mut epd = Display::new(interface, cfg); 115 + epd.reset(&mut Delay::new()); 116 + 117 + DisplayHw { epd } 118 + } 119 + }
+28
src/board/pins.rs
··· 1 + //! GPIO | Function | Notes 2 + //! -----+-----------------+---------------------------------- 3 + //! 1 | ADC1 - Button 2 | Resistance ladder: Right/Left/Confirm/Back 4 + //! 2 | ADC2 - Button 1 | Resistance ladder: Volume Up/Down 5 + //! 3 | Digital - Power | Active LOW, internal pullup 6 + //! 4 | EPD DC | Data/Command select 7 + //! 5 | EPD RST | Reset (active low) 8 + //! 6 | EPD BUSY | Busy signal from display 9 + //! 8 | SPI2 SCK | Shared SPI clock 10 + //! 10 | SPI2 MOSI | Shared SPI data out 11 + //! 21 | EPD CS | Display chip select 12 + 13 + // ----- E-Paper Display ----- 14 + pub const EPD_CS: u8 = 21; 15 + pub const EPD_DC: u8 = 4; 16 + pub const EPD_RST: u8 = 5; 17 + pub const EPD_BUSY: u8 = 6; 18 + 19 + // ----- SPI Bus ----- 20 + pub const SPI_SCK: u8 = 8; 21 + pub const SPI_MOSI: u8 = 10; 22 + 23 + // ----- Buttons (ADC) ----- 24 + pub const BTN_ROW1_ADC: u8 = 1; // GPIO1 - Right/Left/Confirm/Back 25 + pub const BTN_ROW2_ADC: u8 = 2; // GPIO2 - Vol Up/Down 26 + 27 + // ----- Power Button ----- 28 + pub const BTN_POWER: u8 = 3; // Digital, active LOW
+32 -8
src/display.rs src/drivers/display.rs
··· 1 + //! Display driver for XTEink X4 e-paper 2 + //! 3 + //! Wraps the SSD1677-based e-paper display with a framebuffer and 4 + //! implements `embedded_graphics::DrawTarget` for easy rendering. 5 + 1 6 extern crate alloc; 7 + 2 8 use alloc::vec; 3 9 use alloc::vec::Vec; 4 10 use core::convert::Infallible; 5 11 6 12 use embedded_graphics_core::{ 7 13 draw_target::DrawTarget, 8 - geometry::{OriginDimensions, Size}, 14 + geometry::{OriginDimensions, Point, Size}, 9 15 pixelcolor::BinaryColor, 10 16 prelude::Pixel, 11 - geometry::Point, 12 17 }; 13 18 use esp_hal::delay::Delay; 14 19 use log::info; 15 - use ssd1677::{RefreshMode, Region, Rotation, UpdateRegion}; 16 20 use ssd1677::rotation::apply_rotation; 21 + use ssd1677::{RefreshMode, Region, UpdateRegion}; 17 22 18 - use crate::board::{DisplayHw, Epd, DISPLAY_HEIGHT, DISPLAY_WIDTH, FB_SIZE}; 23 + use crate::board::display::{FRAMEBUFFER_SIZE, HEIGHT, WIDTH}; 24 + use crate::board::{DisplayHw, Epd}; 19 25 26 + /// Number of fast/partial refreshes before forcing a full refresh. 27 + /// 28 + /// E-paper displays accumulate "ghosting" artifacts with partial updates. 29 + /// A periodic full refresh clears these artifacts. 20 30 const FULL_REFRESH_INTERVAL: u32 = 10; 21 31 32 + /// High-level display driver with framebuffer management. 22 33 pub struct DisplayDriver { 23 34 epd: Epd, 24 35 buf: Vec<u8>, 25 - /// Number of fast/partial flushes since the last full refresh. 26 36 fast_count: u32, 27 37 dirty: bool, 28 38 } 29 39 30 40 impl DisplayDriver { 41 + /// Create a new display driver from initialized hardware. 31 42 pub fn new(hw: DisplayHw) -> Self { 32 - let buf = vec![0xFFu8; FB_SIZE]; // 0xFF = all white 43 + // 0xFF = all white (bit set = white pixel for this controller) 44 + let buf = vec![0xFFu8; FRAMEBUFFER_SIZE]; 33 45 34 46 Self { 35 47 epd: hw.epd, ··· 39 51 } 40 52 } 41 53 54 + /// Clear the framebuffer to white. 42 55 pub fn clear_white(&mut self) { 43 56 self.buf.fill(0xFF); 44 57 self.dirty = true; 45 58 } 46 59 60 + /// Clear the framebuffer to black. 47 61 pub fn clear_black(&mut self) { 48 62 self.buf.fill(0x00); 49 63 self.dirty = true; 50 64 } 51 65 66 + /// Flush the framebuffer to the display. 67 + /// 68 + /// Automatically chooses full vs. fast refresh based on the 69 + /// number of fast refreshes since the last full refresh. 52 70 pub fn flush(&mut self, delay: &mut Delay) { 53 71 if !self.dirty { 54 72 return; ··· 64 82 self.flush_inner(mode, delay); 65 83 } 66 84 85 + /// Flush with a specific refresh mode. 67 86 pub fn flush_with_mode(&mut self, mode: RefreshMode, delay: &mut Delay) { 68 87 self.flush_inner(mode, delay); 69 88 } 70 89 90 + /// Force a full refresh (clears ghosting artifacts). 71 91 pub fn flush_full(&mut self, delay: &mut Delay) { 72 92 self.flush_inner(RefreshMode::Full, delay); 73 93 } 74 94 95 + /// Returns `true` if the framebuffer has been modified since the last flush. 75 96 pub fn is_dirty(&self) -> bool { 76 97 self.dirty 77 98 } 78 99 100 + /// Number of fast refreshes since the last full refresh. 79 101 pub fn fast_count(&self) -> u32 { 80 102 self.fast_count 81 103 } 82 104 105 + /// Access the underlying EPD driver (for advanced use). 83 106 pub fn epd(&mut self) -> &mut Epd { 84 107 &mut self.epd 85 108 } 86 109 110 + /// Read-only access to the framebuffer. 87 111 pub fn buffer(&self) -> &[u8] { 88 112 &self.buf 89 113 } 90 114 91 115 fn flush_inner(&mut self, mode: RefreshMode, delay: &mut Delay) { 92 - let region = Region::new(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); 116 + let region = Region::new(0, 0, WIDTH, HEIGHT); 93 117 94 118 let update = UpdateRegion { 95 119 region, ··· 113 137 fn set_pixel(&mut self, x: u32, y: u32, on: bool) { 114 138 let rotation = self.epd.rotation(); 115 139 116 - // Physical (unrotated) dimensions — cols is the byte-row width. 140 + // Physical (unrotated) dimensions 117 141 let dims = self.epd.dimensions(); 118 142 let width = dims.cols as u32; 119 143 let height = dims.rows as u32;
+197
src/drivers/input.rs
··· 1 + //! Input event driver for XTEink X4 2 + //! 3 + //! The X4 has three physical input sources that all funnel into a 4 + //! single "one button at a time" model: 5 + //! 6 + //! - **Row 1 ADC** (GPIO1): Right, Left, Confirm, Back via resistance ladder 7 + //! - **Row 2 ADC** (GPIO2): Volume Up/Down via resistance ladder 8 + //! - **Power button** (GPIO3): Digital input, active low 9 + //! 10 + //! Because each resistance ladder can only report one press at a time, 11 + //! we collapse everything into `Option<Button>` per poll cycle. 12 + 13 + use esp_hal::time::{Duration, Instant}; 14 + 15 + use crate::board::button::{decode_ladder, Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS}; 16 + use crate::board::InputHw; 17 + 18 + /// Debounce time - ignore state changes shorter than this. 19 + const DEBOUNCE_MS: u64 = 30; 20 + 21 + /// Time held before firing a long-press event. 22 + const LONG_PRESS_MS: u64 = 600; 23 + 24 + /// Interval between repeat events when holding past long-press. 25 + const REPEAT_MS: u64 = 150; 26 + 27 + /// Input events returned from [`InputDriver::poll`]. 28 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 29 + pub enum Event { 30 + /// Button was just pressed. 31 + Press(Button), 32 + /// Button was just released. 33 + Release(Button), 34 + /// Button held long enough to trigger long-press. 35 + LongPress(Button), 36 + /// Button still held, firing repeat event. 37 + Repeat(Button), 38 + } 39 + 40 + /// Small fixed-size event queue for buffering multiple events per poll. 41 + /// 42 + /// Needed because a single state change can produce both Release and Press. 43 + struct EventQueue { 44 + buf: [Option<Event>; 2], 45 + read: u8, 46 + } 47 + 48 + impl EventQueue { 49 + const fn new() -> Self { 50 + Self { 51 + buf: [None; 2], 52 + read: 0, 53 + } 54 + } 55 + 56 + fn push(&mut self, ev: Event) { 57 + for slot in self.buf.iter_mut() { 58 + if slot.is_none() { 59 + *slot = Some(ev); 60 + return; 61 + } 62 + } 63 + // If both slots are full, something is wrong with our logic. 64 + // Silently dropping is safer than panic in embedded. 65 + } 66 + 67 + fn pop(&mut self) -> Option<Event> { 68 + if (self.read as usize) < self.buf.len() { 69 + let idx = self.read as usize; 70 + if let Some(ev) = self.buf[idx].take() { 71 + self.read += 1; 72 + return Some(ev); 73 + } 74 + } 75 + // Reset for next cycle 76 + self.read = 0; 77 + None 78 + } 79 + 80 + fn is_empty(&self) -> bool { 81 + self.buf.iter().all(|s| s.is_none()) 82 + } 83 + } 84 + 85 + /// Stateful input driver with debouncing, long-press, and repeat support. 86 + pub struct InputDriver { 87 + hw: InputHw, 88 + /// Currently stable (debounced) button state. 89 + stable: Option<Button>, 90 + /// Candidate state during debounce window. 91 + candidate: Option<Button>, 92 + /// When the candidate state was first seen. 93 + candidate_since: Instant, 94 + /// When the current stable button was first pressed. 95 + press_since: Instant, 96 + /// Whether we've already fired a long-press for the current hold. 97 + long_press_fired: bool, 98 + /// When we last fired a repeat event. 99 + last_repeat: Instant, 100 + /// Buffered events to return. 101 + queue: EventQueue, 102 + } 103 + 104 + impl InputDriver { 105 + /// Create a new input driver from initialized hardware. 106 + pub fn new(hw: InputHw) -> Self { 107 + let now = Instant::now(); 108 + Self { 109 + hw, 110 + stable: None, 111 + candidate: None, 112 + candidate_since: now, 113 + press_since: now, 114 + long_press_fired: false, 115 + last_repeat: now, 116 + queue: EventQueue::new(), 117 + } 118 + } 119 + 120 + /// Poll for the next input event. 121 + /// 122 + /// Call this regularly (e.g., every 10-20ms). Returns `None` when 123 + /// there are no pending events. 124 + pub fn poll(&mut self) -> Option<Event> { 125 + // Drain any buffered events first 126 + if !self.queue.is_empty() { 127 + return self.queue.pop(); 128 + } 129 + 130 + let raw = self.read_raw(); 131 + let now = Instant::now(); 132 + 133 + // Track candidate state for debouncing 134 + if raw != self.candidate { 135 + self.candidate = raw; 136 + self.candidate_since = now; 137 + } 138 + 139 + // Only accept the candidate as stable after debounce period 140 + let debounced = if now - self.candidate_since >= Duration::from_millis(DEBOUNCE_MS) { 141 + self.candidate 142 + } else { 143 + self.stable 144 + }; 145 + 146 + // Handle state transitions 147 + if debounced != self.stable { 148 + if let Some(old) = self.stable { 149 + self.queue.push(Event::Release(old)); 150 + } 151 + if let Some(new) = debounced { 152 + self.queue.push(Event::Press(new)); 153 + self.press_since = now; 154 + self.long_press_fired = false; 155 + self.last_repeat = now; 156 + } 157 + self.stable = debounced; 158 + return self.queue.pop(); 159 + } 160 + 161 + // Handle held button: long-press and repeat 162 + if let Some(btn) = self.stable { 163 + let held = now - self.press_since; 164 + 165 + // Fire long-press once after threshold 166 + if !self.long_press_fired && held >= Duration::from_millis(LONG_PRESS_MS) { 167 + self.long_press_fired = true; 168 + self.last_repeat = now; 169 + return Some(Event::LongPress(btn)); 170 + } 171 + 172 + // Fire repeat events at interval 173 + if self.long_press_fired && (now - self.last_repeat) >= Duration::from_millis(REPEAT_MS) 174 + { 175 + self.last_repeat = now; 176 + return Some(Event::Repeat(btn)); 177 + } 178 + } 179 + 180 + None 181 + } 182 + 183 + /// Read raw button state from hardware (before debouncing). 184 + fn read_raw(&mut self) -> Option<Button> { 185 + // Power button has priority (digital, active low) 186 + if self.hw.power.is_low() { 187 + return Some(Button::Power); 188 + } 189 + 190 + // Read ADC channels 191 + let mv1: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row1)).unwrap(); 192 + let mv2: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row2)).unwrap(); 193 + 194 + // Decode resistance ladder readings 195 + decode_ladder(mv1, ROW1_THRESHOLDS).or_else(|| decode_ladder(mv2, ROW2_THRESHOLDS)) 196 + } 197 + }
+4
src/drivers/mod.rs
··· 1 + 2 + 3 + pub mod input; 4 + pub mod display;
-159
src/input.rs
··· 1 - //! Input event driver 2 - //! 3 - //! The XTEink X4 has three physical input sources that all funnel 4 - //! into a single "one button at a time" model: 5 - //! Because each resistance ladder can only report one press at a 6 - //! time, and the power button is digital, we collapse everything 7 - //! into `Option<Button>` per poll cycle. 8 - //! 9 - 10 - use esp_hal::time::{Duration, Instant}; 11 - 12 - use crate::board::{self, Button, InputHw, ROW1_THRESHOLDS, ROW2_THRESHOLDS}; 13 - 14 - const DEBOUNCE_MS: u64 = 30; 15 - const LONG_PRESS_MS: u64 = 600; 16 - const REPEAT_MS: u64 = 150; 17 - 18 - /// Events are returned one at a time from InputDriver::Poll 19 - #[derive(Debug, Clone, Copy, PartialEq, Eq)] 20 - pub enum Event { 21 - Press(Button), 22 - Release(Button), 23 - LongPress(Button), 24 - Repeat(Button), 25 - } 26 - 27 - struct EventQueue { 28 - buf: [Option<Event>; 2], 29 - read: u8, 30 - } 31 - 32 - impl EventQueue { 33 - const fn new() -> Self { 34 - Self { 35 - buf: [None; 2], 36 - read: 0, 37 - } 38 - } 39 - 40 - fn push(&mut self, ev: Event) { 41 - for slot in self.buf.iter_mut() { 42 - if slot.is_none() { 43 - *slot = Some(ev); 44 - return; 45 - } 46 - } 47 - // NOTE: If both slots are full something is wrong with our logic, 48 - // silently dropping is safer than panic. 49 - } 50 - 51 - fn pop(&mut self) -> Option<Event> { 52 - if (self.read as usize) < self.buf.len() { 53 - let idx = self.read as usize; 54 - if let Some(ev) = self.buf[idx].take() { 55 - self.read += 1; 56 - return Some(ev); 57 - } 58 - } 59 - // Reset for next cycle. 60 - self.read = 0; 61 - None 62 - } 63 - 64 - fn is_empty(&self) -> bool { 65 - self.buf.iter().all(|s| s.is_none()) 66 - } 67 - } 68 - 69 - pub struct InputDriver { 70 - hw: InputHw, 71 - stable: Option<Button>, 72 - candidate: Option<Button>, 73 - candidate_since: Instant, 74 - press_since: Instant, 75 - long_press_fired: bool, 76 - last_repeat: Instant, 77 - queue: EventQueue, 78 - } 79 - 80 - impl InputDriver { 81 - pub fn new(hw: InputHw) -> Self { 82 - let now = Instant::now(); 83 - Self { 84 - hw, 85 - stable: None, 86 - candidate: None, 87 - candidate_since: now, 88 - press_since: now, 89 - long_press_fired: false, 90 - last_repeat: now, 91 - queue: EventQueue::new(), 92 - } 93 - } 94 - 95 - /// Poll for the next input event. 96 - pub fn poll(&mut self) -> Option<Event> { 97 - if !self.queue.is_empty() { 98 - return self.queue.pop(); 99 - } 100 - 101 - let raw = self.read_raw(); 102 - let now = Instant::now(); 103 - 104 - if raw != self.candidate { 105 - self.candidate = raw; 106 - self.candidate_since = now; 107 - } 108 - 109 - let debounced = if now - self.candidate_since >= Duration::from_millis(DEBOUNCE_MS) { 110 - self.candidate 111 - } else { 112 - self.stable 113 - }; 114 - 115 - if debounced != self.stable { 116 - if let Some(old) = self.stable { 117 - self.queue.push(Event::Release(old)); 118 - } 119 - if let Some(new) = debounced { 120 - self.queue.push(Event::Press(new)); 121 - self.press_since = now; 122 - self.long_press_fired = false; 123 - self.last_repeat = now; 124 - } 125 - self.stable = debounced; 126 - return self.queue.pop(); 127 - } 128 - 129 - if let Some(btn) = self.stable { 130 - let held = now - self.press_since; 131 - 132 - if !self.long_press_fired && held >= Duration::from_millis(LONG_PRESS_MS) { 133 - self.long_press_fired = true; 134 - self.last_repeat = now; 135 - return Some(Event::LongPress(btn)); 136 - } 137 - 138 - if self.long_press_fired && (now - self.last_repeat) >= Duration::from_millis(REPEAT_MS) 139 - { 140 - self.last_repeat = now; 141 - return Some(Event::Repeat(btn)); 142 - } 143 - } 144 - 145 - None 146 - } 147 - 148 - fn read_raw(&mut self) -> Option<Button> { 149 - if self.hw.power.is_low() { 150 - return Some(Button::Power); 151 - } 152 - 153 - let mv1: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row1)).unwrap(); 154 - let mv2: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row2)).unwrap(); 155 - 156 - board::decode_ladder(mv1, ROW1_THRESHOLDS) 157 - .or_else(|| board::decode_ladder(mv2, ROW2_THRESHOLDS)) 158 - } 159 - }
+4 -3
src/lib.rs
··· 1 1 #![no_std] 2 2 3 - pub mod board; 4 - pub mod input; 5 - pub mod display; 3 + // pub mod input; 4 + pub mod board; 5 + pub mod drivers; 6 + // pub mod display;