···11// debounced input from ADC ladders and power button
22// one button at a time (ladder hw limitation)
33-// 15 ms debounce, 1 s long press, 150 ms repeat
44-// ADC reads oversampled 4x to reject noise (~40 us per channel)
33+// ADC reads oversampled to reject noise (~40 us per channel)
5465use esp_hal::time::{Duration, Instant};
7677+use crate::board::button::{decode_ladder, Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS};
88use crate::board::InputHw;
99-use crate::board::button::{Button, ROW1_THRESHOLDS, ROW2_THRESHOLDS, decode_ladder};
99+use crate::kernel::timing;
10101111macro_rules! read_averaged {
1212 ($adc:expr, $pin:expr) => {{
1313 let mut sum: u32 = 0;
1414- for _ in 0..ADC_OVERSAMPLE {
1414+ for _ in 0..timing::ADC_OVERSAMPLE {
1515 sum += nb::block!($adc.read_oneshot($pin)).unwrap() as u32;
1616 }
1717- (sum / ADC_OVERSAMPLE) as u16
1717+ (sum / timing::ADC_OVERSAMPLE) as u16
1818 }};
1919}
2020-2121-const DEBOUNCE_MS: u64 = 15;
2222-const LONG_PRESS_MS: u64 = 1000;
2323-const REPEAT_MS: u64 = 150;
2424-const ADC_OVERSAMPLE: u32 = 4;
25202621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2722pub enum Event {
···115110 self.candidate_since = now;
116111 }
117112118118- let debounced = if now - self.candidate_since >= Duration::from_millis(DEBOUNCE_MS) {
113113+ let debounced = if now - self.candidate_since >= Duration::from_millis(timing::DEBOUNCE_MS)
114114+ {
119115 self.candidate
120116 } else {
121117 self.stable
···140136 if !self.hold_consumed {
141137 let held = now - self.press_since;
142138143143- if !self.long_press_fired && held >= Duration::from_millis(LONG_PRESS_MS) {
139139+ if !self.long_press_fired && held >= Duration::from_millis(timing::LONG_PRESS_MS) {
144140 self.long_press_fired = true;
145141 self.last_repeat = now;
146142 return Some(Event::LongPress(btn));
147143 }
148144149145 if self.long_press_fired
150150- && (now - self.last_repeat) >= Duration::from_millis(REPEAT_MS)
146146+ && (now - self.last_repeat) >= Duration::from_millis(timing::REPEAT_MS)
151147 {
152148 self.last_repeat = now;
153149 return Some(Event::Repeat(btn));
+4-2
kernel/src/kernel/app.rs
···186186 self.coalesce_until = None;
187187 }
188188189189- // mark dirty with 50ms coalescing window; use only for background
189189+ // mark dirty with coalescing window; use only for background
190190 // batch updates (title scanner) where many rapid dirty marks
191191 // should coalesce into a single refresh
192192 #[inline]
193193 pub fn mark_dirty_coalesced(&mut self, region: Region) {
194194+ use super::timing;
194195 self.request_partial_redraw(region);
195196 if !self.immediate && self.coalesce_until.is_none() {
196196- self.coalesce_until = Some(Instant::now() + embassy_time::Duration::from_millis(50));
197197+ self.coalesce_until =
198198+ Some(Instant::now() + embassy_time::Duration::from_millis(timing::COALESCE_WINDOW_MS));
197199 }
198200 }
199201
···1414pub mod handle;
1515pub mod scheduler;
1616pub mod tasks;
1717+pub mod timing;
1718pub mod wake;
1819pub mod work_queue;
1920
+3-3
kernel/src/kernel/scheduler.rs
···27272828use crate::ui::{free_stack_bytes, stack_high_water_mark};
29293030-const TICK_MS: u64 = 10;
3030+use super::timing;
31313232impl super::Kernel {
3333 // render boot console to EPD; call before boot() to show
···7171 // 2. EPD busy pin wait inside render()
7272 // everything between them is synchronous function calls
7373 pub async fn run<A: AppLayer>(&mut self, app_mgr: &mut A) -> ! {
7474- let mut work_ticker = Ticker::every(Duration::from_millis(TICK_MS));
7474+ let mut work_ticker = Ticker::every(Duration::from_millis(timing::TICK_MS));
75757676 loop {
7777 if app_mgr.needs_special_mode() {
···349349 match select(
350350 app_mgr.run_background(&mut handle),
351351 with_timeout(
352352- Duration::from_millis(TICK_MS),
352352+ Duration::from_millis(timing::TICK_MS),
353353 tasks::INPUT_EVENTS.receive(),
354354 ),
355355 )
+9-9
kernel/src/kernel/tasks.rs
···88use crate::drivers::battery;
99use crate::drivers::input::{Event, InputDriver};
10101111+use super::timing;
1212+1113pub const INPUT_CHANNEL_CAP: usize = 8;
1214pub static INPUT_EVENTS: Channel<CriticalSectionRawMutex, Event, INPUT_CHANNEL_CAP> =
1315 Channel::new();
···22242325pub static BATTERY_MV: Signal<CriticalSectionRawMutex, u16> = Signal::new();
24262525-const BATTERY_INTERVAL_TICKS: u32 = 3000; // 3000 x 10 ms = 30 s
2626-2727#[embassy_executor::task]
2828pub async fn input_task(mut input: InputDriver) -> ! {
2929- let mut ticker = Ticker::every(Duration::from_millis(10));
2929+ let mut ticker = Ticker::every(Duration::from_millis(timing::INPUT_TICK_MS));
3030 let mut battery_counter: u32 = 0;
31313232 let raw = input.read_battery_mv();
···4545 }
46464747 battery_counter += 1;
4848- if battery_counter >= BATTERY_INTERVAL_TICKS {
4848+ if battery_counter >= timing::BATTERY_INTERVAL_TICKS {
4949 battery_counter = 0;
5050 let raw = input.read_battery_mv();
5151 BATTERY_MV.signal(battery::adc_to_battery_mv(raw));
···59596060#[embassy_executor::task]
6161pub async fn housekeeping_task() -> ! {
6262- Timer::after(Duration::from_secs(5)).await;
6262+ Timer::after(Duration::from_secs(timing::HOUSEKEEPING_INITIAL_DELAY_SECS)).await;
63636464- let mut status_ticker = Ticker::every(Duration::from_secs(5));
6565- let mut sd_ticker = Ticker::every(Duration::from_secs(30));
6464+ let mut status_ticker = Ticker::every(Duration::from_secs(timing::STATUS_INTERVAL_SECS));
6565+ let mut sd_ticker = Ticker::every(Duration::from_secs(timing::SD_CHECK_INTERVAL_SECS));
66666767- Timer::after(Duration::from_secs(2)).await; // stagger behind SD
6868- let mut bm_ticker = Ticker::every(Duration::from_secs(30));
6767+ Timer::after(Duration::from_secs(timing::BOOKMARK_FLUSH_STAGGER_SECS)).await;
6868+ let mut bm_ticker = Ticker::every(Duration::from_secs(timing::BOOKMARK_FLUSH_INTERVAL_SECS));
69697070 loop {
7171 use embassy_futures::select::{Either3, select3};
+53
kernel/src/kernel/timing.rs
···11+// timing constants for the kernel scheduler and tasks
22+//
33+// timing values that control polling intervals, debouncing,
44+// coalescing, and housekeeping. some of these may
55+// become runtime-configurable settings in the future, idk
66+77+// main scheduler tick interval (ms)
88+// controls how often the event loop wakes to check for work
99+pub const TICK_MS: u64 = 10;
1010+1111+// input task poll interval (ms)
1212+// ADC button sampling rate; should probably(?) match or equal TICK_MS
1313+pub const INPUT_TICK_MS: u64 = 10;
1414+1515+// button debounce window (ms)
1616+// raw input must be stable for this duration before registering
1717+pub const DEBOUNCE_MS: u64 = 15;
1818+1919+// long-press detection threshold (ms)
2020+// holding a button for this duration generates a LongPress event
2121+pub const LONG_PRESS_MS: u64 = 1000;
2222+2323+// key repeat interval (ms)
2424+// after long-press, generates Repeat events at this rate
2525+pub const REPEAT_MS: u64 = 150;
2626+2727+// ADC oversampling count
2828+pub const ADC_OVERSAMPLE: u32 = 4;
2929+3030+// status log interval (s)
3131+pub const STATUS_INTERVAL_SECS: u64 = 5;
3232+3333+// SD card presence check interval (s)
3434+pub const SD_CHECK_INTERVAL_SECS: u64 = 30;
3535+3636+// bookmark flush interval (seconds)
3737+pub const BOOKMARK_FLUSH_INTERVAL_SECS: u64 = 30;
3838+3939+// dookmark flush stagger delay (seconds)
4040+// dffset from SD check to avoid simultaneous SD operations
4141+pub const BOOKMARK_FLUSH_STAGGER_SECS: u64 = 2;
4242+4343+// initial housekeeping delay (seconds)
4444+// delay before first housekeeping cycle to let boot settle
4545+pub const HOUSEKEEPING_INITIAL_DELAY_SECS: u64 = 5;
4646+4747+// coalesce window for batch dirty marks (ms)
4848+// batch multiple rapid dirty marks into a single refresh
4949+pub const COALESCE_WINDOW_MS: u64 = 50;
5050+5151+// battery read interval in input task ticks
5252+// ticks * 10 ms = seconds between battery reads
5353+pub const BATTERY_INTERVAL_TICKS: u32 = 3000;
+93
kernel/src/ui/layout.rs
···11+// common layout constants for UI rendering
22+//
33+// centralizes magic layout values used across apps and widgets.
44+// some of these may become runtime-configurable settings in the
55+// future (e.g., margin sizes based on font size preferences).
66+77+use super::statusbar::BAR_HEIGHT;
88+use crate::board::{SCREEN_H, SCREEN_W};
99+1010+// ── Content area ────────────────────────────────────────────────
1111+1212+/// Top of the content area (below status bar).
1313+pub const CONTENT_TOP: u16 = BAR_HEIGHT;
1414+1515+/// Height of the content area (screen minus status bar).
1616+pub const CONTENT_H: u16 = SCREEN_H - BAR_HEIGHT;
1717+1818+// ── Standard spacing ────────────────────────────────────────────
1919+2020+/// Standard margin for content edges (left/right).
2121+pub const STANDARD_MARGIN: u16 = 8;
2222+2323+/// Large margin for content edges (used in some UIs).
2424+pub const LARGE_MARGIN: u16 = 16;
2525+2626+/// Standard gap between items.
2727+pub const STANDARD_GAP: u16 = 4;
2828+2929+/// Larger gap between sections or after headers.
3030+pub const SECTION_GAP: u16 = 8;
3131+3232+// ── Title/header layout ─────────────────────────────────────────
3333+3434+/// Y offset for titles below CONTENT_TOP.
3535+pub const TITLE_Y_OFFSET: u16 = 4;
3636+3737+/// Standard title Y position.
3838+pub const TITLE_Y: u16 = CONTENT_TOP + TITLE_Y_OFFSET;
3939+4040+/// Full-width for content spanning most of the screen.
4141+/// Used for headers and wide content areas.
4242+pub const FULL_CONTENT_W: u16 = SCREEN_W - 2 * LARGE_MARGIN; // 448
4343+4444+/// Width for header/title regions (leaves room for status on right).
4545+pub const HEADER_W: u16 = 300;
4646+4747+/// Width for status regions (battery, page number, etc.).
4848+pub const STATUS_W: u16 = 144;
4949+5050+/// X position for right-aligned status in header.
5151+pub const STATUS_X: u16 = SCREEN_W - LARGE_MARGIN - STATUS_W; // 320
5252+5353+// ── List/menu layout ────────────────────────────────────────────
5454+5555+/// Standard row height for list items.
5656+pub const LIST_ROW_H: u16 = 52;
5757+5858+/// Gap between list rows.
5959+pub const LIST_ROW_GAP: u16 = 4;
6060+6161+/// Combined row stride (row + gap).
6262+pub const LIST_ROW_STRIDE: u16 = LIST_ROW_H + LIST_ROW_GAP;
6363+6464+/// Menu/settings row height (slightly smaller than list).
6565+pub const MENU_ROW_H: u16 = 40;
6666+6767+/// Gap between menu rows.
6868+pub const MENU_ROW_GAP: u16 = 6;
6969+7070+/// Combined menu row stride.
7171+pub const MENU_ROW_STRIDE: u16 = MENU_ROW_H + MENU_ROW_GAP;
7272+7373+// ── Progress and overlays ───────────────────────────────────────
7474+7575+/// Height of progress bars.
7676+pub const PROGRESS_H: u16 = 2;
7777+7878+/// Position overlay dimensions (for page/chapter display).
7979+pub const POSITION_OVERLAY_W: u16 = 280;
8080+pub const POSITION_OVERLAY_H: u16 = 40;
8181+8282+// ── Loading indicator ───────────────────────────────────────────
8383+8484+/// Default height for loading indicator region.
8585+pub const LOADING_H: u16 = 24;
8686+8787+// ── Footer layout ───────────────────────────────────────────────
8888+8989+/// Standard footer height from bottom.
9090+pub const FOOTER_MARGIN: u16 = 60;
9191+9292+/// Footer Y position.
9393+pub const FOOTER_Y: u16 = SCREEN_H - FOOTER_MARGIN;
+9-4
kernel/src/ui/mod.rs
···44// font-dependent widgets (BitmapLabel, QuickMenu, ButtonFeedback)
55// live in the distro's apps::widgets module
6677+pub mod layout;
78pub mod stack_fmt;
89pub mod statusbar;
910mod widget;
10111111-pub use stack_fmt::{StackFmt, stack_fmt};
1212-pub use statusbar::{
1313- BAR_HEIGHT, CONTENT_TOP, free_stack_bytes, paint_stack, stack_high_water_mark,
1212+pub use layout::{
1313+ CONTENT_H, CONTENT_TOP, FOOTER_MARGIN, FOOTER_Y, FULL_CONTENT_W, HEADER_W, LARGE_MARGIN,
1414+ LIST_ROW_GAP, LIST_ROW_H, LIST_ROW_STRIDE, LOADING_H, MENU_ROW_GAP, MENU_ROW_H,
1515+ MENU_ROW_STRIDE, POSITION_OVERLAY_H, POSITION_OVERLAY_W, PROGRESS_H, SECTION_GAP, STANDARD_GAP,
1616+ STANDARD_MARGIN, STATUS_W, STATUS_X, TITLE_Y, TITLE_Y_OFFSET,
1417};
1818+pub use stack_fmt::{stack_fmt, StackFmt};
1919+pub use statusbar::{free_stack_bytes, paint_stack, stack_high_water_mark, BAR_HEIGHT};
1520pub use widget::{
1616- Alignment, Region, draw_loading_indicator, draw_progress_bar, wrap_next, wrap_prev,
2121+ draw_loading_indicator, draw_progress_bar, wrap_next, wrap_prev, Alignment, Region,
1722};
18231924pub use crate::board::{SCREEN_H, SCREEN_W};
+5-9
kernel/src/ui/statusbar.rs
···22// system stats are emitted via log::info! in the scheduler
3344pub const BAR_HEIGHT: u16 = 4;
55-pub const CONTENT_TOP: u16 = BAR_HEIGHT;
6576const STACK_PAINT_WORD: u32 = 0xDEAD_BEEF;
8799-// paint the unused stack with a sentinel word so stack_high_water_mark
1010-// can later measure peak usage; call very early in boot
88+const STACK_GUARD_SKIP: usize = 256;
99+1110pub fn paint_stack() {
1211 #[cfg(target_arch = "riscv32")]
1312 {
···2120 }
2221 let bottom = (&raw const _stack_end_cpu0) as usize;
23222424- let guard_skip = 256;
2525- let paint_bottom = bottom + guard_skip;
2626-2727- let paint_top = sp.saturating_sub(256);
2323+ let paint_bottom = bottom + STACK_GUARD_SKIP;
2424+ let paint_top = sp.saturating_sub(STACK_GUARD_SKIP);
28252926 if paint_top <= paint_bottom {
3027 return;
···7370 let bottom = (&raw const _stack_end_cpu0) as usize;
7471 let top = (&raw const _stack_start_cpu0) as usize;
75727676- let guard_skip = 256;
7777- let scan_bottom = bottom + guard_skip;
7373+ let scan_bottom = bottom + STACK_GUARD_SKIP;
78747975 let start = (scan_bottom + 3) & !3;
8076
+11-3
kernel/src/ui/widget.rs
···11// region geometry, alignment helpers, progress bar, loading indicator
2233use embedded_graphics::{
44- mono_font::MonoTextStyle, mono_font::ascii::FONT_9X18, pixelcolor::BinaryColor, prelude::*,
44+ mono_font::ascii::FONT_9X18, mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*,
55 primitives::PrimitiveStyle, primitives::Rectangle, text::Text,
66};
77···106106 if count == 0 {
107107 return 0;
108108 }
109109- if current + 1 >= count { 0 } else { current + 1 }
109109+ if current + 1 >= count {
110110+ 0
111111+ } else {
112112+ current + 1
113113+ }
110114}
111115112116#[inline]
···114118 if count == 0 {
115119 return 0;
116120 }
117117- if current == 0 { count - 1 } else { current - 1 }
121121+ if current == 0 {
122122+ count - 1
123123+ } else {
124124+ current - 1
125125+ }
118126}
119127120128// horizontal progress bar for 1-bit e-paper