···11-// battery calibration for the XTEink X4.
11+// battery calibration for the XTEink X4
22// GPIO0 reads through 100K/100K divider (2:1); ADC 11dB attenuation
33-// gives 0..2500 mV --- multiply by 2 for actual cell voltage.
33+// gives 0..2500 mV; multiply by 2 for actual cell voltage
4455-// voltage divider multiplier (100K/100K resistive divider).
55+// voltage divider multiplier (100K/100K resistive divider)
66pub const DIVIDER_MULT: u32 = 2;
7788-// piecewise-linear li-ion discharge curve. sorted descending by mV.
88+// piecewise-linear li-ion discharge curve, sorted descending by mV
99pub const DISCHARGE_CURVE: &[(u32, u8)] = &[
1010 (4200, 100),
1111 (4060, 90),
+4-4
kernel/src/board/layout.rs
···11-// physical button positions on the XTEink X4 bezel.
22-// used by button_feedback to render labels at the correct screen edge.
11+// physical button positions on the XTEink X4 bezel
22+// used by button_feedback to render labels at the correct screen edge
3344-// center-x of bottom-edge buttons.
44+// center-x of bottom-edge buttons
55pub const CX_BACK: u16 = 84;
66pub const CX_CONFIRM: u16 = 194;
77pub const CX_LEFT: u16 = 286;
88pub const CX_RIGHT: u16 = 396;
991010-// center-y of right-edge buttons.
1010+// center-y of right-edge buttons
1111pub const CY_VOL_UP: u16 = 364;
1212pub const CY_VOL_DOWN: u16 = 484;
+2-2
kernel/src/drivers/battery.rs
···11-// battery voltage estimation --- generic over board calibration.
22-// board-specific divider ratio and discharge curve live in board::battery.
11+// battery voltage estimation, generic over board calibration
22+// board-specific divider ratio and discharge curve live in board::battery
3344use crate::board::battery::{DISCHARGE_CURVE, DIVIDER_MULT};
55
···22//
33// all I/O through embedded-sdmmc AsyncVolumeManager; functions are
44// synchronous, wrapping async ops with poll_once (SPI bus is blocking
55-// so every .await resolves immediately).
55+// so every .await resolves immediately)
66//
77-// Returns the unified `Error` type (re-exported as `StorageError` for
88-// backward compatibility). Apps receive it directly through KernelHandle.
77+// returns the unified Error type (re-exported as StorageError for
88+// backward compat); apps receive it through KernelHandle
991010use core::ops::ControlFlow;
1111···1818pub const TITLES_FILE: &str = "TITLES.BIN";
1919pub const TITLE_CAP: usize = 48;
20202121-/// Backward-compatible alias — old code that references `StorageError`
2222-/// continues to compile while call-sites are migrated to `Error`.
2121+// backward-compatible alias
2322pub type StorageError = Error;
24232524#[derive(Clone, Copy)]
···9897 pos as u8
9998}
10099101101-// ── file-operation macros ─────────────────────────────────────────
102102-//
103103-// each evaluates to Result<T, Error>; none use `?` internally
104104-// so caller cleanup (close_dir etc) is never bypassed
100100+// file-operation macros; each evaluates to Result<T, Error>
101101+// none use ? internally so caller cleanup is never bypassed
105102106103macro_rules! op_file_size {
107104 ($inner:expr, $dir:expr, $name:expr) => {
···220217 }};
221218}
222219223223-// dir-scoping macros: open subdir, execute body, close handle
220220+// dir-scoping macros; open subdir, execute body, close handle
224221225222macro_rules! in_dir {
226223 ($inner:expr, $dirname:expr, |$dir:ident| $body:expr) => {
···255252 };
256253}
257254258258-// borrow helper
259259-260255fn borrow(sd: &SdStorage) -> core::result::Result<core::cell::RefMut<'_, SdStorageInner>, Error> {
261256 sd.borrow_inner()
262257 .ok_or(Error::new(ErrorKind::NoCard, "storage::borrow"))
263258}
264259265265-// ── root file operations ──────────────────────────────────────────
260260+// root file operations
266261267262pub fn file_size(sd: &SdStorage, name: &str) -> crate::error::Result<u32> {
268263 poll_once(async {
···321316 })
322317}
323318324324-// ── directory listing ─────────────────────────────────────────────
319319+// directory listing
325320326321pub fn list_root_files(sd: &SdStorage, buf: &mut [DirEntry]) -> crate::error::Result<usize> {
327322 poll_once(async {
···379374 })
380375}
381376382382-// ── directory management ──────────────────────────────────────────
377377+// directory management
383378384379pub fn ensure_dir(sd: &SdStorage, name: &str) -> crate::error::Result<()> {
385380 // two poll_once calls so the large make_dir future never shares
···411406 })
412407}
413408414414-// ── single-directory file operations ──────────────────────────────
409409+// single-directory file operations
415410416411pub fn write_file_in_dir(
417412 sd: &SdStorage,
···468463 })
469464}
470465471471-// ── async boot path (runs inside the real executor) ───────────────
466466+// async boot path (runs inside the real executor)
472467473468pub async fn ensure_pulp_dir_async(sd: &SdStorage) -> crate::error::Result<()> {
474469 let mut guard = borrow(sd)?;
···488483 }
489484}
490485491491-// ── _PULP subdirectory operations ─────────────────────────────────
486486+// _PULP subdirectory operations
492487493488pub fn ensure_pulp_subdir(sd: &SdStorage, name: &str) -> crate::error::Result<()> {
494489 let exists = poll_once(async {
···590585 })
591586}
592587593593-// ── title mapping ─────────────────────────────────────────────────
588588+// title mapping
594589595595-/// Append a title mapping line to _PULP/TITLES.BIN
590590+// append a title line to _PULP/TITLES.BIN
596591pub fn save_title(sd: &SdStorage, filename: &str, title: &str) -> crate::error::Result<()> {
597592 let name_bytes = filename.as_bytes();
598593 let title_bytes = title.as_bytes();
+4-1
kernel/src/drivers/strip.rs
···218218 }
219219220220 // buf_y for x=0: physical row offset into window
221221- let base_buf_y = (HEIGHT as i32 - 1 - gx - wy) as usize;
221221+ let base_buf_y_i = HEIGHT as i32 - 1 - gx - wy;
222222+ debug_assert!(base_buf_y_i >= 0, "blit_1bpp_270: base_buf_y underflow");
223223+ debug_assert!(gy + y0 as i32 >= wx, "blit_1bpp_270: buf_x underflow");
224224+ let base_buf_y = base_buf_y_i as usize;
222225223226 for y in y0..y1 {
224227 let row = offset + y * stride;
+21-143
kernel/src/error.rs
···11-// Unified error type for pulp-os
11+// unified error type for pulp-os
22//
33-// Replaces the flat `StorageError` enum and ad-hoc `&'static str`
44-// errors with a single `Copy` type that carries:
55-//
66-// ErrorKind — *what* went wrong (storage, parse, resource …)
77-// source — *where* it happened (`&'static str`, usually
88-// module_path!() or a short caller-supplied tag)
99-//
1010-// Every `Result` in the kernel and app layers should use this type.
1111-// The smol-epub trait boundary (`Result<T, &'static str>`) converts
1212-// at the edge via the `From` impls.
33+// single Copy type carrying ErrorKind (what) and a &'static str
44+// source tag (where); smol-epub boundary converts via From impls
135146use core::fmt;
1571616-// ---------------------------------------------------------------------------
1717-// ErrorKind — the category of failure
1818-// ---------------------------------------------------------------------------
1919-2020-/// What went wrong.
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229#[non_exhaustive]
2310pub enum ErrorKind {
2424- // -- storage / SD card --
2525- /// SD card not inserted or not responding.
1111+ // storage / sd card
2612 NoCard,
2727- /// Could not open the FAT volume.
2813 OpenVolume,
2929- /// Could not open a directory.
3014 OpenDir,
3131- /// Could not open a file.
3215 OpenFile,
3333- /// Read I/O failed.
3416 ReadFailed,
3535- /// Write I/O failed.
3617 WriteFailed,
3737- /// Seek within a file failed.
3818 SeekFailed,
3939- /// Delete operation failed.
4019 DeleteFailed,
4141- /// Directory is full (cannot create entry).
4220 DirFull,
4343- /// File or directory not found.
4421 NotFound,
45224646- // -- data / parsing --
4747- /// EPUB, ZIP, or similar structure is invalid.
2323+ // data / parsing
4824 ParseFailed,
4949- /// Data is malformed or unexpected.
5025 InvalidData,
5151- /// UTF-8 or other text-encoding error.
5226 BadEncoding,
53275454- // -- resources --
5555- /// Heap allocation failed.
2828+ // resources
5629 OutOfMemory,
5757- /// Supplied buffer is too small for the operation.
5830 BufferTooSmall,
59316060- // -- network (upload) --
6161- /// Network read/write failed.
3232+ // network (upload)
6233 NetworkIo,
6363- /// Protocol-level error (HTTP, multipart, etc.).
6434 Protocol,
65356666- // -- catch-all --
6767- /// Unclassified error (carries context in `source`).
3636+ // catch-all
6837 Other,
6938}
70397140impl ErrorKind {
7272- /// Short human-readable label (suitable for UI and log lines).
7341 pub const fn as_str(self) -> &'static str {
7442 match self {
7543 Self::NoCard => "no sd card",
···9361 }
9462 }
95639696- /// True for any variant that originates from SD-card storage I/O.
9764 pub const fn is_storage(self) -> bool {
9865 matches!(
9966 self,
···11784 }
11885}
11986120120-// ---------------------------------------------------------------------------
121121-// Error — the unified error value
122122-// ---------------------------------------------------------------------------
123123-124124-/// Unified error for the entire pulp-os stack.
125125-///
126126-/// Cheap to copy (one discriminant byte + one `&'static str` pointer).
127127-/// Carries *what* failed ([`ErrorKind`]) and a compile-time *source*
128128-/// string that identifies the call-site or subsystem.
129129-///
130130-/// # Constructing
131131-///
132132-/// ```ignore
133133-/// // Constant shorthand (no source tag):
134134-/// Error::READ_FAILED
135135-///
136136-/// // With explicit source:
137137-/// Error::new(ErrorKind::OpenFile, "epub_init_zip")
138138-///
139139-/// // Via the err!() macro (auto-stamps module_path!()):
140140-/// err!(ReadFailed)
141141-/// err!(OpenFile, "epub_init_zip")
142142-/// ```
8787+// one discriminant byte + one &'static str pointer; cheap to copy
8888+// source is module_path!() or a short caller-supplied tag
14389#[derive(Clone, Copy)]
14490pub struct Error {
14591 kind: ErrorKind,
146146- /// Where the error was created — a `module_path!()` or free-form
147147- /// tag. Empty string when no source was attached.
14892 source: &'static str,
14993}
15094151151-// -- construction ----------------------------------------------------------
152152-15395impl Error {
154154- /// Create an error with explicit kind and source tag.
15596 #[inline]
15697 pub const fn new(kind: ErrorKind, source: &'static str) -> Self {
15798 Self { kind, source }
15899 }
159100160160- /// Create an error from a kind alone (no source context).
161101 #[inline]
162102 pub const fn from_kind(kind: ErrorKind) -> Self {
163103 Self { kind, source: "" }
164104 }
165105166166- // Named constants that mirror the old `StorageError` variants so
167167- // existing match-arms keep compiling during migration.
168168-169106 pub const NO_CARD: Self = Self::from_kind(ErrorKind::NoCard);
170107 pub const OPEN_VOLUME: Self = Self::from_kind(ErrorKind::OpenVolume);
171108 pub const OPEN_DIR: Self = Self::from_kind(ErrorKind::OpenDir);
···178115 pub const NOT_FOUND: Self = Self::from_kind(ErrorKind::NotFound);
179116}
180117181181-// -- accessors -------------------------------------------------------------
182182-183118impl Error {
184184- /// The failure category.
185119 #[inline]
186120 pub const fn kind(&self) -> ErrorKind {
187121 self.kind
188122 }
189123190190- /// The compile-time tag identifying where this error was created.
191191- /// Returns `""` when no source was attached.
192124 #[inline]
193125 pub const fn source_tag(&self) -> &'static str {
194126 self.source
195127 }
196128197197- /// Attach (or replace) the source tag. Useful when propagating
198198- /// an error upward and adding the caller's context.
199129 #[inline]
200130 pub const fn with_source(self, source: &'static str) -> Self {
201131 Self {
···204134 }
205135 }
206136207207- /// Change the kind while keeping the source.
208137 #[inline]
209138 pub const fn with_kind(self, kind: ErrorKind) -> Self {
210139 Self {
···213142 }
214143 }
215144216216- /// True when a source tag has been attached.
217145 #[inline]
218146 pub const fn has_source(&self) -> bool {
219147 !self.source.is_empty()
220148 }
221149222222- /// True when the error originates from storage I/O.
223150 #[inline]
224151 pub const fn is_storage(&self) -> bool {
225152 self.kind.is_storage()
226153 }
227154228228- /// Short label for the smol-epub `Result<T, &'static str>` boundary.
229155 #[inline]
230156 pub const fn as_str(&self) -> &'static str {
231157 self.kind.as_str()
232158 }
233159}
234234-235235-// -- formatting ------------------------------------------------------------
236160237161impl fmt::Debug for Error {
238162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
···254178 }
255179}
256180257257-// -- equality (semantic: kind only, source is diagnostic) ------------------
258258-181181+// equality is semantic: kind only, source is diagnostic
259182impl PartialEq for Error {
260183 #[inline]
261184 fn eq(&self, other: &Self) -> bool {
···265188266189impl Eq for Error {}
267190268268-// ---------------------------------------------------------------------------
269269-// Conversions
270270-// ---------------------------------------------------------------------------
271271-272272-/// Wrap a bare `&'static str` (from smol-epub helpers, etc.) into an
273273-/// [`Error`]. Well-known strings are mapped to the appropriate kind;
274274-/// everything else becomes [`ErrorKind::Other`] with the original
275275-/// string preserved as the source tag.
191191+// wrap &'static str (smol-epub returns) into Error; well-known
192192+// strings map to the appropriate kind, rest becomes Other
276193impl From<&'static str> for Error {
277194 #[inline]
278195 fn from(msg: &'static str) -> Self {
···297214 }
298215}
299216300300-/// Project back to `&'static str` for the smol-epub trait boundary.
217217+// project back to &'static str for the smol-epub trait boundary
301218impl From<Error> for &'static str {
302219 #[inline]
303220 fn from(e: Error) -> &'static str {
304304- // Prefer the source tag if it is a meaningful human string;
305305- // otherwise fall back to the kind label.
306221 if e.source.is_empty() {
307222 e.kind.as_str()
308223 } else {
···311226 }
312227}
313228314314-// ---------------------------------------------------------------------------
315315-// ResultExt — ergonomic source tagging on Results
316316-// ---------------------------------------------------------------------------
317317-318318-/// Extension trait for stamping source context onto any
319319-/// `Result<T, Error>`.
320320-///
321321-/// ```ignore
322322-/// storage::read_file_chunk(sd, name, off, buf)
323323-/// .source("epub_init_zip")?;
324324-/// ```
229229+// ergonomic source tagging on Result<T, Error> and
230230+// Result<T, &'static str> (smol-epub returns)
325231pub trait ResultExt<T> {
326326- /// Attach a source tag to the error (if any).
327232 fn source(self, src: &'static str) -> Result<T>;
328328-329329- /// Replace the error kind while adding a source tag.
330233 fn map_kind(self, kind: ErrorKind, src: &'static str) -> Result<T>;
331234}
332235···342245 }
343246}
344247345345-/// Blanket impl so `Result<T, &'static str>` (smol-epub returns) can
346346-/// be tagged and converted in one step.
347248impl<T> ResultExt<T> for core::result::Result<T, &'static str> {
348249 #[inline]
349250 fn source(self, src: &'static str) -> Result<T> {
···356257 }
357258}
358259359359-// ---------------------------------------------------------------------------
360360-// err! macro — stamps module_path!() automatically
361361-// ---------------------------------------------------------------------------
362362-363363-/// Create an [`Error`] with the caller's module path baked in.
364364-///
365365-/// ```ignore
366366-/// // Kind only — source is the calling module's path:
367367-/// err!(ReadFailed)
368368-///
369369-/// // Kind + explicit context string:
370370-/// err!(OpenFile, "epub_init_zip")
371371-/// ```
260260+// create an Error with module_path!() as source
261261+// err!(ReadFailed)
262262+// err!(OpenFile, "epub_init_zip")
372263#[macro_export]
373264macro_rules! err {
374265 ($kind:ident) => {
···379270 };
380271}
381272382382-/// Map any `Result<T, _>` error into an [`Error`] of the given kind,
383383-/// stamping the caller's module path.
384384-///
385385-/// ```ignore
386386-/// mgr.read(file, buf).await.or_err!(ReadFailed)?;
387387-/// ```
273273+// map any Result<T, _> into an Error of the given kind
274274+// or_err!(mgr.read(file, buf).await, ReadFailed)
388275#[macro_export]
389276macro_rules! or_err {
390277 ($result:expr, $kind:ident) => {
···396283 };
397284}
398285399399-// ---------------------------------------------------------------------------
400400-// Result alias
401401-// ---------------------------------------------------------------------------
402402-403403-/// Convenience alias used throughout pulp-os.
404404-///
405405-/// Intentionally shadows `core::result::Result` only when imported
406406-/// unqualified — callers that need the two-param form can still write
407407-/// `core::result::Result<T, E>`.
408286pub type Result<T> = core::result::Result<T, Error>;
+74-16
kernel/src/kernel/app.rs
···11-// app protocol: trait, context, transitions, redraw types, and coalescing
11+// app protocol: trait, context, transitions, redraw types, coalescing,
22+// and loading indicator state
23//
34// these types define the contract between the kernel scheduler and
45// the app layer. concrete apps implement the App trait; the kernel
···910// it. the kernel never knows which specific apps exist.
1011//
1112// QuickAction types also live here -- they are pure data describing
1212-// what actions an app exposes. the renderer (QuickMenu widget) is
1313-// app-side, but the protocol is kernel-side.
1313+// what actions an app exposes; the renderer (QuickMenu widget) is
1414+// app-side, but the protocol is kernel-side
14151516use embassy_time::Instant;
1617use esp_hal::delay::Delay;
···72737374pub const RECENT_FILE: &str = "RECENT";
74757575-// distros define their own AppId enum and implement this trait.
7676+// distros define their own AppId enum and implement this trait
7677// the kernel uses HOME to initialise the nav stack and reset on
7777-// Transition::Home. nothing else about the concrete variants is
7878-// known to the kernel.
7878+// Transition::Home; nothing else about the concrete variants is
7979+// known to the kernel
79808081pub trait AppIdType: Copy + Eq + core::fmt::Debug {
8182 const HOME: Self;
···103104}
104105105106const MSG_BUF_SIZE: usize = 64;
107107+const LOADING_BUF_SIZE: usize = 32;
106108107109pub struct AppContext {
108110 msg_buf: [u8; MSG_BUF_SIZE],
···110112 redraw: Redraw,
111113 coalesce_until: Option<Instant>,
112114 immediate: bool,
115115+116116+ // loading indicator; kernel-level so any app can use it.
117117+ // drawn by the app manager after app content, before overlays.
118118+ // uses the built-in mono font so it works even with no bitmap
119119+ // fonts loaded.
120120+ loading_buf: [u8; LOADING_BUF_SIZE],
121121+ loading_len: u8,
122122+ loading_pct: u8,
123123+ loading_active: bool,
124124+ loading_region: Region,
113125}
114126115127impl Default for AppContext {
···126138 redraw: Redraw::None,
127139 coalesce_until: None,
128140 immediate: false,
141141+ loading_buf: [0u8; LOADING_BUF_SIZE],
142142+ loading_len: 0,
143143+ loading_pct: 0,
144144+ loading_active: false,
145145+ loading_region: Region::new(0, 0, 0, 0),
129146 }
130147 }
131148···169186 self.coalesce_until = None;
170187 }
171188172172- // alias; kept so callers in on_event that were already converted
173173- // continue to compile without churn
174174- #[inline]
175175- pub fn mark_dirty_immediate(&mut self, region: Region) {
176176- self.mark_dirty(region);
177177- }
178178-179189 // mark dirty with 50ms coalescing window; use only for background
180190 // batch updates (title scanner) where many rapid dirty marks
181191 // should coalesce into a single refresh
···213223 self.immediate = false;
214224 r
215225 }
226226+227227+ // loading indicator: set text and percentage.
228228+ // draws "msg...pct%" using the built-in mono font.
229229+ // region defines where it renders; typically just below the
230230+ // app header in the content area.
231231+ // auto-marks the region dirty so the next render shows it.
232232+ pub fn set_loading(&mut self, region: Region, msg: &str, pct: u8) {
233233+ let n = msg.len().min(LOADING_BUF_SIZE);
234234+ self.loading_buf[..n].copy_from_slice(&msg[..n].as_bytes()[..n]);
235235+ self.loading_len = n as u8;
236236+ self.loading_pct = pct.min(100);
237237+ self.loading_region = region;
238238+239239+ self.loading_active = true;
240240+ self.mark_dirty(region);
241241+ }
242242+243243+ // clear the loading indicator and mark its region dirty so
244244+ // the underlying content repaints
245245+ pub fn clear_loading(&mut self) {
246246+ if self.loading_active {
247247+ let region = self.loading_region;
248248+ self.loading_active = false;
249249+ self.loading_len = 0;
250250+ self.loading_pct = 0;
251251+ self.mark_dirty(region);
252252+ }
253253+ }
254254+255255+ #[inline]
256256+ pub fn loading_active(&self) -> bool {
257257+ self.loading_active
258258+ }
259259+260260+ #[inline]
261261+ pub fn loading_msg(&self) -> &str {
262262+ core::str::from_utf8(&self.loading_buf[..self.loading_len as usize]).unwrap_or("")
263263+ }
264264+265265+ #[inline]
266266+ pub fn loading_pct(&self) -> u8 {
267267+ self.loading_pct
268268+ }
269269+270270+ #[inline]
271271+ pub fn loading_region(&self) -> Region {
272272+ self.loading_region
273273+ }
216274}
217275218276// background is async for epub streaming (stream_strip_entry_async);
···351409 }
352410}
353411354354-// aggregate interface the kernel scheduler calls on the app layer.
412412+// aggregate interface the kernel scheduler calls on the app layer
355413// a distro implements this (typically via an AppManager struct that
356356-// holds concrete app types and a with_app! dispatch macro). the
414414+// holds concrete app types and a with_app! dispatch macro); the
357415// scheduler is generic over AppLayer without importing any concrete
358358-// app types.
416416+// app types
359417360418// run_special_mode is genuinely async (wifi radio); the rest is sync
361419#[allow(async_fn_in_trait)]
···22// once to EPD before the app layer takes over
33//
44// uses the embedded-graphics built-in FONT_6X13 -- no TTF assets or
55-// build.rs font pipeline needed. the kernel can show boot progress
66-// on a bare display with nothing but this mono font.
55+// build.rs font pipeline needed; the kernel can show boot progress
66+// on a bare display with nothing but this mono font
7788use embedded_graphics::mono_font::MonoTextStyle;
99use embedded_graphics::mono_font::ascii::FONT_6X13;
+3-3
kernel/src/kernel/handle.rs
···11// kernel handle: synchronous syscall boundary for apps
22//
33// every storage method calls a single storage::* function and returns
44-// the unified Error result. apps call these directly.
44+// the unified Error result; apps call these directly
55//
66-// app-specific logic (bookmarks, title scan, etc.) accesses the
66+// app-specific logic (bookmarks, title scan, etc) accesses the
77// underlying caches directly via bookmark_cache() / dir_cache_mut()
88-// rather than through dedicated handle methods.
88+// rather than through dedicated handle methods
991010use crate::drivers::storage::{self, DirEntry, DirPage};
1111use crate::error::{Error, Result};
+3-4
kernel/src/kernel/mod.rs
···11// kernel: owns hardware resources, caches, and system state
22//
33// constructed once during boot in main(), lives for the lifetime of
44-// the program. not a separate Embassy task -- a struct held by main.
44+// the program; not a separate Embassy task -- a struct held by main
55//
66// apps interact exclusively through KernelHandle, which borrows the
77-// kernel for the duration of an async lifecycle method.
77+// kernel for the duration of an async lifecycle method
8899pub mod app;
1010pub mod bookmarks;
···2020// Unified error types (primary home: crate::error)
2121pub use crate::error::{Error, ErrorKind, Result, ResultExt};
22222323-// Backward-compatible alias so `kernel::StorageError` keeps working
2424-// during migration. It is now `type StorageError = Error`.
2323+// backward-compatible alias
2524pub use crate::drivers::storage::StorageError;
26252726pub use app::{
+39-43
kernel/src/kernel/scheduler.rs
···11// scheduler: main event loop, render pipeline, housekeeping, sleep
22//
33-// EPD and SD share a single SPI bus via CriticalSectionDevice.
33+// EPD and SD share a single SPI bus via CriticalSectionDevice;
44// during normal operation, all SD I/O completes before render()
55-// touches the EPD. during the DU/GC waveform (~400ms), the EPD
55+// touches the EPD; during the DU/GC waveform (~400ms), the EPD
66// charge pump drives pixels with no SPI commands, so the bus is
77-// free for SD I/O. busy_wait_with_background exploits this window
88-// to run background caching and housekeeping during the waveform.
77+// free for SD I/O - busy_wait_with_background exploits this
88+// window to run background caching and housekeeping
99//
1010// handle_input and poll_housekeeping are synchronous; they return
1111// a bool flag when the caller should enter_sleep (which is async
1212-// because it renders a sleep screen via the EPD).
1212+// because it renders a sleep screen via the EPD)
1313//
1414// sd_card_sleep sends cmd0 before deep sleep to reduce sd card
1515-// idle current from ~150 µa to ~10 µa
1515+// idle current from ~150 uA to ~10 uA
16161717use embassy_futures::select::{Either, select};
1818use embassy_time::{Duration, Ticker, with_timeout};
···181181 false
182182 }
183183184184- // returns true if idle sleep is due
185185- fn poll_housekeeping<A: AppLayer>(&mut self, app_mgr: &A) -> bool {
184184+ // shared housekeeping body: battery, sd probe, bookmark flush, stats
185185+ fn poll_housekeeping_inner<A: AppLayer>(&mut self, app_mgr: &A) {
186186 if let Some(mv) = tasks::BATTERY_MV.try_take() {
187187 self.cached_battery_mv = mv;
188188 }
···201201 tasks::set_idle_timeout(app_mgr.system_settings().sleep_timeout);
202202 }
203203 }
204204+ }
204205206206+ // returns true if idle sleep is due
207207+ fn poll_housekeeping<A: AppLayer>(&mut self, app_mgr: &A) -> bool {
208208+ self.poll_housekeeping_inner(app_mgr);
205209 tasks::IDLE_SLEEP_DUE.try_take().is_some()
206210 }
207211208208- // housekeeping without idle-sleep check; do not initiate sleep mid-refresh
212212+ // housekeeping without idle-sleep check; never sleep mid-refresh
209213 fn poll_housekeeping_waveform<A: AppLayer>(&mut self, app_mgr: &A) {
210210- if let Some(mv) = tasks::BATTERY_MV.try_take() {
211211- self.cached_battery_mv = mv;
212212- }
213213-214214- if tasks::SD_CHECK_DUE.try_take().is_some() {
215215- self.sd_ok = self.sd.probe_ok();
216216- }
217217-218218- if tasks::BOOKMARK_FLUSH_DUE.try_take().is_some() && self.bm_cache.is_dirty() {
219219- self.bm_cache.flush(&self.sd);
220220- }
221221-222222- if tasks::STATUS_DUE.try_take().is_some() {
223223- self.log_stats();
224224- if app_mgr.settings_loaded() {
225225- tasks::set_idle_timeout(app_mgr.system_settings().sleep_timeout);
226226- }
227227- }
228228- // idle sleep not checked here; never sleep during a waveform
214214+ self.poll_housekeeping_inner(app_mgr);
229215 }
230216231217 // partial refreshes use DU waveform (~400 ms); after ghost_clear_every
···330316 // no SPI commands are sent, so the bus is free for SD I/O.
331317 // is_busy() is a sync GPIO read; no epd borrow is held across
332318 // any .await point, so self is fully available for handle() etc.
319319+ //
320320+ // run_background is wrapped in select so input interrupts long
321321+ // background work (e.g. chapter caching). when the background
322322+ // future is dropped mid-stream, partial cache writes are safe
323323+ // because ch_cached stays false until the full write completes.
324324+ // the TICK_MS timeout ensures is_busy is re-checked regularly
325325+ // even during long background operations.
333326 async fn busy_wait_with_background<A: AppLayer>(
334327 &mut self,
335328 app_mgr: &mut A,
···337330 let mut deferred: Option<Transition<A::Id>> = None;
338331339332 loop {
340340- // sync gpio read; no borrow held after this line
341333 if !self.epd.is_busy() {
342334 break;
343335 }
344336345345- // wait up to TICK_MS for input; no epd borrow involved
346346- let input_event = with_timeout(
347347- Duration::from_millis(TICK_MS),
348348- tasks::INPUT_EVENTS.receive(),
349349- )
350350- .await
351351- .ok();
337337+ // run background, interruptible by input or tick timeout
338338+ let ev = {
339339+ let mut handle = self.handle();
340340+ match select(
341341+ app_mgr.run_background(&mut handle),
342342+ with_timeout(
343343+ Duration::from_millis(TICK_MS),
344344+ tasks::INPUT_EVENTS.receive(),
345345+ ),
346346+ )
347347+ .await
348348+ {
349349+ Either::First(()) => None,
350350+ Either::Second(Ok(ev)) => Some(ev),
351351+ Either::Second(Err(_)) => None,
352352+ }
353353+ };
352354353353- if let Some(hw_event) = input_event {
355355+ if let Some(hw_event) = ev {
354356 if !app_mgr.suppress_deferred_input() {
355357 let t = app_mgr.dispatch_event(hw_event, &mut *self.bm_cache);
356358 if t != Transition::None && deferred.is_none() {
357359 deferred = Some(t);
358360 }
359361 }
360360- continue;
361362 }
362363363363- // timeout elapsed; spi bus is free during waveform
364364- {
365365- let mut handle = self.handle();
366366- app_mgr.run_background(&mut handle).await;
367367- }
368364 self.poll_housekeeping_waveform(app_mgr);
369365 }
370366
+7-24
kernel/src/kernel/tasks.rs
···14141515pub static BATTERY_MV: Signal<CriticalSectionRawMutex, u16> = Signal::new();
16161717-const POLL_ACTIVE_MS: u64 = 10; // 100 hz: during/after input
1818-const POLL_IDLE_MS: u64 = 50; // 20 hz: no recent input
1919-const POLL_DROWSY_MS: u64 = 200; // 5 hz: approaching sleep timeout
2020-2121-const IDLE_AFTER_MS: u64 = 2_000;
2222-const DROWSY_AFTER_MS: u64 = 30_000;
2323-2424-const BATTERY_INTERVAL_MS: u64 = 30_000;
1717+const BATTERY_INTERVAL_TICKS: u32 = 3000; // 3000 x 10 ms = 30 s
25182619#[embassy_executor::task]
2720pub async fn input_task(mut input: InputDriver) -> ! {
2828- let mut poll_ms = POLL_ACTIVE_MS;
2929- let mut battery_accum_ms: u64 = 0;
2121+ let mut ticker = Ticker::every(Duration::from_millis(10));
2222+ let mut battery_counter: u32 = 0;
30233124 let raw = input.read_battery_mv();
3225 BATTERY_MV.signal(battery::adc_to_battery_mv(raw));
33263427 loop {
3535- Timer::after(Duration::from_millis(poll_ms)).await;
2828+ ticker.next().await;
36293730 if let Some(ev) = input.poll() {
3831 let _ = INPUT_EVENTS.try_send(ev);
3932 IDLE_RESET.signal(());
4040- poll_ms = POLL_ACTIVE_MS;
4141- } else {
4242- let since = input.ms_since_last_event();
4343- poll_ms = if since > DROWSY_AFTER_MS {
4444- POLL_DROWSY_MS
4545- } else if since > IDLE_AFTER_MS {
4646- POLL_IDLE_MS
4747- } else {
4848- POLL_ACTIVE_MS
4949- };
5033 }
51345252- battery_accum_ms += poll_ms;
5353- if battery_accum_ms >= BATTERY_INTERVAL_MS {
5454- battery_accum_ms = 0;
3535+ battery_counter += 1;
3636+ if battery_counter >= BATTERY_INTERVAL_TICKS {
3737+ battery_counter = 0;
5538 let raw = input.read_battery_mv();
5639 BATTERY_MV.signal(battery::adc_to_battery_mv(raw));
5740 }
+3-3
kernel/src/lib.rs
···11// pulp-kernel -- hardware drivers, scheduling, and system core
22//
33-// generic over AppLayer; never imports concrete apps or fonts.
33+// generic over AppLayer; never imports concrete apps or fonts
44// ships a built-in mono font (FONT_6X13) for boot console and
55-// sleep screen. distros bring their own proportional fonts.
55+// sleep screen; distros bring their own proportional fonts
6677#![no_std]
88···1414pub mod kernel;
1515pub mod ui;
16161717-// Re-export the core error types at crate root for convenience.
1717+// re-export core error types at crate root
1818pub use error::{Error, ErrorKind, Result, ResultExt};
+5-3
kernel/src/ui/mod.rs
···11// widget primitives for 1-bit e-paper displays
22//
33-// font-independent: Region, Alignment, stack measurement, StackFmt.
33+// font-independent: Region, Alignment, stack measurement, StackFmt
44// font-dependent widgets (BitmapLabel, QuickMenu, ButtonFeedback)
55-// live in the distro's apps::widgets module.
55+// live in the distro's apps::widgets module
6677pub mod stack_fmt;
88pub mod statusbar;
···1212pub use statusbar::{
1313 BAR_HEIGHT, CONTENT_TOP, free_stack_bytes, paint_stack, stack_high_water_mark,
1414};
1515-pub use widget::{Alignment, Region, draw_progress_bar, wrap_next, wrap_prev};
1515+pub use widget::{
1616+ Alignment, Region, draw_loading_indicator, draw_progress_bar, wrap_next, wrap_prev,
1717+};
16181719pub use crate::board::{SCREEN_H, SCREEN_W};
+40-5
kernel/src/ui/widget.rs
···11-// region geometry and alignment helpers, progress bar drawing
11+// region geometry, alignment helpers, progress bar, loading indicator
2233use embedded_graphics::{
44- pixelcolor::BinaryColor, prelude::*, primitives::PrimitiveStyle, primitives::Rectangle,
44+ mono_font::MonoTextStyle, mono_font::ascii::FONT_6X13, pixelcolor::BinaryColor, prelude::*,
55+ primitives::PrimitiveStyle, primitives::Rectangle, text::Text,
56};
6778use crate::drivers::strip::StripBuffer;
99+use crate::ui::stack_fmt::BorrowedFmt;
810911#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1012pub struct Region {
···115117 if current == 0 { count - 1 } else { current - 1 }
116118}
117119118118-// horizontal progress bar for 1-bit e-paper.
120120+// horizontal progress bar for 1-bit e-paper
119121// draws a 1px black border around the full track and fills
120120-// proportionally from the left. pct is clamped to 0..=100.
121121-// region should be at least 4px wide and 4px tall.
122122+// proportionally from the left; pct is clamped to 0..=100
123123+// region should be at least 4px wide and 4px tall
122124pub fn draw_progress_bar(strip: &mut StripBuffer, region: Region, pct: u8) {
123125 let pct = pct.min(100) as u32;
124126···149151 .unwrap();
150152 }
151153}
154154+155155+// loading indicator for 1-bit e-paper
156156+// draws "msg...pct%" centered vertically in the region using the
157157+// built-in FONT_6X13 mono font; works without any custom bitmap
158158+// fonts loaded, usable from any app or the kernel itself
159159+//
160160+// typical usage:
161161+// draw_loading_indicator(strip, region, "Loading", 25) => "Loading...25%"
162162+// draw_loading_indicator(strip, region, "Caching 3/15", 20) => "Caching 3/15...20%"
163163+pub fn draw_loading_indicator(strip: &mut StripBuffer, region: Region, msg: &str, pct: u8) {
164164+ use core::fmt::Write;
165165+166166+ // clear region
167167+ region
168168+ .to_rect()
169169+ .into_styled(PrimitiveStyle::with_fill(BinaryColor::Off))
170170+ .draw(strip)
171171+ .unwrap();
172172+173173+ // format "msg...pct%"
174174+ let mut buf = [0u8; 48];
175175+ let mut fmt = BorrowedFmt::new(&mut buf);
176176+ let _ = write!(fmt, "{}...{}%", msg, pct.min(100));
177177+ let text = fmt.as_str();
178178+179179+ // FONT_6X13: 6px wide, 13px tall, ~10px ascent
180180+ // center vertically; baseline = region.y + (h + 7) / 2
181181+ let style = MonoTextStyle::new(&FONT_6X13, BinaryColor::On);
182182+ let baseline_y = region.y as i32 + (region.h as i32 + 7) / 2;
183183+ Text::new(text, Point::new(region.x as i32 + 2, baseline_y), style)
184184+ .draw(strip)
185185+ .unwrap();
186186+}
+16
src/apps/manager.rs
···11// app lifecycle manager: nav stack, dispatch, font propagation, draw
22//
33// all dispatch is static (monomorphized via with_app!); no dyn, no vtable
44+// loading indicator is drawn between app content and overlays so it
55+// sits on top of page content but under quick menu and button bumps
4657use crate::apps::files::FilesApp;
68use crate::apps::home::HomeApp;
···264266 }
265267266268 self.propagate_fonts();
269269+ self.launcher.ctx.clear_loading();
267270268271 if nav.to != AppId::Upload {
269272 if nav.resume {
···307310 pub fn draw(&self, strip: &mut StripBuffer) {
308311 let active = self.launcher.active();
309312 with_app_ref!(active, self, |app| app.draw(strip));
313313+314314+ // loading indicator: after app content, before overlays
315315+ if self.launcher.ctx.loading_active() {
316316+ let region = self.launcher.ctx.loading_region();
317317+ if region.intersects(strip.logical_window()) {
318318+ crate::ui::draw_loading_indicator(
319319+ strip,
320320+ region,
321321+ self.launcher.ctx.loading_msg(),
322322+ self.launcher.ctx.loading_pct(),
323323+ );
324324+ }
325325+ }
310326311327 if self.quick_menu.open {
312328 self.quick_menu.draw(strip);
+3-4
src/apps/mod.rs
···11// app modules, AppId definition, and re-exports from kernel::app
22//
33// AppId is defined here (the distro side) -- the kernel is generic
44-// over AppIdType and never knows which concrete apps exist.
44+// over AppIdType and never knows which concrete apps exist
5566pub mod files;
77pub mod home;
···35353636pub use crate::kernel::app::{App, AppContext, PendingSetting, RECENT_FILE, Redraw};
37373838-// Unified error types — available to all app code as `crate::apps::Error` etc.
3838+// unified error types
3939pub use crate::kernel::{Error, ErrorKind, Result, ResultExt};
40404141-// Backward-compatible alias; old app code referencing `StorageError`
4242-// keeps compiling — it is now `type StorageError = Error`.
4141+// backward-compatible alias
4342pub use crate::kernel::StorageError;
+2-2
src/apps/reader/images.rs
···33// scan_chapter_for_image is the shared core: reads chapter data in
44// chunks, finds IMG_REF markers, resolves paths, checks cache, and
55// either decodes inline (large images) or dispatches to the worker
66-// (small images). both epub_find_and_dispatch_image (background scan)
77-// and dispatch_one_image_in_chapter (nearby prefetch) call through it.
66+// (small images); both epub_find_and_dispatch_image (background scan)
77+// and dispatch_one_image_in_chapter (nearby prefetch) call through it
8899extern crate alloc;
1010
···11// font-dependent UI widgets (app-side)
22//
33// these widgets depend on BitmapFont from the fontdue pipeline and
44-// live in the apps layer, not the kernel. the kernel's ui/ module
55-// holds only font-independent primitives (Region, Alignment, etc.).
44+// live in the apps layer, not the kernel; the kernel's ui/ module
55+// holds only font-independent primitives (Region, Alignment, etc)
6677pub mod bitmap_label;
88pub mod button_feedback;