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.

update with kernel changes

hansmrtn 9082a61e ba3751c4

+243 -114
+1 -1
Cargo.toml
··· 9 9 path = "./src/bin/main.rs" 10 10 11 11 [dependencies] 12 - esp-hal = { version = "~1.0", features = ["esp32c3", "log-04", "unstable"] } 12 + esp-hal = { version = "1.0.0", features = ["esp32c3", "log-04", "unstable"] } 13 13 14 14 esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32c3", "log-04"] } 15 15 log = "0.4.27"
+199 -31
src/bin/main.rs
··· 4 4 use esp_backtrace as _; 5 5 use esp_hal::clock::CpuClock; 6 6 use esp_hal::delay::Delay; 7 + use esp_hal::interrupt::Priority; 8 + use esp_hal::time::Duration; 9 + use esp_hal::timer::timg::TimerGroup; 10 + use esp_hal::timer::PeriodicTimer; 7 11 use log::info; 8 12 13 + use core::cell::RefCell; 14 + use critical_section::Mutex; 15 + 9 16 use embedded_graphics::mono_font::ascii::FONT_10X20; 10 17 11 18 use pulp_os::board::Board; 12 - use pulp_os::drivers::input::{InputDriver, Event}; 13 - use pulp_os::ui::{Region, Widget, Label, Button}; 19 + use pulp_os::board::button::Button as HwButton; 20 + use pulp_os::drivers::input::{Event, InputDriver}; 21 + use pulp_os::kernel::{AdaptivePoller, Job, Scheduler}; 22 + use pulp_os::kernel::wake::{signal_timer, try_wake, WakeReason}; 23 + use pulp_os::ui::{Button, Label, Region, Widget}; 14 24 15 25 extern crate alloc; 16 26 17 27 esp_bootloader_esp_idf::esp_app_desc!(); 18 28 19 - // 8px aligned 29 + // timer interrupt setup 30 + static TIMER0: Mutex<RefCell<Option<PeriodicTimer<'static, esp_hal::Blocking>>>> = 31 + Mutex::new(RefCell::new(None)); 32 + 33 + #[esp_hal::handler(priority = Priority::Priority1)] 34 + fn timer0_handler() { 35 + critical_section::with(|cs| { 36 + if let Some(timer) = TIMER0.borrow_ref_mut(cs).as_mut() { 37 + timer.clear_interrupt(); 38 + } 39 + }); 40 + signal_timer(); 41 + } 42 + 43 + // test ui 20 44 const TITLE: Region = Region::new(16, 16, 200, 32); 21 - const BTN: Region = Region::new(16, 80, 120, 48); 22 - const STATUS: Region = Region::new(16, 160, 200, 32); 45 + const ITEM0: Region = Region::new(16, 80, 200, 48); 46 + const ITEM1: Region = Region::new(16, 144, 200, 48); 47 + const STATUS: Region = Region::new(16, 220, 300, 32); 23 48 24 49 #[esp_hal::main] 25 50 fn main() -> ! { 26 51 esp_println::logger::init_logger_from_env(); 27 52 let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); 28 53 let peripherals = esp_hal::init(config); 29 - esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 66320); 54 + esp_alloc::heap_allocator!(size: 66320); 30 55 31 56 info!("booting..."); 32 57 58 + // timer init 10ms tick 59 + let timg0 = TimerGroup::new(unsafe { peripherals.TIMG0.clone_unchecked() }); 60 + let mut timer0 = PeriodicTimer::new(timg0.timer0); 61 + 62 + critical_section::with(|cs| { 63 + timer0.set_interrupt_handler(timer0_handler); 64 + timer0.start(Duration::from_millis(10)).unwrap(); 65 + TIMER0.borrow_ref_mut(cs).replace(timer0); 66 + }); 67 + 68 + info!("timer initialized."); 69 + 70 + // hardware init 33 71 let mut board = Board::init(peripherals); 34 72 let mut delay = Delay::new(); 35 73 36 74 board.display.epd.init(&mut delay); 37 75 board.display.epd.fill_white(); 76 + info!("hardware initialized."); 38 77 39 - // test widgets 78 + // widgets 40 79 let title = Label::new(TITLE, "pulp-os", &FONT_10X20); 41 - let mut btn = Button::new(BTN, "Press", &FONT_10X20); 80 + let mut item0 = Button::new(ITEM0, "Item 0", &FONT_10X20); 81 + let mut item1 = Button::new(ITEM1, "Item 1", &FONT_10X20); 42 82 let mut status = Label::new(STATUS, "Ready", &FONT_10X20); 43 83 44 - // init draw 84 + let mut selected: usize = 0; 85 + item0.set_pressed(true); 86 + 45 87 title.draw(&mut board.display.epd).unwrap(); 46 - btn.draw(&mut board.display.epd).unwrap(); 88 + item0.draw(&mut board.display.epd).unwrap(); 89 + item1.draw(&mut board.display.epd).unwrap(); 47 90 status.draw(&mut board.display.epd).unwrap(); 48 91 board.display.epd.refresh_full(&mut delay); 49 92 50 - info!("UI ready"); 93 + info!("ui ready."); 51 94 95 + let mut scheduler = Scheduler::new(); 96 + let mut poller = AdaptivePoller::new(); 52 97 let mut input = InputDriver::new(board.input); 53 98 54 - loop { 55 - if let Some(event) = input.poll() { 56 - match event { 57 - Event::Press(button) => { 58 - info!("[BTN] Press: {}", button.name()); 99 + info!("kernel ready."); 59 100 60 - btn.set_pressed(true); 61 - btn.draw(&mut board.display.epd).unwrap(); 62 - let r = btn.refresh_bounds(); 63 - board.display.epd.refresh_partial(r.x, r.y, r.w, r.h, &mut delay); 101 + let mut tick_count: u32 = 0; 64 102 65 - status.set_text(button.name()); 66 - status.draw(&mut board.display.epd).unwrap(); 67 - let r = status.refresh_bounds(); 68 - board.display.epd.refresh_partial(r.x, r.y, r.w, r.h, &mut delay); 103 + loop { 104 + while let Some(job) = scheduler.pop() { 105 + match job { 106 + Job::RenderPage => { 107 + info!("Job: RenderPage"); 108 + } 109 + Job::PrefetchNext => { 110 + info!("Job: PrefetchNext"); 111 + } 112 + Job::PrefetchPrev => { 113 + info!("Job: PrefetchPrev"); 69 114 } 70 - Event::Release(_button) => { 71 - btn.set_pressed(false); 72 - btn.draw(&mut board.display.epd).unwrap(); 73 - let r = btn.refresh_bounds(); 74 - board.display.epd.refresh_partial(r.x, r.y, r.w, r.h, &mut delay); 115 + Job::LayoutChapter { chapter } => { 116 + info!("Job: LayoutChapter {}", chapter); 75 117 } 76 - _ => {} // Ignore LongPress, Repeat 118 + Job::CacheChapter { chapter } => { 119 + info!("Job: CacheChapter {}", chapter); 120 + } 121 + Job::HandleInput => {} 77 122 } 78 123 } 79 124 80 - delay.delay_millis(10); 125 + let timer_wake = match try_wake() { 126 + Some(WakeReason::Timer) | Some(WakeReason::Multiple) => true, 127 + Some(WakeReason::Button) => { 128 + poller.on_activity(); 129 + true 130 + } 131 + Some(WakeReason::Display) => { 132 + info!("Display ready"); 133 + false 134 + } 135 + None => false, 136 + }; 137 + 138 + tick_count += 1; 139 + let should_poll = timer_wake || (tick_count >= 10); 140 + 141 + if should_poll { 142 + tick_count = 0; 143 + 144 + if poller.tick() { 145 + if let Some(event) = input.poll() { 146 + poller.on_activity(); 147 + 148 + // Handle input and only act on Press for navigation 149 + // LongPress/Repeat only for special actions 150 + match event { 151 + Event::Press(button) => { 152 + info!("[BTN] Press: {}", button.name()); 153 + 154 + match button { 155 + HwButton::Right | HwButton::VolUp => { 156 + let old = selected; 157 + selected = (selected + 1) % 2; 158 + if old != selected { 159 + update_selection( 160 + selected, 161 + &mut item0, 162 + &mut item1, 163 + &mut board.display.epd, 164 + &mut delay, 165 + ); 166 + } 167 + scheduler.push_or_drop(Job::PrefetchNext); 168 + } 169 + HwButton::Left | HwButton::VolDown => { 170 + let old = selected; 171 + selected = if selected == 0 { 1 } else { 0 }; 172 + if old != selected { 173 + update_selection( 174 + selected, 175 + &mut item0, 176 + &mut item1, 177 + &mut board.display.epd, 178 + &mut delay, 179 + ); 180 + } 181 + scheduler.push_or_drop(Job::PrefetchPrev); 182 + } 183 + HwButton::Confirm => { 184 + let msg = if selected == 0 { 185 + "Selected: Item 0" 186 + } else { 187 + "Selected: Item 1" 188 + }; 189 + status.set_text(msg); 190 + status.draw(&mut board.display.epd).unwrap(); 191 + let r = status.refresh_bounds(); 192 + board.display.epd.refresh_partial(r.x, r.y, r.w, r.h, &mut delay); 193 + scheduler.push_or_drop(Job::RenderPage); 194 + } 195 + HwButton::Power => { 196 + // TODO: do smth but just log for now 197 + info!("Power pressed"); 198 + } 199 + _ => {} 200 + } 201 + } 202 + Event::Release(button) => { 203 + info!("[BTN] Release: {}", button.name()); 204 + } 205 + Event::LongPress(button) => { 206 + info!("[BTN] LongPress: {}", button.name()); 207 + // TODO: could use for special actions like shutdown 208 + if button == HwButton::Power { 209 + status.set_text("Shutting down..."); 210 + status.draw(&mut board.display.epd).unwrap(); 211 + let r = status.refresh_bounds(); 212 + board.display.epd.refresh_partial(r.x, r.y, r.w, r.h, &mut delay); 213 + } 214 + } 215 + Event::Repeat(button) => { 216 + // TODO: figure it out 217 + info!("[BTN] Repeat: {}", button.name()); 218 + } 219 + } 220 + } else { 221 + poller.on_idle(); 222 + } 223 + } 224 + } 225 + 226 + delay.delay_millis(1); 81 227 } 82 228 } 229 + 230 + use pulp_os::board::Epd; 231 + 232 + fn update_selection( 233 + selected: usize, 234 + item0: &mut Button, 235 + item1: &mut Button, 236 + display: &mut Epd, 237 + delay: &mut Delay, 238 + ) { 239 + item0.set_pressed(selected == 0); 240 + item1.set_pressed(selected == 1); 241 + 242 + item0.draw(display).unwrap(); 243 + item1.draw(display).unwrap(); 244 + 245 + // Single refresh for both items 246 + let r = Region::new(16, 80, 200, 112).align8(); 247 + display.refresh_partial(r.x, r.y, r.w, r.h, delay); 248 + 249 + info!("Selected: Item {}", selected); 250 + }
+24 -34
src/board/display.rs
··· 3 3 //! Based on GxEPD2_426_GDEQ0426T82.cpp by Jean-Marc Zingg 4 4 //! <https://github.com/ZinggJM/GxEPD2> 5 5 use embedded_graphics_core::{ 6 + Pixel, 6 7 draw_target::DrawTarget, 7 8 geometry::{OriginDimensions, Size}, 8 9 pixelcolor::BinaryColor, 9 - Pixel, 10 10 }; 11 11 use embedded_hal::digital::{InputPin, OutputPin}; 12 12 use embedded_hal::spi::SpiDevice; ··· 17 17 pub const HEIGHT: u16 = 480; 18 18 pub const FRAMEBUFFER_SIZE: usize = (WIDTH as usize * HEIGHT as usize) / 8; 19 19 20 - // SPI frequency (10MHz as per GxEPD2) 21 - pub const SPI_FREQ_MHZ: u32 = 10; 20 + // SPI frequency 21 + pub const SPI_FREQ_MHZ: u32 = 20; 22 22 23 23 // Timing constants from GxEPD2 24 24 #[allow(dead_code)] ··· 53 53 pub const MASTER_ACTIVATION: u8 = 0x20; 54 54 pub const DISPLAY_UPDATE_CONTROL_1: u8 = 0x21; 55 55 pub const DISPLAY_UPDATE_CONTROL_2: u8 = 0x22; 56 - pub const WRITE_RAM_BW: u8 = 0x24; // Current/New buffer 57 - pub const WRITE_RAM_RED: u8 = 0x26; // Previous buffer (for differential) 56 + pub const WRITE_RAM_BW: u8 = 0x24; // Current/New buffer 57 + pub const WRITE_RAM_RED: u8 = 0x26; // Previous buffer (for differential) 58 58 pub const BORDER_WAVEFORM: u8 = 0x3C; 59 59 pub const SET_RAM_X_RANGE: u8 = 0x44; 60 60 pub const SET_RAM_Y_RANGE: u8 = 0x45; ··· 196 196 // Write to both buffers for full refresh 197 197 self.set_partial_ram_area(0, 0, WIDTH, HEIGHT); 198 198 self.write_full_buffer(cmd::WRITE_RAM_RED); // Previous 199 - 199 + 200 200 delay.delay_millis(1); // Yield between large transfers 201 - 201 + 202 202 self.set_partial_ram_area(0, 0, WIDTH, HEIGHT); 203 - self.write_full_buffer(cmd::WRITE_RAM_BW); // Current 203 + self.write_full_buffer(cmd::WRITE_RAM_BW); // Current 204 204 205 205 self.update_full(delay); 206 206 self.initial_refresh = false; 207 207 self.initial_write = false; 208 208 } 209 209 210 - /// Partial screen refresh (fast, for small updates) 211 - /// Takes LOGICAL coordinates (affected by rotation) 212 - /// Matches GxEPD2's drawImage() sequence exactly: 213 - /// 1. writeImage to 0x24 (current buffer only) 214 - /// 2. refresh (partial update compares 0x24 vs 0x26) 215 - /// 3. writeImageAgain to BOTH 0x26 and 0x24 (sync buffers for next update) 210 + /// Partial screen refresh 211 + /// Takes LOGICAL coordinates 216 212 pub fn refresh_partial(&mut self, x: u16, y: u16, w: u16, h: u16, delay: &mut Delay) { 217 213 // Initial refresh must be full 218 214 if self.initial_refresh { ··· 267 263 // But we need the physical top-left of the region 268 264 (WIDTH - y - h, x, h, w) 269 265 } 270 - Rotation::Deg180 => { 271 - (WIDTH - x - w, HEIGHT - y - h, w, h) 272 - } 273 - Rotation::Deg270 => { 274 - (y, HEIGHT - x - w, h, w) 275 - } 266 + Rotation::Deg180 => (WIDTH - x - w, HEIGHT - y - h, w, h), 267 + Rotation::Deg270 => (y, HEIGHT - x - w, h, w), 276 268 } 277 269 } 278 270 ··· 318 310 // Driver output control 319 311 self.send_command(cmd::DRIVER_OUTPUT_CONTROL); 320 312 self.send_data(&[ 321 - ((HEIGHT - 1) & 0xFF) as u8, // A[7:0] 322 - ((HEIGHT - 1) >> 8) as u8, // A[9:8] 323 - 0x02, // SM = interlaced 313 + ((HEIGHT - 1) & 0xFF) as u8, // A[7:0] 314 + ((HEIGHT - 1) >> 8) as u8, // A[9:8] 315 + 0x02, // SM = interlaced 324 316 ]); 325 317 326 318 // Border waveform ··· 404 396 // Write in normal row order - gate reversal is handled by RAM address setup 405 397 // (Y-decrease mode in _setPartialRamArea), NOT by reversing rows here 406 398 let bytes_per_row = (WIDTH / 8) as usize; 407 - 399 + 408 400 // Use a temporary buffer to avoid borrow checker issues 409 401 let mut row_buf = [0u8; 256]; 410 - 402 + 411 403 for row in 0..HEIGHT as usize { 412 404 let start = row * bytes_per_row; 413 405 // Write row in chunks to avoid issues 414 406 for chunk_start in (0..bytes_per_row).step_by(256) { 415 407 let chunk_end = (chunk_start + 256).min(bytes_per_row); 416 408 let chunk_len = chunk_end - chunk_start; 417 - 409 + 418 410 // Copy to temp buffer 419 - row_buf[..chunk_len].copy_from_slice( 420 - &self.framebuffer[start + chunk_start..start + chunk_end] 421 - ); 411 + row_buf[..chunk_len] 412 + .copy_from_slice(&self.framebuffer[start + chunk_start..start + chunk_end]); 422 413 self.send_data(&row_buf[..chunk_len]); 423 414 } 424 415 } ··· 442 433 for row in 0..h as usize { 443 434 let src_row = y as usize + row; 444 435 let src_start = src_row * bytes_per_row + x_byte; 445 - 436 + 446 437 // Copy to temp buffer 447 - row_buf[..window_bytes].copy_from_slice( 448 - &self.framebuffer[src_start..src_start + window_bytes] 449 - ); 438 + row_buf[..window_bytes] 439 + .copy_from_slice(&self.framebuffer[src_start..src_start + window_bytes]); 450 440 self.send_data(&row_buf[..window_bytes]); 451 441 } 452 442 } ··· 504 494 } 505 495 } 506 496 507 - // embedded-graphics integration 497 + // embedded-graphics integration 508 498 509 499 impl<SPI, DC, RST, BUSY, E> OriginDimensions for DisplayDriver<SPI, DC, RST, BUSY> 510 500 where
+17 -47
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. 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" deal: 5 + // - Row 1 ADC (GPIO1): Right, Left, Confirm, Back via resistance ladder 6 + // - Row 2 ADC (GPIO2): Volume Up/Down via resistance ladder 7 + // - Power button (GPIO3): Digital input, active low 8 + // NOTE: Because each resistance ladder can only report one press at a time, 9 + // we collapse everything into `Option<Button>` per poll cycle. 12 10 13 11 use esp_hal::time::{Duration, Instant}; 14 12 15 13 use crate::board::InputHw; 16 14 use crate::board::button::{Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS, decode_ladder}; 17 15 18 - /// Debounce time - ignore state changes shorter than this. 19 16 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. 17 + const LONG_PRESS_MS: u64 = 1000; 25 18 const REPEAT_MS: u64 = 150; 26 19 27 - /// Input events returned from [`InputDriver::poll`]. 28 20 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 29 21 pub enum Event { 30 - /// Button was just pressed. 31 22 Press(Button), 32 - /// Button was just released. 33 23 Release(Button), 34 - /// Button held long enough to trigger long-press. 35 24 LongPress(Button), 36 - /// Button still held, firing repeat event. 37 25 Repeat(Button), 38 26 } 39 27 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. 28 + // Small fixed-size event queue for buffering multiple events per poll. 43 29 struct EventQueue { 44 30 buf: [Option<Event>; 2], 45 31 read: u8, ··· 61 47 } 62 48 } 63 49 // If both slots are full, something is wrong with our logic. 64 - // Silently dropping is safer than panic in embedded. 65 50 } 66 51 67 52 fn pop(&mut self) -> Option<Event> { ··· 82 67 } 83 68 } 84 69 85 - /// Stateful input driver with debouncing, long-press, and repeat support. 70 + // debounce, long-press, and repeat support. 86 71 pub struct InputDriver { 87 72 hw: InputHw, 88 - /// Currently stable (debounced) button state. 89 73 stable: Option<Button>, 90 - /// Candidate state during debounce window. 91 74 candidate: Option<Button>, 92 - /// When the candidate state was first seen. 93 75 candidate_since: Instant, 94 - /// When the current stable button was first pressed. 95 76 press_since: Instant, 96 - /// Whether we've already fired a long-press for the current hold. 97 77 long_press_fired: bool, 98 - /// When we last fired a repeat event. 99 78 last_repeat: Instant, 100 - /// Buffered events to return. 101 79 queue: EventQueue, 102 80 } 103 81 104 82 impl InputDriver { 105 - /// Create a new input driver from initialized hardware. 106 83 pub fn new(hw: InputHw) -> Self { 107 84 let now = Instant::now(); 108 85 Self { ··· 117 94 } 118 95 } 119 96 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. 97 + // poll for the next input event. 124 98 pub fn poll(&mut self) -> Option<Event> { 125 - // Drain any buffered events first 99 + // drain any buffd events first 126 100 if !self.queue.is_empty() { 127 101 return self.queue.pop(); 128 102 } ··· 130 104 let raw = self.read_raw(); 131 105 let now = Instant::now(); 132 106 133 - // Track candidate state for debouncing 134 107 if raw != self.candidate { 135 108 self.candidate = raw; 136 109 self.candidate_since = now; 137 110 } 138 111 139 - // Only accept the candidate as stable after debounce period 140 112 let debounced = if now - self.candidate_since >= Duration::from_millis(DEBOUNCE_MS) { 141 113 self.candidate 142 114 } else { 143 115 self.stable 144 116 }; 145 117 146 - // Handle state transitions 118 + // normal press 147 119 if debounced != self.stable { 148 120 if let Some(old) = self.stable { 149 121 self.queue.push(Event::Release(old)); ··· 158 130 return self.queue.pop(); 159 131 } 160 132 161 - // Handle held button: long-press and repeat 133 + // long press and repeat 162 134 if let Some(btn) = self.stable { 163 135 let held = now - self.press_since; 164 136 165 - // Fire long-press once after threshold 166 137 if !self.long_press_fired && held >= Duration::from_millis(LONG_PRESS_MS) { 167 138 self.long_press_fired = true; 168 139 self.last_repeat = now; ··· 187 158 return Some(Button::Power); 188 159 } 189 160 190 - // Read ADC channels 161 + // read adc channels & decode 191 162 let mv1: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row1)).unwrap(); 192 163 let mv2: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row2)).unwrap(); 193 164 194 - // Decode resistance ladder readings 195 165 decode_ladder(mv1, ROW1_THRESHOLDS).or_else(|| decode_ladder(mv2, ROW2_THRESHOLDS)) 196 166 } 197 167 }
+2 -1
src/lib.rs
··· 2 2 3 3 pub mod board; 4 4 pub mod drivers; 5 - pub mod ui; 5 + pub mod kernel; 6 + pub mod ui;