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.

add: proper'er errors

make `KernelHandle` synchrnous
make `AppTrait, `AppLayer` sync
add adaptive input polling*
increase work q cap
other fixes

hans 6764be66 9cbc8a99

+1342 -958
+15 -1
kernel/src/board/mod.rs
··· 40 40 static SPI_BUS: StaticCell<Mutex<RefCell<SpiBus>>> = StaticCell::new(); 41 41 42 42 // cached ref to the SPI bus mutex, set once in Board::init 43 - static SPI_BUS_REF: Mutex<core::cell::Cell<Option<&'static Mutex<RefCell<SpiBus>>>>> = 43 + // cached ref to the SPI bus mutex; pub(crate) so scheduler can 44 + // access the bus in sd_card_sleep before deep sleep 45 + pub(crate) static SPI_BUS_REF: Mutex<core::cell::Cell<Option<&'static Mutex<RefCell<SpiBus>>>>> = 44 46 Mutex::new(core::cell::Cell::new(None)); 47 + 48 + // sd cs clone; only used in enter_sleep to send cmd0 49 + // safety: same clone_unchecked pattern as gpio0/1/2/3 in init_input; 50 + // only accessed after all normal sd i/o has stopped and before mcu halts 51 + pub(crate) static SD_CS_SLEEP: Mutex<RefCell<Option<raw_gpio::RawOutputPin>>> = 52 + Mutex::new(RefCell::new(None)); 45 53 46 54 static POWER_BTN: Mutex<RefCell<Option<Input<'static>>>> = Mutex::new(RefCell::new(None)); 47 55 ··· 178 186 179 187 // GPIO12 free in DIO mode; no esp-hal type, use raw registers 180 188 let sd_cs = unsafe { raw_gpio::RawOutputPin::new(12) }; 189 + 190 + // second handle to GPIO12 for sending cmd0 before deep sleep 191 + let sd_cs_sleep = unsafe { raw_gpio::RawOutputPin::new(12) }; 192 + critical_section::with(|cs| { 193 + SD_CS_SLEEP.borrow_ref_mut(cs).replace(sd_cs_sleep); 194 + }); 181 195 182 196 let slow_cfg = spi::master::Config::default().with_frequency(Rate::from_khz(400)); 183 197
+18 -2
kernel/src/drivers/input.rs
··· 71 71 press_since: Instant, 72 72 long_press_fired: bool, 73 73 last_repeat: Instant, 74 + last_event_at: Instant, 74 75 queue: EventQueue, 75 76 } 76 77 ··· 85 86 press_since: now, 86 87 long_press_fired: false, 87 88 last_repeat: now, 89 + last_event_at: now, 88 90 queue: EventQueue::new(), 89 91 } 90 92 } 91 93 92 94 pub fn poll(&mut self) -> Option<Event> { 93 95 if !self.queue.is_empty() { 94 - return self.queue.pop(); 96 + let ev = self.queue.pop(); 97 + if ev.is_some() { 98 + self.last_event_at = Instant::now(); 99 + } 100 + return ev; 95 101 } 96 102 97 103 let raw = self.read_raw(); ··· 119 125 self.last_repeat = now; 120 126 } 121 127 self.stable = debounced; 122 - return self.queue.pop(); 128 + let ev = self.queue.pop(); 129 + if ev.is_some() { 130 + self.last_event_at = now; 131 + } 132 + return ev; 123 133 } 124 134 125 135 if let Some(btn) = self.stable { ··· 128 138 if !self.long_press_fired && held >= Duration::from_millis(LONG_PRESS_MS) { 129 139 self.long_press_fired = true; 130 140 self.last_repeat = now; 141 + self.last_event_at = now; 131 142 return Some(Event::LongPress(btn)); 132 143 } 133 144 134 145 if self.long_press_fired && (now - self.last_repeat) >= Duration::from_millis(REPEAT_MS) 135 146 { 136 147 self.last_repeat = now; 148 + self.last_event_at = now; 137 149 return Some(Event::Repeat(btn)); 138 150 } 139 151 } 140 152 141 153 None 154 + } 155 + 156 + pub fn ms_since_last_event(&self) -> u64 { 157 + (Instant::now() - self.last_event_at).as_millis() 142 158 } 143 159 144 160 fn read_raw(&mut self) -> Option<Button> {
+13
kernel/src/drivers/sdcard.rs
··· 170 170 pub(crate) fn borrow_inner(&self) -> Option<core::cell::RefMut<'_, SdStorageInner>> { 171 171 self.inner.as_ref().map(|c| c.borrow_mut()) 172 172 } 173 + 174 + // flush pending writes and close fat handles; best-effort before halt. 175 + // after this call no further sd i/o is possible until mcu reset 176 + pub fn flush_and_close(&self) { 177 + if let Some(ref cell) = self.inner { 178 + let mut guard = cell.borrow_mut(); 179 + let inner = &mut *guard; 180 + let _ = inner.mgr.close_dir(inner.root); 181 + poll_once(async { 182 + let _ = inner.mgr.close_volume(inner.vol).await; 183 + }); 184 + } 185 + } 173 186 } 174 187 175 188 // drive a future to completion in exactly one poll
+76 -54
kernel/src/drivers/storage.rs
··· 2 2 // 3 3 // all I/O through embedded-sdmmc AsyncVolumeManager; functions are 4 4 // synchronous, wrapping async ops with poll_once (SPI bus is blocking 5 - // so every .await resolves immediately) 5 + // so every .await resolves immediately). 6 + // 7 + // Returns the unified `Error` type (re-exported as `StorageError` for 8 + // backward compatibility). Apps receive it directly through KernelHandle. 6 9 7 10 use core::ops::ControlFlow; 8 11 9 12 use embedded_sdmmc::Mode; 10 13 11 14 use crate::drivers::sdcard::{SdStorage, SdStorageInner, poll_once}; 15 + use crate::error::{Error, ErrorKind}; 12 16 13 17 pub const PULP_DIR: &str = "_PULP"; 14 18 pub const TITLES_FILE: &str = "TITLES.BIN"; 15 19 pub const TITLE_CAP: usize = 48; 20 + 21 + /// Backward-compatible alias — old code that references `StorageError` 22 + /// continues to compile while call-sites are migrated to `Error`. 23 + pub type StorageError = Error; 16 24 17 25 #[derive(Clone, Copy)] 18 26 pub struct DirEntry { ··· 90 98 pos as u8 91 99 } 92 100 93 - // file-operation macros 101 + // ── file-operation macros ───────────────────────────────────────── 94 102 // 95 - // each evaluates to Result<T, &'static str>; none use `?` internally 103 + // each evaluates to Result<T, Error>; none use `?` internally 96 104 // so caller cleanup (close_dir etc) is never bypassed 97 105 98 106 macro_rules! op_file_size { ··· 102 110 .find_directory_entry($dir, $name) 103 111 .await 104 112 .map(|e| e.size) 105 - .map_err(|_| "open file failed") 113 + .map_err(|_| Error::new(ErrorKind::OpenFile, "file_size")) 106 114 }; 107 115 } 108 116 ··· 113 121 .open_file_in_dir($dir, $name, Mode::ReadOnly) 114 122 .await 115 123 { 116 - Err(_) => Err("open file failed"), 124 + Err(_) => Err(Error::new(ErrorKind::OpenFile, "read_chunk")), 117 125 Ok(file) => { 118 126 let result = match $inner.mgr.file_seek_from_start(file, $offset) { 119 - Ok(()) => $inner.mgr.read(file, $buf).await.map_err(|_| "read failed"), 120 - Err(_) => Err("seek failed"), 127 + Ok(()) => $inner 128 + .mgr 129 + .read(file, $buf) 130 + .await 131 + .map_err(|_| Error::new(ErrorKind::ReadFailed, "read_chunk")), 132 + Err(_) => Err(Error::new(ErrorKind::SeekFailed, "read_chunk")), 121 133 }; 122 134 let _ = $inner.mgr.close_file(file).await; 123 135 result ··· 133 145 .open_file_in_dir($dir, $name, Mode::ReadOnly) 134 146 .await 135 147 { 136 - Err(_) => Err("open file failed"), 148 + Err(_) => Err(Error::new(ErrorKind::OpenFile, "read_start")), 137 149 Ok(file) => { 138 150 let size = $inner.mgr.file_length(file).unwrap_or(0); 139 - let result = $inner.mgr.read(file, $buf).await.map_err(|_| "read failed"); 151 + let result = $inner 152 + .mgr 153 + .read(file, $buf) 154 + .await 155 + .map_err(|_| Error::new(ErrorKind::ReadFailed, "read_start")); 140 156 let _ = $inner.mgr.close_file(file).await; 141 157 result.map(|n| (size, n)) 142 158 } ··· 151 167 .open_file_in_dir($dir, $name, Mode::ReadWriteCreateOrTruncate) 152 168 .await 153 169 { 154 - Err(_) => Err("create file failed"), 170 + Err(_) => Err(Error::new(ErrorKind::OpenFile, "write")), 155 171 Ok(file) => { 156 172 let result = if ($data).is_empty() { 157 173 Ok(()) ··· 160 176 .mgr 161 177 .write(file, $data) 162 178 .await 163 - .map_err(|_| "write failed") 179 + .map_err(|_| Error::new(ErrorKind::WriteFailed, "write")) 164 180 }; 165 181 let _ = $inner.mgr.close_file(file).await; 166 182 result ··· 176 192 .open_file_in_dir($dir, $name, Mode::ReadWriteCreateOrAppend) 177 193 .await 178 194 { 179 - Err(_) => Err("create file failed"), 195 + Err(_) => Err(Error::new(ErrorKind::OpenFile, "append")), 180 196 Ok(file) => { 181 197 let result = if ($data).is_empty() { 182 198 Ok(()) ··· 185 201 .mgr 186 202 .write(file, $data) 187 203 .await 188 - .map_err(|_| "write failed") 204 + .map_err(|_| Error::new(ErrorKind::WriteFailed, "append")) 189 205 }; 190 206 let _ = $inner.mgr.close_file(file).await; 191 207 result ··· 200 216 .mgr 201 217 .delete_entry_in_dir($dir, $name) 202 218 .await 203 - .map_err(|_| "delete failed") 219 + .map_err(|_| Error::new(ErrorKind::DeleteFailed, "delete")) 204 220 }}; 205 221 } 206 222 ··· 209 225 macro_rules! in_dir { 210 226 ($inner:expr, $dirname:expr, |$dir:ident| $body:expr) => { 211 227 match $inner.mgr.open_dir($inner.root, $dirname).await { 212 - Err(_) => Err("open dir failed"), 228 + Err(_) => Err(Error::new(ErrorKind::OpenDir, "in_dir")), 213 229 Ok($dir) => { 214 230 let _r = $body; 215 231 let _ = $inner.mgr.close_dir($dir); ··· 222 238 macro_rules! in_subdir { 223 239 ($inner:expr, $d1:expr, $d2:expr, |$dir:ident| $body:expr) => { 224 240 match $inner.mgr.open_dir($inner.root, $d1).await { 225 - Err(_) => Err("open dir failed"), 241 + Err(_) => Err(Error::new(ErrorKind::OpenDir, "in_subdir")), 226 242 Ok(_mid) => match $inner.mgr.open_dir(_mid, $d2).await { 227 243 Err(_) => { 228 244 let _ = $inner.mgr.close_dir(_mid); 229 - Err("open dir failed") 245 + Err(Error::new(ErrorKind::OpenDir, "in_subdir")) 230 246 } 231 247 Ok($dir) => { 232 248 let _r = $body; ··· 241 257 242 258 // borrow helper 243 259 244 - fn borrow(sd: &SdStorage) -> Result<core::cell::RefMut<'_, SdStorageInner>, &'static str> { 245 - sd.borrow_inner().ok_or("SD not mounted") 260 + fn borrow(sd: &SdStorage) -> core::result::Result<core::cell::RefMut<'_, SdStorageInner>, Error> { 261 + sd.borrow_inner() 262 + .ok_or(Error::new(ErrorKind::NoCard, "storage::borrow")) 246 263 } 247 264 248 - // root file operations 265 + // ── root file operations ────────────────────────────────────────── 249 266 250 - pub fn file_size(sd: &SdStorage, name: &str) -> Result<u32, &'static str> { 267 + pub fn file_size(sd: &SdStorage, name: &str) -> crate::error::Result<u32> { 251 268 poll_once(async { 252 269 let mut guard = borrow(sd)?; 253 270 let inner = &mut *guard; ··· 260 277 name: &str, 261 278 offset: u32, 262 279 buf: &mut [u8], 263 - ) -> Result<usize, &'static str> { 280 + ) -> crate::error::Result<usize> { 264 281 poll_once(async { 265 282 let mut guard = borrow(sd)?; 266 283 let inner = &mut *guard; ··· 272 289 sd: &SdStorage, 273 290 name: &str, 274 291 buf: &mut [u8], 275 - ) -> Result<(u32, usize), &'static str> { 292 + ) -> crate::error::Result<(u32, usize)> { 276 293 poll_once(async { 277 294 let mut guard = borrow(sd)?; 278 295 let inner = &mut *guard; ··· 280 297 }) 281 298 } 282 299 283 - pub fn write_file(sd: &SdStorage, name: &str, data: &[u8]) -> Result<(), &'static str> { 300 + pub fn write_file(sd: &SdStorage, name: &str, data: &[u8]) -> crate::error::Result<()> { 284 301 poll_once(async { 285 302 let mut guard = borrow(sd)?; 286 303 let inner = &mut *guard; ··· 288 305 }) 289 306 } 290 307 291 - pub fn append_root_file(sd: &SdStorage, name: &str, data: &[u8]) -> Result<(), &'static str> { 308 + pub fn append_root_file(sd: &SdStorage, name: &str, data: &[u8]) -> crate::error::Result<()> { 292 309 poll_once(async { 293 310 let mut guard = borrow(sd)?; 294 311 let inner = &mut *guard; ··· 296 313 }) 297 314 } 298 315 299 - pub fn delete_file(sd: &SdStorage, name: &str) -> Result<(), &'static str> { 316 + pub fn delete_file(sd: &SdStorage, name: &str) -> crate::error::Result<()> { 300 317 poll_once(async { 301 318 let mut guard = borrow(sd)?; 302 319 let inner = &mut *guard; ··· 304 321 }) 305 322 } 306 323 307 - // directory listing 324 + // ── directory listing ───────────────────────────────────────────── 308 325 309 - pub fn list_root_files(sd: &SdStorage, buf: &mut [DirEntry]) -> Result<usize, &'static str> { 326 + pub fn list_root_files(sd: &SdStorage, buf: &mut [DirEntry]) -> crate::error::Result<usize> { 310 327 poll_once(async { 311 328 let mut guard = borrow(sd)?; 312 329 let inner = &mut *guard; ··· 348 365 ControlFlow::Continue(()) 349 366 }) 350 367 .await 351 - .map_err(|_| "iterate dir failed")?; 368 + .map_err(|_| Error::new(ErrorKind::ReadFailed, "list_root_files"))?; 352 369 353 370 if total > count { 354 371 log::warn!( ··· 362 379 }) 363 380 } 364 381 365 - // directory management 382 + // ── directory management ────────────────────────────────────────── 366 383 367 - pub fn ensure_dir(sd: &SdStorage, name: &str) -> Result<(), &'static str> { 384 + pub fn ensure_dir(sd: &SdStorage, name: &str) -> crate::error::Result<()> { 368 385 // two poll_once calls so the large make_dir future never shares 369 386 // a stack frame with open_dir, halving peak stack usage 370 387 let exists = poll_once(async { ··· 373 390 match inner.mgr.open_dir(inner.root, name).await { 374 391 Ok(dir) => { 375 392 let _ = inner.mgr.close_dir(dir); 376 - Ok::<_, &'static str>(true) 393 + Ok::<_, Error>(true) 377 394 } 378 395 Err(_) => Ok(false), 379 396 } ··· 389 406 match inner.mgr.make_dir_in_dir(inner.root, name).await { 390 407 Ok(()) => Ok(()), 391 408 Err(embedded_sdmmc::Error::DirAlreadyExists) => Ok(()), 392 - Err(_) => Err("make dir failed"), 409 + Err(_) => Err(Error::new(ErrorKind::WriteFailed, "ensure_dir")), 393 410 } 394 411 }) 395 412 } 396 413 397 - // single-directory file operations 414 + // ── single-directory file operations ────────────────────────────── 398 415 399 416 pub fn write_file_in_dir( 400 417 sd: &SdStorage, 401 418 dir: &str, 402 419 name: &str, 403 420 data: &[u8], 404 - ) -> Result<(), &'static str> { 421 + ) -> crate::error::Result<()> { 405 422 poll_once(async { 406 423 let mut guard = borrow(sd)?; 407 424 let inner = &mut *guard; ··· 414 431 dir: &str, 415 432 name: &str, 416 433 data: &[u8], 417 - ) -> Result<(), &'static str> { 434 + ) -> crate::error::Result<()> { 418 435 poll_once(async { 419 436 let mut guard = borrow(sd)?; 420 437 let inner = &mut *guard; ··· 428 445 name: &str, 429 446 offset: u32, 430 447 buf: &mut [u8], 431 - ) -> Result<usize, &'static str> { 448 + ) -> crate::error::Result<usize> { 432 449 poll_once(async { 433 450 let mut guard = borrow(sd)?; 434 451 let inner = &mut *guard; ··· 443 460 dir: &str, 444 461 name: &str, 445 462 buf: &mut [u8], 446 - ) -> Result<(u32, usize), &'static str> { 463 + ) -> crate::error::Result<(u32, usize)> { 447 464 poll_once(async { 448 465 let mut guard = borrow(sd)?; 449 466 let inner = &mut *guard; ··· 451 468 }) 452 469 } 453 470 454 - // async boot path (runs inside the real executor) 471 + // ── async boot path (runs inside the real executor) ─────────────── 455 472 456 - pub async fn ensure_pulp_dir_async(sd: &SdStorage) -> Result<(), &'static str> { 473 + pub async fn ensure_pulp_dir_async(sd: &SdStorage) -> crate::error::Result<()> { 457 474 let mut guard = borrow(sd)?; 458 475 let inner = &mut *guard; 459 476 ··· 467 484 match inner.mgr.make_dir_in_dir(inner.root, PULP_DIR).await { 468 485 Ok(()) => Ok(()), 469 486 Err(embedded_sdmmc::Error::DirAlreadyExists) => Ok(()), 470 - Err(_) => Err("make dir failed"), 487 + Err(_) => Err(Error::new(ErrorKind::WriteFailed, "ensure_pulp_dir_async")), 471 488 } 472 489 } 473 490 474 - // _PULP subdirectory operations 491 + // ── _PULP subdirectory operations ───────────────────────────────── 475 492 476 - pub fn ensure_pulp_subdir(sd: &SdStorage, name: &str) -> Result<(), &'static str> { 493 + pub fn ensure_pulp_subdir(sd: &SdStorage, name: &str) -> crate::error::Result<()> { 477 494 let exists = poll_once(async { 478 495 let mut guard = borrow(sd)?; 479 496 let inner = &mut *guard; ··· 481 498 match inner.mgr.open_dir(pulp_h, name).await { 482 499 Ok(sub) => { 483 500 let _ = inner.mgr.close_dir(sub); 484 - Ok::<_, &'static str>(true) 501 + Ok::<_, Error>(true) 485 502 } 486 503 Err(_) => Ok(false), 487 504 } ··· 497 514 let inner = &mut *guard; 498 515 in_dir!(inner, PULP_DIR, |pulp_h| { 499 516 match inner.mgr.make_dir_in_dir(pulp_h, name).await { 500 - Ok(()) => Ok::<_, &'static str>(()), 517 + Ok(()) => Ok::<_, Error>(()), 501 518 Err(embedded_sdmmc::Error::DirAlreadyExists) => Ok(()), 502 - Err(_) => Err("make dir failed"), 519 + Err(_) => Err(Error::new(ErrorKind::WriteFailed, "ensure_pulp_subdir")), 503 520 } 504 521 }) 505 522 }) ··· 510 527 dir: &str, 511 528 name: &str, 512 529 data: &[u8], 513 - ) -> Result<(), &'static str> { 530 + ) -> crate::error::Result<()> { 514 531 poll_once(async { 515 532 let mut guard = borrow(sd)?; 516 533 let inner = &mut *guard; ··· 525 542 dir: &str, 526 543 name: &str, 527 544 data: &[u8], 528 - ) -> Result<(), &'static str> { 545 + ) -> crate::error::Result<()> { 529 546 poll_once(async { 530 547 let mut guard = borrow(sd)?; 531 548 let inner = &mut *guard; ··· 541 558 name: &str, 542 559 offset: u32, 543 560 buf: &mut [u8], 544 - ) -> Result<usize, &'static str> { 561 + ) -> crate::error::Result<usize> { 545 562 poll_once(async { 546 563 let mut guard = borrow(sd)?; 547 564 let inner = &mut *guard; ··· 555 572 sd: &SdStorage, 556 573 dir: &str, 557 574 name: &str, 558 - ) -> Result<u32, &'static str> { 575 + ) -> crate::error::Result<u32> { 559 576 poll_once(async { 560 577 let mut guard = borrow(sd)?; 561 578 let inner = &mut *guard; ··· 565 582 }) 566 583 } 567 584 568 - pub fn delete_in_pulp_subdir(sd: &SdStorage, dir: &str, name: &str) -> Result<(), &'static str> { 585 + pub fn delete_in_pulp_subdir(sd: &SdStorage, dir: &str, name: &str) -> crate::error::Result<()> { 569 586 poll_once(async { 570 587 let mut guard = borrow(sd)?; 571 588 let inner = &mut *guard; ··· 573 590 }) 574 591 } 575 592 576 - // append a title mapping line to _PULP/TITLES.BIN 577 - pub fn save_title(sd: &SdStorage, filename: &str, title: &str) -> Result<(), &'static str> { 593 + // ── title mapping ───────────────────────────────────────────────── 594 + 595 + /// Append a title mapping line to _PULP/TITLES.BIN 596 + pub fn save_title(sd: &SdStorage, filename: &str, title: &str) -> crate::error::Result<()> { 578 597 let name_bytes = filename.as_bytes(); 579 598 let title_bytes = title.as_bytes(); 580 599 let title_len = title_bytes.len().min(TITLE_CAP); 581 600 let line_len = name_bytes.len() + 1 + title_len + 1; // name + \t + title + \n 582 601 if line_len > 128 { 583 - return Err("title line too long"); 602 + return Err(Error::new( 603 + ErrorKind::WriteFailed, 604 + "save_title: line too long", 605 + )); 584 606 } 585 607 let mut line = [0u8; 128]; 586 608 line[..name_bytes.len()].copy_from_slice(name_bytes);
+408
kernel/src/error.rs
··· 1 + // Unified error type for pulp-os 2 + // 3 + // Replaces the flat `StorageError` enum and ad-hoc `&'static str` 4 + // errors with a single `Copy` type that carries: 5 + // 6 + // ErrorKind — *what* went wrong (storage, parse, resource …) 7 + // source — *where* it happened (`&'static str`, usually 8 + // module_path!() or a short caller-supplied tag) 9 + // 10 + // Every `Result` in the kernel and app layers should use this type. 11 + // The smol-epub trait boundary (`Result<T, &'static str>`) converts 12 + // at the edge via the `From` impls. 13 + 14 + use core::fmt; 15 + 16 + // --------------------------------------------------------------------------- 17 + // ErrorKind — the category of failure 18 + // --------------------------------------------------------------------------- 19 + 20 + /// What went wrong. 21 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 22 + #[non_exhaustive] 23 + pub enum ErrorKind { 24 + // -- storage / SD card -- 25 + /// SD card not inserted or not responding. 26 + NoCard, 27 + /// Could not open the FAT volume. 28 + OpenVolume, 29 + /// Could not open a directory. 30 + OpenDir, 31 + /// Could not open a file. 32 + OpenFile, 33 + /// Read I/O failed. 34 + ReadFailed, 35 + /// Write I/O failed. 36 + WriteFailed, 37 + /// Seek within a file failed. 38 + SeekFailed, 39 + /// Delete operation failed. 40 + DeleteFailed, 41 + /// Directory is full (cannot create entry). 42 + DirFull, 43 + /// File or directory not found. 44 + NotFound, 45 + 46 + // -- data / parsing -- 47 + /// EPUB, ZIP, or similar structure is invalid. 48 + ParseFailed, 49 + /// Data is malformed or unexpected. 50 + InvalidData, 51 + /// UTF-8 or other text-encoding error. 52 + BadEncoding, 53 + 54 + // -- resources -- 55 + /// Heap allocation failed. 56 + OutOfMemory, 57 + /// Supplied buffer is too small for the operation. 58 + BufferTooSmall, 59 + 60 + // -- network (upload) -- 61 + /// Network read/write failed. 62 + NetworkIo, 63 + /// Protocol-level error (HTTP, multipart, etc.). 64 + Protocol, 65 + 66 + // -- catch-all -- 67 + /// Unclassified error (carries context in `source`). 68 + Other, 69 + } 70 + 71 + impl ErrorKind { 72 + /// Short human-readable label (suitable for UI and log lines). 73 + pub const fn as_str(self) -> &'static str { 74 + match self { 75 + Self::NoCard => "no sd card", 76 + Self::OpenVolume => "open volume failed", 77 + Self::OpenDir => "open dir failed", 78 + Self::OpenFile => "open file failed", 79 + Self::ReadFailed => "read failed", 80 + Self::WriteFailed => "write failed", 81 + Self::SeekFailed => "seek failed", 82 + Self::DeleteFailed => "delete failed", 83 + Self::DirFull => "directory full", 84 + Self::NotFound => "not found", 85 + Self::ParseFailed => "parse failed", 86 + Self::InvalidData => "invalid data", 87 + Self::BadEncoding => "bad encoding", 88 + Self::OutOfMemory => "out of memory", 89 + Self::BufferTooSmall => "buffer too small", 90 + Self::NetworkIo => "network error", 91 + Self::Protocol => "protocol error", 92 + Self::Other => "error", 93 + } 94 + } 95 + 96 + /// True for any variant that originates from SD-card storage I/O. 97 + pub const fn is_storage(self) -> bool { 98 + matches!( 99 + self, 100 + Self::NoCard 101 + | Self::OpenVolume 102 + | Self::OpenDir 103 + | Self::OpenFile 104 + | Self::ReadFailed 105 + | Self::WriteFailed 106 + | Self::SeekFailed 107 + | Self::DeleteFailed 108 + | Self::DirFull 109 + | Self::NotFound 110 + ) 111 + } 112 + } 113 + 114 + impl fmt::Display for ErrorKind { 115 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 116 + f.write_str(self.as_str()) 117 + } 118 + } 119 + 120 + // --------------------------------------------------------------------------- 121 + // Error — the unified error value 122 + // --------------------------------------------------------------------------- 123 + 124 + /// Unified error for the entire pulp-os stack. 125 + /// 126 + /// Cheap to copy (one discriminant byte + one `&'static str` pointer). 127 + /// Carries *what* failed ([`ErrorKind`]) and a compile-time *source* 128 + /// string that identifies the call-site or subsystem. 129 + /// 130 + /// # Constructing 131 + /// 132 + /// ```ignore 133 + /// // Constant shorthand (no source tag): 134 + /// Error::READ_FAILED 135 + /// 136 + /// // With explicit source: 137 + /// Error::new(ErrorKind::OpenFile, "epub_init_zip") 138 + /// 139 + /// // Via the err!() macro (auto-stamps module_path!()): 140 + /// err!(ReadFailed) 141 + /// err!(OpenFile, "epub_init_zip") 142 + /// ``` 143 + #[derive(Clone, Copy)] 144 + pub struct Error { 145 + kind: ErrorKind, 146 + /// Where the error was created — a `module_path!()` or free-form 147 + /// tag. Empty string when no source was attached. 148 + source: &'static str, 149 + } 150 + 151 + // -- construction ---------------------------------------------------------- 152 + 153 + impl Error { 154 + /// Create an error with explicit kind and source tag. 155 + #[inline] 156 + pub const fn new(kind: ErrorKind, source: &'static str) -> Self { 157 + Self { kind, source } 158 + } 159 + 160 + /// Create an error from a kind alone (no source context). 161 + #[inline] 162 + pub const fn from_kind(kind: ErrorKind) -> Self { 163 + Self { kind, source: "" } 164 + } 165 + 166 + // Named constants that mirror the old `StorageError` variants so 167 + // existing match-arms keep compiling during migration. 168 + 169 + pub const NO_CARD: Self = Self::from_kind(ErrorKind::NoCard); 170 + pub const OPEN_VOLUME: Self = Self::from_kind(ErrorKind::OpenVolume); 171 + pub const OPEN_DIR: Self = Self::from_kind(ErrorKind::OpenDir); 172 + pub const OPEN_FILE: Self = Self::from_kind(ErrorKind::OpenFile); 173 + pub const READ_FAILED: Self = Self::from_kind(ErrorKind::ReadFailed); 174 + pub const WRITE_FAILED: Self = Self::from_kind(ErrorKind::WriteFailed); 175 + pub const SEEK_FAILED: Self = Self::from_kind(ErrorKind::SeekFailed); 176 + pub const DELETE_FAILED: Self = Self::from_kind(ErrorKind::DeleteFailed); 177 + pub const DIR_FULL: Self = Self::from_kind(ErrorKind::DirFull); 178 + pub const NOT_FOUND: Self = Self::from_kind(ErrorKind::NotFound); 179 + } 180 + 181 + // -- accessors ------------------------------------------------------------- 182 + 183 + impl Error { 184 + /// The failure category. 185 + #[inline] 186 + pub const fn kind(&self) -> ErrorKind { 187 + self.kind 188 + } 189 + 190 + /// The compile-time tag identifying where this error was created. 191 + /// Returns `""` when no source was attached. 192 + #[inline] 193 + pub const fn source_tag(&self) -> &'static str { 194 + self.source 195 + } 196 + 197 + /// Attach (or replace) the source tag. Useful when propagating 198 + /// an error upward and adding the caller's context. 199 + #[inline] 200 + pub const fn with_source(self, source: &'static str) -> Self { 201 + Self { 202 + kind: self.kind, 203 + source, 204 + } 205 + } 206 + 207 + /// Change the kind while keeping the source. 208 + #[inline] 209 + pub const fn with_kind(self, kind: ErrorKind) -> Self { 210 + Self { 211 + kind, 212 + source: self.source, 213 + } 214 + } 215 + 216 + /// True when a source tag has been attached. 217 + #[inline] 218 + pub const fn has_source(&self) -> bool { 219 + !self.source.is_empty() 220 + } 221 + 222 + /// True when the error originates from storage I/O. 223 + #[inline] 224 + pub const fn is_storage(&self) -> bool { 225 + self.kind.is_storage() 226 + } 227 + 228 + /// Short label for the smol-epub `Result<T, &'static str>` boundary. 229 + #[inline] 230 + pub const fn as_str(&self) -> &'static str { 231 + self.kind.as_str() 232 + } 233 + } 234 + 235 + // -- formatting ------------------------------------------------------------ 236 + 237 + impl fmt::Debug for Error { 238 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 239 + if self.source.is_empty() { 240 + write!(f, "Error({:?})", self.kind) 241 + } else { 242 + write!(f, "Error({:?} @ {:?})", self.kind, self.source) 243 + } 244 + } 245 + } 246 + 247 + impl fmt::Display for Error { 248 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 249 + if self.source.is_empty() { 250 + f.write_str(self.kind.as_str()) 251 + } else { 252 + write!(f, "{} [{}]", self.kind.as_str(), self.source) 253 + } 254 + } 255 + } 256 + 257 + // -- equality (semantic: kind only, source is diagnostic) ------------------ 258 + 259 + impl PartialEq for Error { 260 + #[inline] 261 + fn eq(&self, other: &Self) -> bool { 262 + self.kind == other.kind 263 + } 264 + } 265 + 266 + impl Eq for Error {} 267 + 268 + // --------------------------------------------------------------------------- 269 + // Conversions 270 + // --------------------------------------------------------------------------- 271 + 272 + /// Wrap a bare `&'static str` (from smol-epub helpers, etc.) into an 273 + /// [`Error`]. Well-known strings are mapped to the appropriate kind; 274 + /// everything else becomes [`ErrorKind::Other`] with the original 275 + /// string preserved as the source tag. 276 + impl From<&'static str> for Error { 277 + #[inline] 278 + fn from(msg: &'static str) -> Self { 279 + let kind = match msg { 280 + "read failed" | "read local header failed" => ErrorKind::ReadFailed, 281 + "write failed" => ErrorKind::WriteFailed, 282 + "read error" | "read error during upload" => ErrorKind::NetworkIo, 283 + "no sd card" => ErrorKind::NoCard, 284 + "not found" | "OPF not found" | "no filename in upload" => ErrorKind::NotFound, 285 + "too small" | "CD truncated" | "cache file too small" => ErrorKind::InvalidData, 286 + "CD too large" | "OOM for cached image" => ErrorKind::OutOfMemory, 287 + "bad OPF path" | "bad encoding" | "filename encoding error" => ErrorKind::BadEncoding, 288 + "parse failed" | "no title in OPF" => ErrorKind::ParseFailed, 289 + "boundary too long" 290 + | "part headers too large" 291 + | "invalid filename" 292 + | "upload incomplete" 293 + | "connection closed during headers" => ErrorKind::Protocol, 294 + _ => ErrorKind::Other, 295 + }; 296 + Self { kind, source: msg } 297 + } 298 + } 299 + 300 + /// Project back to `&'static str` for the smol-epub trait boundary. 301 + impl From<Error> for &'static str { 302 + #[inline] 303 + fn from(e: Error) -> &'static str { 304 + // Prefer the source tag if it is a meaningful human string; 305 + // otherwise fall back to the kind label. 306 + if e.source.is_empty() { 307 + e.kind.as_str() 308 + } else { 309 + e.source 310 + } 311 + } 312 + } 313 + 314 + // --------------------------------------------------------------------------- 315 + // ResultExt — ergonomic source tagging on Results 316 + // --------------------------------------------------------------------------- 317 + 318 + /// Extension trait for stamping source context onto any 319 + /// `Result<T, Error>`. 320 + /// 321 + /// ```ignore 322 + /// storage::read_file_chunk(sd, name, off, buf) 323 + /// .source("epub_init_zip")?; 324 + /// ``` 325 + pub trait ResultExt<T> { 326 + /// Attach a source tag to the error (if any). 327 + fn source(self, src: &'static str) -> Result<T>; 328 + 329 + /// Replace the error kind while adding a source tag. 330 + fn map_kind(self, kind: ErrorKind, src: &'static str) -> Result<T>; 331 + } 332 + 333 + impl<T> ResultExt<T> for Result<T> { 334 + #[inline] 335 + fn source(self, src: &'static str) -> Result<T> { 336 + self.map_err(|e| e.with_source(src)) 337 + } 338 + 339 + #[inline] 340 + fn map_kind(self, kind: ErrorKind, src: &'static str) -> Result<T> { 341 + self.map_err(|_| Error::new(kind, src)) 342 + } 343 + } 344 + 345 + /// Blanket impl so `Result<T, &'static str>` (smol-epub returns) can 346 + /// be tagged and converted in one step. 347 + impl<T> ResultExt<T> for core::result::Result<T, &'static str> { 348 + #[inline] 349 + fn source(self, src: &'static str) -> Result<T> { 350 + self.map_err(|msg| Error::from(msg).with_source(src)) 351 + } 352 + 353 + #[inline] 354 + fn map_kind(self, kind: ErrorKind, src: &'static str) -> Result<T> { 355 + self.map_err(|_| Error::new(kind, src)) 356 + } 357 + } 358 + 359 + // --------------------------------------------------------------------------- 360 + // err! macro — stamps module_path!() automatically 361 + // --------------------------------------------------------------------------- 362 + 363 + /// Create an [`Error`] with the caller's module path baked in. 364 + /// 365 + /// ```ignore 366 + /// // Kind only — source is the calling module's path: 367 + /// err!(ReadFailed) 368 + /// 369 + /// // Kind + explicit context string: 370 + /// err!(OpenFile, "epub_init_zip") 371 + /// ``` 372 + #[macro_export] 373 + macro_rules! err { 374 + ($kind:ident) => { 375 + $crate::error::Error::new($crate::error::ErrorKind::$kind, module_path!()) 376 + }; 377 + ($kind:ident, $src:expr) => { 378 + $crate::error::Error::new($crate::error::ErrorKind::$kind, $src) 379 + }; 380 + } 381 + 382 + /// Map any `Result<T, _>` error into an [`Error`] of the given kind, 383 + /// stamping the caller's module path. 384 + /// 385 + /// ```ignore 386 + /// mgr.read(file, buf).await.or_err!(ReadFailed)?; 387 + /// ``` 388 + #[macro_export] 389 + macro_rules! or_err { 390 + ($result:expr, $kind:ident) => { 391 + ($result) 392 + .map_err(|_| $crate::error::Error::new($crate::error::ErrorKind::$kind, module_path!())) 393 + }; 394 + ($result:expr, $kind:ident, $src:expr) => { 395 + ($result).map_err(|_| $crate::error::Error::new($crate::error::ErrorKind::$kind, $src)) 396 + }; 397 + } 398 + 399 + // --------------------------------------------------------------------------- 400 + // Result alias 401 + // --------------------------------------------------------------------------- 402 + 403 + /// Convenience alias used throughout pulp-os. 404 + /// 405 + /// Intentionally shadows `core::result::Result` only when imported 406 + /// unqualified — callers that need the two-param form can still write 407 + /// `core::result::Result<T, E>`. 408 + pub type Result<T> = core::result::Result<T, Error>;
+53 -7
kernel/src/kernel/app.rs
··· 1 - // app protocol: trait, context, transitions, and redraw types 1 + // app protocol: trait, context, transitions, redraw types, and coalescing 2 2 // 3 3 // these types define the contract between the kernel scheduler and 4 4 // the app layer. concrete apps implement the App trait; the kernel ··· 12 12 // what actions an app exposes. the renderer (QuickMenu widget) is 13 13 // app-side, but the protocol is kernel-side. 14 14 15 + use embassy_time::Instant; 15 16 use esp_hal::delay::Delay; 16 17 17 18 use crate::board::Epd; ··· 107 108 msg_buf: [u8; MSG_BUF_SIZE], 108 109 msg_len: usize, 109 110 redraw: Redraw, 111 + coalesce_until: Option<Instant>, 112 + immediate: bool, 110 113 } 111 114 112 115 impl Default for AppContext { ··· 121 124 msg_buf: [0u8; MSG_BUF_SIZE], 122 125 msg_len: 0, 123 126 redraw: Redraw::None, 127 + coalesce_until: None, 128 + immediate: false, 124 129 } 125 130 } 126 131 ··· 156 161 } 157 162 } 158 163 164 + // mark dirty and render on next tick; the default for all callers 159 165 #[inline] 160 166 pub fn mark_dirty(&mut self, region: Region) { 161 167 self.request_partial_redraw(region); 168 + self.immediate = true; 169 + self.coalesce_until = None; 170 + } 171 + 172 + // alias; kept so callers in on_event that were already converted 173 + // continue to compile without churn 174 + #[inline] 175 + pub fn mark_dirty_immediate(&mut self, region: Region) { 176 + self.mark_dirty(region); 177 + } 178 + 179 + // mark dirty with 50ms coalescing window; use only for background 180 + // batch updates (title scanner) where many rapid dirty marks 181 + // should coalesce into a single refresh 182 + #[inline] 183 + pub fn mark_dirty_coalesced(&mut self, region: Region) { 184 + self.request_partial_redraw(region); 185 + if !self.immediate && self.coalesce_until.is_none() { 186 + self.coalesce_until = Some(Instant::now() + embassy_time::Duration::from_millis(50)); 187 + } 162 188 } 163 189 164 190 pub fn has_redraw(&self) -> bool { 165 191 !matches!(self.redraw, Redraw::None) 166 192 } 167 193 194 + // true when a pending redraw is ready to render 195 + pub fn render_ready(&self) -> bool { 196 + match self.redraw { 197 + Redraw::None => false, 198 + Redraw::Full => true, 199 + Redraw::Partial(_) => { 200 + self.immediate 201 + || self 202 + .coalesce_until 203 + .map(|t| Instant::now() >= t) 204 + .unwrap_or(true) 205 + } 206 + } 207 + } 208 + 168 209 pub fn take_redraw(&mut self) -> Redraw { 169 210 let r = self.redraw; 170 211 self.redraw = Redraw::None; 212 + self.coalesce_until = None; 213 + self.immediate = false; 171 214 r 172 215 } 173 216 } 174 217 218 + // background is async for epub streaming (stream_strip_entry_async); 219 + // other app impls compile to immediately-ready futures 175 220 #[allow(async_fn_in_trait)] 176 221 pub trait App<Id> { 177 - async fn on_enter(&mut self, ctx: &mut AppContext, k: &mut KernelHandle<'_>); 222 + fn on_enter(&mut self, ctx: &mut AppContext, k: &mut KernelHandle<'_>); 178 223 179 224 fn on_exit(&mut self) {} 180 225 ··· 182 227 self.on_exit(); 183 228 } 184 229 185 - async fn on_resume(&mut self, ctx: &mut AppContext, k: &mut KernelHandle<'_>) { 186 - self.on_enter(ctx, k).await; 230 + fn on_resume(&mut self, ctx: &mut AppContext, k: &mut KernelHandle<'_>) { 231 + self.on_enter(ctx, k); 187 232 } 188 233 189 234 fn on_event(&mut self, event: ActionEvent, ctx: &mut AppContext) -> Transition<Id>; ··· 312 357 // scheduler is generic over AppLayer without importing any concrete 313 358 // app types. 314 359 360 + // run_special_mode is genuinely async (wifi radio); the rest is sync 315 361 #[allow(async_fn_in_trait)] 316 362 pub trait AppLayer { 317 363 type Id: AppIdType; ··· 319 365 // active app and event dispatch 320 366 fn active(&self) -> Self::Id; 321 367 fn dispatch_event(&mut self, event: Event, bm: &mut BookmarkCache) -> Transition<Self::Id>; 322 - async fn apply_transition(&mut self, t: Transition<Self::Id>, k: &mut KernelHandle<'_>); 368 + fn apply_transition(&mut self, t: Transition<Self::Id>, k: &mut KernelHandle<'_>); 323 369 324 - // background work (SD I/O, caching) -- runs before render 370 + // background work (SD I/O, caching); async for epub streaming 325 371 async fn run_background(&mut self, k: &mut KernelHandle<'_>); 326 372 327 373 // rendering ··· 340 386 // boot-time init: load settings, populate caches, enter first app 341 387 fn load_eager_settings(&mut self, k: &mut KernelHandle<'_>); 342 388 fn load_initial_state(&mut self, k: &mut KernelHandle<'_>); 343 - async fn enter_initial(&mut self, k: &mut KernelHandle<'_>); 389 + fn enter_initial(&mut self, k: &mut KernelHandle<'_>); 344 390 345 391 // true when the active app wants to take over the main loop 346 392 // (e.g. wifi upload mode bypasses the normal event dispatch)
+2 -1
kernel/src/kernel/dir_cache.rs
··· 5 5 use crate::drivers::storage::{ 6 6 DirEntry, DirPage, PULP_DIR, TITLES_FILE, list_root_files, read_file_start_in_dir, 7 7 }; 8 + use crate::error::Result; 8 9 9 10 const MAX_DIR_ENTRIES: usize = 128; 10 11 ··· 29 30 } 30 31 } 31 32 32 - pub fn ensure_loaded(&mut self, sd: &SdStorage) -> Result<(), &'static str> { 33 + pub fn ensure_loaded(&mut self, sd: &SdStorage) -> Result<()> { 33 34 if self.valid { 34 35 return Ok(()); 35 36 }
+58 -312
kernel/src/kernel/handle.rs
··· 1 - // kernel handle: async syscall boundary for apps. 1 + // kernel handle: synchronous syscall boundary for apps 2 2 // 3 - // every storage method does a synchronous operation then yields to 4 - // the executor, giving other tasks a scheduling opportunity. 3 + // every storage method calls a single storage::* function and returns 4 + // the unified Error result. apps call these directly. 5 5 // 6 6 // app-specific logic (bookmarks, title scan, etc.) accesses the 7 7 // underlying caches directly via bookmark_cache() / dir_cache_mut() 8 8 // rather than through dedicated handle methods. 9 9 10 10 use crate::drivers::storage::{self, DirEntry, DirPage}; 11 + use crate::error::{Error, Result}; 11 12 use crate::kernel::bookmarks::BookmarkCache; 12 13 use crate::kernel::dir_cache::DirCache; 13 14 use crate::kernel::wake::uptime_secs; 14 15 15 - // hides SPI generics from app code; detailed diagnostics go to log::warn 16 - #[derive(Debug, Clone, Copy, PartialEq, Eq)] 17 - pub enum StorageError { 18 - OpenVolume, 19 - OpenDir, 20 - OpenFile, 21 - ReadFailed, 22 - WriteFailed, 23 - SeekFailed, 24 - DeleteFailed, 25 - DirFull, 26 - NotFound, 27 - } 28 - 29 - impl core::fmt::Display for StorageError { 30 - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 31 - match self { 32 - Self::OpenVolume => write!(f, "open volume failed"), 33 - Self::OpenDir => write!(f, "open dir failed"), 34 - Self::OpenFile => write!(f, "open file failed"), 35 - Self::ReadFailed => write!(f, "read failed"), 36 - Self::WriteFailed => write!(f, "write failed"), 37 - Self::SeekFailed => write!(f, "seek failed"), 38 - Self::DeleteFailed => write!(f, "delete failed"), 39 - Self::DirFull => write!(f, "directory full"), 40 - Self::NotFound => write!(f, "not found"), 41 - } 42 - } 43 - } 44 - 45 - // yield to executor after a synchronous storage call 46 - macro_rules! yield_op { 47 - ($e:expr) => {{ 48 - let r = $e; 49 - embassy_futures::yield_now().await; 50 - r 51 - }}; 52 - } 53 - 54 - fn map_read_err(e: &'static str) -> StorageError { 55 - if e.contains("volume") { 56 - StorageError::OpenVolume 57 - } else if e.contains("dir") { 58 - StorageError::OpenDir 59 - } else if e.contains("open file") { 60 - StorageError::OpenFile 61 - } else if e.contains("seek") { 62 - StorageError::SeekFailed 63 - } else { 64 - StorageError::ReadFailed 65 - } 66 - } 67 - 68 - fn map_write_err(e: &'static str) -> StorageError { 69 - if e.contains("volume") { 70 - StorageError::OpenVolume 71 - } else if e.contains("dir") && !e.contains("make") { 72 - StorageError::OpenDir 73 - } else if e.contains("make dir") { 74 - StorageError::WriteFailed 75 - } else if e.contains("open file") || e.contains("create") { 76 - StorageError::OpenFile 77 - } else { 78 - StorageError::WriteFailed 79 - } 80 - } 81 - 82 - // async API surface for apps -- the syscall boundary 16 + // synchronous API surface for apps 83 17 // 84 18 // borrows the Kernel for the duration of an app lifecycle method; 85 19 // no SPI, no generics, no driver types visible to apps ··· 92 26 Self { kernel } 93 27 } 94 28 95 - // root file operations 96 - 97 - pub async fn file_size(&mut self, name: &str) -> Result<u32, StorageError> { 98 - yield_op!(self.sync_file_size(name).map_err(map_read_err)) 99 - } 100 - 101 - pub async fn read_file_chunk( 102 - &mut self, 103 - name: &str, 104 - offset: u32, 105 - buf: &mut [u8], 106 - ) -> Result<usize, StorageError> { 107 - yield_op!( 108 - self.sync_read_chunk(name, offset, buf) 109 - .map_err(map_read_err) 110 - ) 111 - } 112 - 113 - pub async fn read_file_start( 114 - &mut self, 115 - name: &str, 116 - buf: &mut [u8], 117 - ) -> Result<(u32, usize), StorageError> { 118 - yield_op!(self.sync_read_file_start(name, buf).map_err(map_read_err)) 119 - } 120 - 121 - pub async fn write_file(&mut self, name: &str, data: &[u8]) -> Result<(), StorageError> { 122 - yield_op!(storage::write_file(&self.kernel.sd, name, data).map_err(map_write_err)) 123 - } 124 - 125 - pub async fn delete_file(&mut self, name: &str) -> Result<(), StorageError> { 126 - yield_op!( 127 - storage::delete_file(&self.kernel.sd, name).map_err(|_| StorageError::DeleteFailed) 128 - ) 129 - } 130 - 131 - // directory listing (cached) 132 - 133 - pub async fn list_dir( 134 - &mut self, 135 - offset: usize, 136 - buf: &mut [DirEntry], 137 - ) -> Result<DirPage, StorageError> { 138 - { 139 - let k = &mut *self.kernel; 140 - k.dir_cache.ensure_loaded(&k.sd).map_err(map_read_err)?; 141 - } 142 - let page = self.kernel.dir_cache.page(offset, buf); 143 - embassy_futures::yield_now().await; 144 - Ok(page) 145 - } 146 - 147 - pub fn invalidate_dir_cache(&mut self) { 148 - self.kernel.dir_cache.invalidate(); 149 - } 150 - 151 - // _PULP app-data directory 152 - 153 - pub async fn ensure_app_dir(&mut self) -> Result<(), StorageError> { 154 - yield_op!(storage::ensure_dir(&self.kernel.sd, storage::PULP_DIR).map_err(map_write_err)) 155 - } 156 - 157 - pub async fn read_app_data_start( 158 - &mut self, 159 - name: &str, 160 - buf: &mut [u8], 161 - ) -> Result<(u32, usize), StorageError> { 162 - yield_op!( 163 - self.sync_read_app_data_start(name, buf) 164 - .map_err(map_read_err) 165 - ) 166 - } 167 - 168 - pub async fn read_app_data( 169 - &mut self, 170 - name: &str, 171 - offset: u32, 172 - buf: &mut [u8], 173 - ) -> Result<usize, StorageError> { 174 - yield_op!( 175 - storage::read_file_chunk_in_dir(&self.kernel.sd, storage::PULP_DIR, name, offset, buf) 176 - .map_err(map_read_err) 177 - ) 178 - } 179 - 180 - pub async fn write_app_data(&mut self, name: &str, data: &[u8]) -> Result<(), StorageError> { 181 - yield_op!(self.sync_write_app_data(name, data).map_err(map_write_err)) 182 - } 183 - 184 - pub async fn ensure_app_subdir(&mut self, dir: &str) -> Result<(), StorageError> { 185 - yield_op!(self.sync_ensure_app_subdir(dir).map_err(map_write_err)) 186 - } 187 - 188 - pub async fn read_app_subdir( 189 - &mut self, 190 - dir: &str, 191 - name: &str, 192 - offset: u32, 193 - buf: &mut [u8], 194 - ) -> Result<usize, StorageError> { 195 - yield_op!( 196 - self.sync_read_app_subdir_chunk(dir, name, offset, buf) 197 - .map_err(map_read_err) 198 - ) 199 - } 200 - 201 - pub async fn write_app_subdir( 202 - &mut self, 203 - dir: &str, 204 - name: &str, 205 - data: &[u8], 206 - ) -> Result<(), StorageError> { 207 - yield_op!( 208 - self.sync_write_app_subdir(dir, name, data) 209 - .map_err(map_write_err) 210 - ) 211 - } 212 - 213 - pub async fn append_app_subdir( 214 - &mut self, 215 - dir: &str, 216 - name: &str, 217 - data: &[u8], 218 - ) -> Result<(), StorageError> { 219 - yield_op!( 220 - self.sync_append_app_subdir(dir, name, data) 221 - .map_err(map_write_err) 222 - ) 223 - } 224 - 225 - pub async fn file_size_app_subdir( 226 - &mut self, 227 - dir: &str, 228 - name: &str, 229 - ) -> Result<u32, StorageError> { 230 - yield_op!( 231 - self.sync_file_size_app_subdir(dir, name) 232 - .map_err(map_read_err) 233 - ) 234 - } 235 - 236 - pub async fn delete_app_subdir(&mut self, dir: &str, name: &str) -> Result<(), StorageError> { 237 - yield_op!( 238 - storage::delete_in_pulp_subdir(&self.kernel.sd, dir, name) 239 - .map_err(|_| StorageError::DeleteFailed) 240 - ) 241 - } 242 - 243 - // arbitrary subdirectory reads (non-_PULP) 244 - 245 - pub async fn read_chunk_in_dir( 246 - &mut self, 247 - dir: &str, 248 - name: &str, 249 - offset: u32, 250 - buf: &mut [u8], 251 - ) -> Result<usize, StorageError> { 252 - let result = storage::read_file_chunk_in_dir(&self.kernel.sd, dir, name, offset, buf) 253 - .map_err(map_read_err); 254 - embassy_futures::yield_now().await; 255 - result 256 - } 257 - 258 - // SD card health 259 - 260 - pub async fn check_sd(&mut self) -> bool { 261 - let ok = self.kernel.sd.probe_ok(); 262 - self.kernel.sd_ok = ok; 263 - yield_op!(ok) 264 - } 265 - 266 - // system info (sync, no I/O) 267 - 268 - #[inline] 269 - pub fn battery_mv(&self) -> u16 { 270 - self.kernel.cached_battery_mv 271 - } 272 - 273 - #[inline] 274 - pub fn uptime_secs(&self) -> u32 { 275 - uptime_secs() 276 - } 277 - 278 - #[inline] 279 - pub fn sd_ok(&self) -> bool { 280 - self.kernel.sd_ok 281 - } 282 - 283 29 // smol-epub sync reader bridge 284 30 // 285 - // smol-epub performs I/O through closures that cannot be async; 286 - // this provides a scoped sync reader that completes before 287 - // returning -- no borrows held across any .await point 31 + // smol-epub performs I/O through closures that return 32 + // Result<usize, &'static str>; these adapters convert 33 + // Error → &'static str at the boundary via the From impl. 288 34 289 35 pub fn with_sync_reader<F, R>(&mut self, f: F) -> R 290 36 where 291 - F: FnOnce(&mut dyn FnMut(&str, u32, &mut [u8]) -> Result<usize, &'static str>) -> R, 37 + F: FnOnce( 38 + &mut dyn FnMut(&str, u32, &mut [u8]) -> core::result::Result<usize, &'static str>, 39 + ) -> R, 292 40 { 293 41 let sd = &self.kernel.sd; 294 42 let mut reader = |name: &str, offset: u32, buf: &mut [u8]| { 295 43 storage::read_file_chunk(sd, name, offset, buf) 44 + .map_err(|e: Error| -> &'static str { e.into() }) 296 45 }; 297 46 f(&mut reader) 298 47 } 299 48 300 49 pub fn with_sync_reader_app_subdir<F, R>(&mut self, dir: &str, f: F) -> R 301 50 where 302 - F: FnOnce(&mut dyn FnMut(&str, u32, &mut [u8]) -> Result<usize, &'static str>) -> R, 51 + F: FnOnce( 52 + &mut dyn FnMut(&str, u32, &mut [u8]) -> core::result::Result<usize, &'static str>, 53 + ) -> R, 303 54 { 304 55 let sd = &self.kernel.sd; 305 56 let mut reader = |name: &str, offset: u32, buf: &mut [u8]| { 306 57 storage::read_chunk_in_pulp_subdir(sd, dir, name, offset, buf) 58 + .map_err(|e: Error| -> &'static str { e.into() }) 307 59 }; 308 60 f(&mut reader) 309 61 } 310 62 311 - // synchronous storage primitives 63 + // storage primitives 312 64 // 313 - // each calls a single storage::* function and returns the raw 314 - // &'static str error; the async methods above delegate to these 315 - // adding map_err + yield_now 65 + // each calls a single storage::* function; return type is 66 + // Result<T> (unified Error) throughout 316 67 317 68 #[inline] 318 - pub fn sync_file_size(&mut self, name: &str) -> Result<u32, &'static str> { 69 + pub fn file_size(&mut self, name: &str) -> Result<u32> { 319 70 storage::file_size(&self.kernel.sd, name) 320 71 } 321 72 322 73 #[inline] 323 - pub fn sync_read_chunk( 324 - &mut self, 325 - name: &str, 326 - offset: u32, 327 - buf: &mut [u8], 328 - ) -> Result<usize, &'static str> { 74 + pub fn read_chunk(&mut self, name: &str, offset: u32, buf: &mut [u8]) -> Result<usize> { 329 75 storage::read_file_chunk(&self.kernel.sd, name, offset, buf) 330 76 } 331 77 332 78 #[inline] 333 - pub fn sync_read_file_start( 334 - &mut self, 335 - name: &str, 336 - buf: &mut [u8], 337 - ) -> Result<(u32, usize), &'static str> { 79 + pub fn read_file_start(&mut self, name: &str, buf: &mut [u8]) -> Result<(u32, usize)> { 338 80 storage::read_file_start(&self.kernel.sd, name, buf) 339 81 } 340 82 341 83 #[inline] 342 - pub fn sync_save_title(&mut self, filename: &str, title: &str) -> Result<(), &'static str> { 84 + pub fn save_title(&mut self, filename: &str, title: &str) -> Result<()> { 343 85 storage::save_title(&self.kernel.sd, filename, title) 344 86 } 345 87 346 88 #[inline] 347 - pub fn sync_read_app_data_start( 348 - &mut self, 349 - name: &str, 350 - buf: &mut [u8], 351 - ) -> Result<(u32, usize), &'static str> { 89 + pub fn read_app_data_start(&mut self, name: &str, buf: &mut [u8]) -> Result<(u32, usize)> { 352 90 storage::read_file_start_in_dir(&self.kernel.sd, storage::PULP_DIR, name, buf) 353 91 } 354 92 355 93 #[inline] 356 - pub fn sync_write_app_data(&mut self, name: &str, data: &[u8]) -> Result<(), &'static str> { 94 + pub fn write_app_data(&mut self, name: &str, data: &[u8]) -> Result<()> { 357 95 storage::write_file_in_dir(&self.kernel.sd, storage::PULP_DIR, name, data) 358 96 } 359 97 360 98 #[inline] 361 - pub fn sync_ensure_app_subdir(&mut self, dir: &str) -> Result<(), &'static str> { 99 + pub fn ensure_app_subdir(&mut self, dir: &str) -> Result<()> { 362 100 storage::ensure_pulp_subdir(&self.kernel.sd, dir) 363 101 } 364 102 365 103 #[inline] 366 - pub fn sync_read_app_subdir_chunk( 104 + pub fn read_app_subdir_chunk( 367 105 &mut self, 368 106 dir: &str, 369 107 name: &str, 370 108 offset: u32, 371 109 buf: &mut [u8], 372 - ) -> Result<usize, &'static str> { 110 + ) -> Result<usize> { 373 111 storage::read_chunk_in_pulp_subdir(&self.kernel.sd, dir, name, offset, buf) 374 112 } 375 113 376 114 #[inline] 377 - pub fn sync_write_app_subdir( 378 - &mut self, 379 - dir: &str, 380 - name: &str, 381 - data: &[u8], 382 - ) -> Result<(), &'static str> { 115 + pub fn write_app_subdir(&mut self, dir: &str, name: &str, data: &[u8]) -> Result<()> { 383 116 storage::write_in_pulp_subdir(&self.kernel.sd, dir, name, data) 384 117 } 385 118 386 119 #[inline] 387 - pub fn sync_append_app_subdir( 388 - &mut self, 389 - dir: &str, 390 - name: &str, 391 - data: &[u8], 392 - ) -> Result<(), &'static str> { 120 + pub fn append_app_subdir(&mut self, dir: &str, name: &str, data: &[u8]) -> Result<()> { 393 121 storage::append_in_pulp_subdir(&self.kernel.sd, dir, name, data) 394 122 } 395 123 396 124 #[inline] 397 - pub fn sync_file_size_app_subdir( 398 - &mut self, 399 - dir: &str, 400 - name: &str, 401 - ) -> Result<u32, &'static str> { 125 + pub fn file_size_app_subdir(&mut self, dir: &str, name: &str) -> Result<u32> { 402 126 storage::file_size_in_pulp_subdir(&self.kernel.sd, dir, name) 403 127 } 404 128 405 - pub fn sync_dir_page( 406 - &mut self, 407 - offset: usize, 408 - buf: &mut [DirEntry], 409 - ) -> Result<DirPage, &'static str> { 129 + #[inline] 130 + pub fn delete_app_subdir(&mut self, dir: &str, name: &str) -> Result<()> { 131 + storage::delete_in_pulp_subdir(&self.kernel.sd, dir, name) 132 + } 133 + 134 + pub fn dir_page(&mut self, offset: usize, buf: &mut [DirEntry]) -> Result<DirPage> { 410 135 let k = &mut *self.kernel; 411 136 k.dir_cache.ensure_loaded(&k.sd)?; 412 137 Ok(k.dir_cache.page(offset, buf)) 138 + } 139 + 140 + pub fn invalidate_dir_cache(&mut self) { 141 + self.kernel.dir_cache.invalidate(); 142 + } 143 + 144 + // system info (sync, no I/O) 145 + 146 + #[inline] 147 + pub fn battery_mv(&self) -> u16 { 148 + self.kernel.cached_battery_mv 149 + } 150 + 151 + #[inline] 152 + pub fn uptime_secs(&self) -> u32 { 153 + uptime_secs() 154 + } 155 + 156 + #[inline] 157 + pub fn sd_ok(&self) -> bool { 158 + self.kernel.sd_ok 413 159 } 414 160 415 161 // direct cache accessors
+8 -1
kernel/src/kernel/mod.rs
··· 17 17 pub mod wake; 18 18 pub mod work_queue; 19 19 20 + // Unified error types (primary home: crate::error) 21 + pub use crate::error::{Error, ErrorKind, Result, ResultExt}; 22 + 23 + // Backward-compatible alias so `kernel::StorageError` keeps working 24 + // during migration. It is now `type StorageError = Error`. 25 + pub use crate::drivers::storage::StorageError; 26 + 20 27 pub use app::{ 21 28 App, AppContext, AppIdType, AppLayer, Launcher, NavEvent, PendingSetting, QuickAction, 22 29 QuickActionKind, RECENT_FILE, Redraw, Transition, 23 30 }; 24 31 pub use bookmarks::BookmarkCache; 25 32 pub use console::BootConsole; 26 - pub use handle::{KernelHandle, StorageError}; 33 + pub use handle::KernelHandle; 27 34 pub use wake::uptime_secs; 28 35 29 36 use esp_hal::delay::Delay;
+160 -70
kernel/src/kernel/scheduler.rs
··· 1 1 // scheduler: main event loop, render pipeline, housekeeping, sleep 2 2 // 3 - // EPD and SD share a single SPI bus via CriticalSectionDevice; 4 - // busy_wait_with_input() does NOT run background SD I/O while 5 - // the EPD is refreshing to avoid RefCell borrow conflicts 3 + // EPD and SD share a single SPI bus via CriticalSectionDevice. 4 + // during normal operation, all SD I/O completes before render() 5 + // touches the EPD. during the DU/GC waveform (~400ms), the EPD 6 + // charge pump drives pixels with no SPI commands, so the bus is 7 + // free for SD I/O. busy_wait_with_background exploits this window 8 + // to run background caching and housekeeping during the waveform. 9 + // 10 + // handle_input and poll_housekeeping are synchronous; they return 11 + // a bool flag when the caller should enter_sleep (which is async 12 + // because it renders a sleep screen via the EPD). 13 + // 14 + // sd_card_sleep sends cmd0 before deep sleep to reduce sd card 15 + // idle current from ~150 µa to ~10 µa 6 16 7 17 use embassy_futures::select::{Either, select}; 8 - use embassy_time::{Duration, Ticker, Timer}; 18 + use embassy_time::{Duration, Ticker, with_timeout}; 9 19 use log::info; 10 20 11 21 use super::app::{AppLayer, Redraw, Transition}; ··· 20 30 const TICK_MS: u64 = 10; 21 31 22 32 impl super::Kernel { 23 - // render boot console to EPD -- call before boot() to show 33 + // render boot console to EPD; call before boot() to show 24 34 // hardware init progress in the built-in mono font 25 35 pub async fn show_boot_console(&mut self, console: &super::BootConsole) { 26 36 let draw = |s: &mut StripBuffer| console.draw(s); ··· 41 51 42 52 tasks::set_idle_timeout(app_mgr.system_settings().sleep_timeout); 43 53 self.log_stats(); 44 - app_mgr.enter_initial(&mut self.handle()).await; 54 + app_mgr.enter_initial(&mut self.handle()); 45 55 46 56 { 47 57 let draw = |s: &mut StripBuffer| app_mgr.draw(s); ··· 54 64 info!("ui ready."); 55 65 } 56 66 57 - // event-driven main loop -- never returns 67 + // event-driven main loop; never returns 68 + // 69 + // two genuine async suspension points in steady state: 70 + // 1. select(INPUT_EVENTS.receive(), work_ticker.next()) 71 + // 2. EPD busy pin wait inside render() 72 + // everything between them is synchronous function calls 58 73 pub async fn run<A: AppLayer>(&mut self, app_mgr: &mut A) -> ! { 59 74 let mut work_ticker = Ticker::every(Duration::from_millis(TICK_MS)); 60 75 ··· 64 79 continue; 65 80 } 66 81 82 + // async point 1: wait for input or tick 67 83 let hw_event = match select(tasks::INPUT_EVENTS.receive(), work_ticker.next()).await { 68 84 Either::First(ev) => Some(ev), 69 85 Either::Second(_) => None, 70 86 }; 71 87 72 88 if let Some(ev) = hw_event { 73 - self.handle_input(ev, app_mgr).await; 89 + if self.handle_input(ev, app_mgr) { 90 + self.enter_sleep("power held").await; 91 + continue; 92 + } 74 93 } 75 94 76 95 if app_mgr.needs_special_mode() { 77 96 continue; 78 97 } 79 98 80 - // SAFETY-CRITICAL: SPI bus sharing invariant 99 + // SPI bus sharing invariant 81 100 // 82 - // The EPD and SD card share a single SPI2 bus via 83 - // CriticalSectionDevice (RefCell under the hood). SD I/O 84 - // and EPD rendering must NEVER overlap, concurrent access 85 - // would cause a RefCell borrow panic at runtime. 101 + // the EPD and SD card share a single SPI2 bus via 102 + // CriticalSectionDevice (RefCell under the hood). 86 103 // 87 - // This ordering enforces that: 88 - // 1. All background SD I/O (app caching, title scan, etc.) 89 - // completes here, before any EPD access. 90 - // 2. poll_housekeeping may do SD I/O (bookmark flush, 91 - // SD probe), also before render. 92 - // 3. render() is the only code below that touches the EPD. 93 - // 4. busy_wait_with_input() does NOT run background work 94 - // while the EPD is refreshing, only input collection. 104 + // 1. background SD I/O runs here, before EPD access; 105 + // interruptible by input so the user can navigate 106 + // away during long-running caching operations 107 + // 2. poll_housekeeping may do SD I/O, also before render 108 + // 3. render() touches the EPD; during the waveform window 109 + // busy_wait_with_background runs SD I/O because the 110 + // EPD charge pump is driving pixels with no SPI commands 111 + // 4. no SD I/O outside these three sites 95 112 // 96 - // If you add new SD I/O call sites, they MUST go above the 97 - // render() call. Violating this will panic, not corrupt. 98 - { 113 + // when input arrives during run_background, the background 114 + // future is dropped. this is safe: partial chapter cache 115 + // writes leave ch_cached=false so the chapter is recached 116 + // on the next attempt 117 + let bg_input = { 99 118 let mut handle = self.handle(); 100 - app_mgr.run_background(&mut handle).await; 119 + match select( 120 + app_mgr.run_background(&mut handle), 121 + tasks::INPUT_EVENTS.receive(), 122 + ) 123 + .await 124 + { 125 + Either::First(()) => None, 126 + Either::Second(ev) => Some(ev), 127 + } 128 + }; 129 + 130 + if let Some(ev) = bg_input { 131 + if self.handle_input(ev, app_mgr) { 132 + self.enter_sleep("power held").await; 133 + continue; 134 + } 135 + 136 + if app_mgr.needs_special_mode() { 137 + continue; 138 + } 101 139 } 102 140 103 - self.poll_housekeeping(app_mgr).await; 141 + if self.poll_housekeeping(app_mgr) { 142 + self.enter_sleep("idle timeout").await; 143 + continue; 144 + } 104 145 105 - if app_mgr.has_redraw() { 146 + if app_mgr.ctx_mut().render_ready() { 106 147 let redraw = app_mgr.take_redraw(); 107 148 self.render(app_mgr, redraw).await; 108 149 } ··· 116 157 .run_special_mode(&mut self.epd, self.strip, &mut self.delay, &self.sd) 117 158 .await; 118 159 119 - app_mgr 120 - .apply_transition(Transition::Pop, &mut self.handle()) 121 - .await; 160 + app_mgr.apply_transition(Transition::Pop, &mut self.handle()); 122 161 app_mgr.request_full_redraw(); 123 162 } 124 163 125 - async fn handle_input<A: AppLayer>(&mut self, hw_event: Event, app_mgr: &mut A) { 126 - // power long-press -> sleep (intercept before app dispatch) 164 + // returns true if caller should call enter_sleep 165 + // 166 + // note: the original async version called enter_sleep inline 167 + // on power-long-press and then fell through to dispatch_event 168 + // if sleep_deep somehow returned; this version correctly returns 169 + // early so the caller can enter_sleep and continue the loop 170 + fn handle_input<A: AppLayer>(&mut self, hw_event: Event, app_mgr: &mut A) -> bool { 127 171 if hw_event == Event::LongPress(Button::Power) { 128 - self.enter_sleep("power held").await; 172 + return true; 129 173 } 130 174 131 175 let transition = app_mgr.dispatch_event(hw_event, &mut *self.bm_cache); 132 176 133 177 if transition != Transition::None { 134 - app_mgr 135 - .apply_transition(transition, &mut self.handle()) 136 - .await; 178 + app_mgr.apply_transition(transition, &mut self.handle()); 137 179 } 180 + 181 + false 138 182 } 139 183 140 - async fn poll_housekeeping<A: AppLayer>(&mut self, app_mgr: &A) { 184 + // returns true if idle sleep is due 185 + fn poll_housekeeping<A: AppLayer>(&mut self, app_mgr: &A) -> bool { 141 186 if let Some(mv) = tasks::BATTERY_MV.try_take() { 142 187 self.cached_battery_mv = mv; 143 188 } ··· 157 202 } 158 203 } 159 204 160 - if tasks::IDLE_SLEEP_DUE.try_take().is_some() { 161 - self.enter_sleep("idle timeout").await; 205 + tasks::IDLE_SLEEP_DUE.try_take().is_some() 206 + } 207 + 208 + // housekeeping without idle-sleep check; do not initiate sleep mid-refresh 209 + fn poll_housekeeping_waveform<A: AppLayer>(&mut self, app_mgr: &A) { 210 + if let Some(mv) = tasks::BATTERY_MV.try_take() { 211 + self.cached_battery_mv = mv; 212 + } 213 + 214 + if tasks::SD_CHECK_DUE.try_take().is_some() { 215 + self.sd_ok = self.sd.probe_ok(); 216 + } 217 + 218 + if tasks::BOOKMARK_FLUSH_DUE.try_take().is_some() && self.bm_cache.is_dirty() { 219 + self.bm_cache.flush(&self.sd); 162 220 } 221 + 222 + if tasks::STATUS_DUE.try_take().is_some() { 223 + self.log_stats(); 224 + if app_mgr.settings_loaded() { 225 + tasks::set_idle_timeout(app_mgr.system_settings().sleep_timeout); 226 + } 227 + } 228 + // idle sleep not checked here; never sleep during a waveform 163 229 } 164 230 165 231 // partial refreshes use DU waveform (~400 ms); after ghost_clear_every ··· 199 265 200 266 if let Some(rs) = rs { 201 267 self.epd.partial_start_du(&rs); 202 - let deferred = self.busy_wait_with_input(app_mgr).await; 268 + let deferred = self.busy_wait_with_background(app_mgr).await; 203 269 204 270 if app_mgr.has_redraw() { 205 271 // content changed mid-DU; leave RED stale ··· 217 283 } 218 284 219 285 if let Some(transition) = deferred { 220 - app_mgr 221 - .apply_transition(transition, &mut self.handle()) 222 - .await; 286 + app_mgr.apply_transition(transition, &mut self.handle()); 223 287 } 224 288 225 289 break 'render; ··· 247 311 248 312 self.epd.start_full_update(); 249 313 250 - let deferred = self.busy_wait_with_input(app_mgr).await; 314 + let deferred = self.busy_wait_with_background(app_mgr).await; 251 315 252 316 self.epd.finish_full_update(); 253 317 self.partial_refreshes = 0; 254 318 self.red_stale = false; 255 319 256 320 if let Some(transition) = deferred { 257 - app_mgr 258 - .apply_transition(transition, &mut self.handle()) 259 - .await; 321 + app_mgr.apply_transition(transition, &mut self.handle()); 260 322 } 261 323 } 262 324 } // 'render 263 325 } 264 326 265 - // Collect input events while EPD is busy refreshing. 327 + // collect input and run background work while EPD is busy refreshing 266 328 // 267 - // SAFETY-CRITICAL: no SD I/O or background work may run here. 268 - // The EPD is actively driving the SPI bus during refresh; any 269 - // SD access would cause a RefCell borrow panic. Only input 270 - // events (from the ADC-based input_task) are collected. 271 - async fn busy_wait_with_input<A: AppLayer>( 329 + // during the DU/GC waveform the EPD charge pump drives pixels; 330 + // no SPI commands are sent, so the bus is free for SD I/O. 331 + // is_busy() is a sync GPIO read; no epd borrow is held across 332 + // any .await point, so self is fully available for handle() etc. 333 + async fn busy_wait_with_background<A: AppLayer>( 272 334 &mut self, 273 335 app_mgr: &mut A, 274 336 ) -> Option<Transition<A::Id>> { 275 337 let mut deferred: Option<Transition<A::Id>> = None; 276 338 277 339 loop { 340 + // sync gpio read; no borrow held after this line 278 341 if !self.epd.is_busy() { 279 342 break; 280 343 } 281 344 282 - match select( 283 - self.epd.busy_pin().wait_for_low(), 284 - select( 285 - tasks::INPUT_EVENTS.receive(), 286 - Timer::after(Duration::from_millis(TICK_MS)), 287 - ), 345 + // wait up to TICK_MS for input; no epd borrow involved 346 + let input_event = with_timeout( 347 + Duration::from_millis(TICK_MS), 348 + tasks::INPUT_EVENTS.receive(), 288 349 ) 289 350 .await 290 - { 291 - Either::First(_) => break, 351 + .ok(); 292 352 293 - Either::Second(Either::First(hw_event)) => { 294 - if app_mgr.suppress_deferred_input() { 295 - continue; 296 - } 297 - 353 + if let Some(hw_event) = input_event { 354 + if !app_mgr.suppress_deferred_input() { 298 355 let t = app_mgr.dispatch_event(hw_event, &mut *self.bm_cache); 299 356 if t != Transition::None && deferred.is_none() { 300 357 deferred = Some(t); 301 358 } 302 359 } 360 + continue; 361 + } 303 362 304 - Either::Second(Either::Second(_)) => {} 363 + // timeout elapsed; spi bus is free during waveform 364 + { 365 + let mut handle = self.handle(); 366 + app_mgr.run_background(&mut handle).await; 305 367 } 368 + self.poll_housekeeping_waveform(app_mgr); 306 369 } 307 370 308 371 deferred 309 372 } 310 373 311 - // flush bookmarks, render sleep screen, enter MCU deep sleep 374 + // flush bookmarks, render sleep screen, enter MCU deep sleep; 312 375 // on real hardware this never returns (wake = full MCU reset) 313 376 pub async fn enter_sleep(&mut self, reason: &str) { 314 377 use embedded_graphics::mono_font::MonoTextStyle; ··· 326 389 self.bm_cache.flush(&self.sd); 327 390 } 328 391 392 + self.sd_card_sleep(); 393 + 329 394 self.epd 330 395 .full_refresh_async(self.strip, &mut self.delay, &|s: &mut StripBuffer| { 331 396 let style = MonoTextStyle::new(&FONT_6X13, BinaryColor::On); ··· 339 404 340 405 // safety: deep sleep never returns, the MCU resets on wake, so 341 406 // these stolen peripherals cannot alias with their original 342 - // owners. LPWR is not used elsewhere; GPIO3 was previously 343 - // cloned into InputHw but we are about to halt the CPU. 407 + // owners. LPWR is not used elsewhere; GPIO3 was previously 408 + // cloned into InputHw but we are about to halt the CPU 344 409 let mut rtc = Rtc::new(unsafe { esp_hal::peripherals::LPWR::steal() }); 345 410 let mut gpio3 = unsafe { esp_hal::peripherals::GPIO3::steal() }; 346 411 let wakeup_pins: &mut [(&mut dyn RtcPinWithResistors, WakeupLevel)] = ··· 355 420 loop { 356 421 core::hint::spin_loop(); 357 422 } 423 + } 424 + 425 + // send cmd0 to put sd card into idle/sleep state; 426 + // reduces sd current from ~150 µa to ~10 µa during deep sleep. 427 + // call after all sd i/o is done and before epd sleep-screen render 428 + fn sd_card_sleep(&self) { 429 + use embedded_hal::digital::OutputPin; 430 + 431 + self.sd.flush_and_close(); 432 + 433 + critical_section::with(|cs| { 434 + let bus_ref = crate::board::SPI_BUS_REF.borrow(cs).get(); 435 + let mut cs_pin = crate::board::SD_CS_SLEEP.borrow_ref_mut(cs); 436 + 437 + if let (Some(bus_ref), Some(pin)) = (bus_ref, cs_pin.as_mut()) { 438 + let mut bus: core::cell::RefMut<'_, _> = bus_ref.borrow(cs).borrow_mut(); 439 + // 80 clocks cs high (sd spec: card ready for command) 440 + let _ = bus.write(&[0xFF; 10]); 441 + let _ = pin.set_low(); 442 + // cmd0 (GO_IDLE_STATE) with valid crc 443 + let _ = bus.write(&[0x40, 0x00, 0x00, 0x00, 0x00, 0x95]); 444 + let _ = bus.write(&[0xFF]); 445 + let _ = pin.set_high(); 446 + } 447 + }); 358 448 } 359 449 360 450 pub fn log_stats(&self) {
+24 -7
kernel/src/kernel/tasks.rs
··· 14 14 15 15 pub static BATTERY_MV: Signal<CriticalSectionRawMutex, u16> = Signal::new(); 16 16 17 - const BATTERY_INTERVAL_TICKS: u32 = 3000; // 3000 x 10 ms = 30 s 17 + const POLL_ACTIVE_MS: u64 = 10; // 100 hz: during/after input 18 + const POLL_IDLE_MS: u64 = 50; // 20 hz: no recent input 19 + const POLL_DROWSY_MS: u64 = 200; // 5 hz: approaching sleep timeout 20 + 21 + const IDLE_AFTER_MS: u64 = 2_000; 22 + const DROWSY_AFTER_MS: u64 = 30_000; 23 + 24 + const BATTERY_INTERVAL_MS: u64 = 30_000; 18 25 19 26 #[embassy_executor::task] 20 27 pub async fn input_task(mut input: InputDriver) -> ! { 21 - let mut ticker = Ticker::every(Duration::from_millis(10)); 22 - let mut battery_counter: u32 = 0; 28 + let mut poll_ms = POLL_ACTIVE_MS; 29 + let mut battery_accum_ms: u64 = 0; 23 30 24 31 let raw = input.read_battery_mv(); 25 32 BATTERY_MV.signal(battery::adc_to_battery_mv(raw)); 26 33 27 34 loop { 28 - ticker.next().await; 35 + Timer::after(Duration::from_millis(poll_ms)).await; 29 36 30 37 if let Some(ev) = input.poll() { 31 38 let _ = INPUT_EVENTS.try_send(ev); 32 39 IDLE_RESET.signal(()); 40 + poll_ms = POLL_ACTIVE_MS; 41 + } else { 42 + let since = input.ms_since_last_event(); 43 + poll_ms = if since > DROWSY_AFTER_MS { 44 + POLL_DROWSY_MS 45 + } else if since > IDLE_AFTER_MS { 46 + POLL_IDLE_MS 47 + } else { 48 + POLL_ACTIVE_MS 49 + }; 33 50 } 34 51 35 - battery_counter += 1; 36 - if battery_counter >= BATTERY_INTERVAL_TICKS { 37 - battery_counter = 0; 52 + battery_accum_ms += poll_ms; 53 + if battery_accum_ms >= BATTERY_INTERVAL_MS { 54 + battery_accum_ms = 0; 38 55 let raw = input.read_battery_mv(); 39 56 BATTERY_MV.signal(battery::adc_to_battery_mv(raw)); 40 57 }
+5 -74
kernel/src/kernel/work_queue.rs
··· 24 24 #[repr(u8)] 25 25 pub enum BgWorkKind { 26 26 Idle = 0, 27 - StripChapter = 1, 28 - DecodeImage = 2, 27 + DecodeImage = 1, 29 28 } 30 29 31 30 impl BgWorkKind { 32 31 pub const fn label(self) -> &'static str { 33 32 match self { 34 33 Self::Idle => "", 35 - Self::StripChapter => "CH", 36 34 Self::DecodeImage => "IMG", 37 35 } 38 36 } ··· 100 98 } 101 99 102 100 pub enum WorkTask { 103 - StripChapter { 104 - chapter_idx: u16, 105 - xhtml: Vec<u8>, 106 - }, 107 101 DecodeImage { 108 102 path_hash: u32, 109 103 data: Vec<u8>, ··· 119 113 } 120 114 121 115 pub enum WorkOutcome { 122 - ChapterReady { 123 - chapter_idx: u16, 124 - text: Vec<u8>, 125 - }, 126 - ChapterFailed { 127 - chapter_idx: u16, 128 - error: &'static str, 129 - }, 130 - ImageReady { 131 - path_hash: u32, 132 - image: DecodedImage, 133 - }, 134 - ImageFailed { 135 - path_hash: u32, 136 - error: &'static str, 137 - }, 116 + ImageReady { path_hash: u32, image: DecodedImage }, 117 + ImageFailed { path_hash: u32, error: &'static str }, 138 118 } 139 119 140 120 pub struct WorkResult { ··· 149 129 } 150 130 } 151 131 152 - static WORK_IN: Channel<CriticalSectionRawMutex, WorkItem, 1> = Channel::new(); 153 - static WORK_OUT: Channel<CriticalSectionRawMutex, WorkResult, 1> = Channel::new(); 132 + static WORK_IN: Channel<CriticalSectionRawMutex, WorkItem, 2> = Channel::new(); 133 + static WORK_OUT: Channel<CriticalSectionRawMutex, WorkResult, 2> = Channel::new(); 154 134 155 135 pub fn submit(generation: u16, task: WorkTask) -> bool { 156 136 WORK_IN.try_send(WorkItem { generation, task }).is_ok() ··· 193 173 } 194 174 195 175 match item.task { 196 - WorkTask::StripChapter { chapter_idx, xhtml } => { 197 - set_status(BgStatus { 198 - kind: BgWorkKind::StripChapter, 199 - generation: g, 200 - }); 201 - 202 - let src_len = xhtml.len(); 203 - log::info!( 204 - "[work] ch{}: strip {} bytes (gen {})", 205 - chapter_idx, 206 - src_len, 207 - g, 208 - ); 209 - 210 - let result = smol_epub::cache::strip_html_buf(&xhtml); 211 - drop(xhtml); 212 - 213 - if g != active_generation() { 214 - log::info!("[work] ch{}: discarded (gen {} stale)", chapter_idx, g,); 215 - continue; 216 - } 217 - 218 - let outcome = match result { 219 - Ok(text) => { 220 - log::info!( 221 - "[work] ch{}: {} -> {} bytes", 222 - chapter_idx, 223 - src_len, 224 - text.len(), 225 - ); 226 - WorkOutcome::ChapterReady { chapter_idx, text } 227 - } 228 - Err(e) => { 229 - log::warn!("[work] ch{}: strip failed: {}", chapter_idx, e); 230 - WorkOutcome::ChapterFailed { 231 - chapter_idx, 232 - error: e, 233 - } 234 - } 235 - }; 236 - 237 - WORK_OUT 238 - .send(WorkResult { 239 - generation: g, 240 - outcome, 241 - }) 242 - .await; 243 - } 244 - 245 176 WorkTask::DecodeImage { 246 177 path_hash, 247 178 data,
+4
kernel/src/lib.rs
··· 10 10 11 11 pub mod board; 12 12 pub mod drivers; 13 + pub mod error; 13 14 pub mod kernel; 14 15 pub mod ui; 16 + 17 + // Re-export the core error types at crate root for convenience. 18 + pub use error::{Error, ErrorKind, Result, ResultExt};
+1 -1
kernel/src/ui/mod.rs
··· 12 12 pub use statusbar::{ 13 13 BAR_HEIGHT, CONTENT_TOP, free_stack_bytes, paint_stack, stack_high_water_mark, 14 14 }; 15 - pub use widget::{Alignment, Region, wrap_next, wrap_prev}; 15 + pub use widget::{Alignment, Region, draw_progress_bar, wrap_next, wrap_prev}; 16 16 17 17 pub use crate::board::{SCREEN_H, SCREEN_W};
+41 -2
kernel/src/ui/widget.rs
··· 1 - // region geometry and alignment helpers 1 + // region geometry and alignment helpers, progress bar drawing 2 2 3 - use embedded_graphics::{prelude::*, primitives::Rectangle}; 3 + use embedded_graphics::{ 4 + pixelcolor::BinaryColor, prelude::*, primitives::PrimitiveStyle, primitives::Rectangle, 5 + }; 6 + 7 + use crate::drivers::strip::StripBuffer; 4 8 5 9 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 6 10 pub struct Region { ··· 110 114 } 111 115 if current == 0 { count - 1 } else { current - 1 } 112 116 } 117 + 118 + // horizontal progress bar for 1-bit e-paper. 119 + // draws a 1px black border around the full track and fills 120 + // proportionally from the left. pct is clamped to 0..=100. 121 + // region should be at least 4px wide and 4px tall. 122 + pub fn draw_progress_bar(strip: &mut StripBuffer, region: Region, pct: u8) { 123 + let pct = pct.min(100) as u32; 124 + 125 + // clear region 126 + region 127 + .to_rect() 128 + .into_styled(PrimitiveStyle::with_fill(BinaryColor::Off)) 129 + .draw(strip) 130 + .unwrap(); 131 + 132 + // 1px border shows full extent even at 0% 133 + region 134 + .to_rect() 135 + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) 136 + .draw(strip) 137 + .unwrap(); 138 + 139 + // filled portion inside the border 140 + if pct > 0 && region.w > 2 && region.h > 2 { 141 + let inner_w = (region.w - 2) as u32; 142 + let fill_w = (inner_w * pct / 100).max(1); 143 + Rectangle::new( 144 + Point::new((region.x + 1) as i32, (region.y + 1) as i32), 145 + Size::new(fill_w, (region.h - 2) as u32), 146 + ) 147 + .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) 148 + .draw(strip) 149 + .unwrap(); 150 + } 151 + }
+56 -29
src/apps/files.rs
··· 15 15 use crate::board::{SCREEN_H, SCREEN_W}; 16 16 use crate::drivers::storage::DirEntry; 17 17 use crate::drivers::strip::StripBuffer; 18 + use crate::error::{Error, ErrorKind}; 18 19 use crate::fonts; 19 20 use crate::kernel::KernelHandle; 20 21 use crate::ui::{Alignment, BitmapDynLabel, BitmapLabel, CONTENT_TOP, Region}; ··· 46 47 selected: usize, 47 48 needs_load: bool, 48 49 stale_cache: bool, 49 - error: Option<&'static str>, 50 + error: Option<Error>, 50 51 ui_fonts: fonts::UiFonts, 51 52 list_y: u16, 52 53 53 54 title_scan_idx: usize, 54 55 title_scanning: bool, 56 + title_reload: bool, 55 57 } 56 58 57 59 impl FilesApp { ··· 70 72 list_y: CONTENT_TOP + 8 + uf.heading.line_height + HEADER_LIST_GAP, 71 73 title_scan_idx: 0, 72 74 title_scanning: false, 75 + title_reload: false, 73 76 } 74 77 } 75 78 ··· 98 101 } 99 102 } 100 103 101 - fn load_failed(&mut self, msg: &'static str) { 104 + fn load_failed(&mut self, e: Error) { 102 105 self.needs_load = false; 103 - self.error = Some(msg); 106 + self.error = Some(e); 104 107 self.count = 0; 105 108 } 106 109 ··· 177 180 } 178 181 179 182 impl App<AppId> for FilesApp { 180 - async fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 183 + fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 181 184 self.scroll = 0; 182 185 self.selected = 0; 183 186 self.needs_load = true; ··· 200 203 201 204 fn on_suspend(&mut self) {} 202 205 203 - async fn on_resume(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 206 + fn on_resume(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 204 207 ctx.mark_dirty(Region::new( 205 208 0, 206 209 CONTENT_TOP, ··· 217 220 } 218 221 219 222 let mut buf = [DirEntry::EMPTY; PAGE_SIZE]; 220 - match k.sync_dir_page(self.scroll, &mut buf) { 223 + match k.dir_page(self.scroll, &mut buf) { 221 224 Ok(page) => { 222 225 self.load_page(&buf[..page.count], page.total); 223 226 } ··· 227 230 } 228 231 } 229 232 230 - ctx.mark_dirty(self.list_region()); 231 - ctx.mark_dirty(STATUS_REGION); 233 + if self.title_reload { 234 + self.title_reload = false; 235 + ctx.mark_dirty_coalesced(self.list_region()); 236 + ctx.mark_dirty_coalesced(STATUS_REGION); 237 + } else { 238 + ctx.mark_dirty(self.list_region()); 239 + ctx.mark_dirty(STATUS_REGION); 240 + } 232 241 return; 233 242 } 234 243 ··· 237 246 self.title_scan_idx = dirty.next_idx; 238 247 if dirty.resolved { 239 248 self.needs_load = true; 249 + self.title_reload = true; 240 250 } 241 251 } else { 242 252 self.title_scanning = false; ··· 308 318 .unwrap(); 309 319 310 320 if self.total > 0 { 311 - let mut status = BitmapDynLabel::<20>::new(STATUS_REGION, self.ui_fonts.body) 321 + let mut status = BitmapDynLabel::<24>::new(STATUS_REGION, self.ui_fonts.body) 312 322 .alignment(Alignment::CenterRight); 313 323 let _ = write!(status, "{}/{}", self.scroll + self.selected + 1, self.total); 324 + if self.title_scanning { 325 + let _ = write!(status, " ..."); 326 + } 314 327 status.draw(strip).unwrap(); 315 328 } 316 329 317 - if let Some(msg) = self.error { 318 - BitmapLabel::new(self.row_region(0), msg, self.ui_fonts.body) 319 - .alignment(Alignment::CenterLeft) 320 - .draw(strip) 321 - .unwrap(); 330 + if let Some(e) = self.error { 331 + let mut label = BitmapDynLabel::<32>::new(self.row_region(0), self.ui_fonts.body) 332 + .alignment(Alignment::CenterLeft); 333 + let _ = core::fmt::Write::write_fmt(&mut label, format_args!("{}", e)); 334 + label.draw(strip).unwrap(); 322 335 return; 323 336 } 324 337 ··· 373 386 374 387 log::info!("titles: scanning {} (idx {})", name, idx); 375 388 376 - let result = (|| -> Result<(), &'static str> { 377 - let file_size = k.sync_file_size(name)?; 389 + let result = (|| -> crate::error::Result<()> { 390 + let file_size = k.file_size(name)?; 378 391 if file_size < 22 { 379 - return Err("too small"); 392 + return Err(Error::new(ErrorKind::InvalidData, "title_scan: too small")); 380 393 } 381 394 382 395 let tail_size = (file_size as usize).min(512); 383 396 let tail_offset = file_size - tail_size as u32; 384 397 let mut buf = [0u8; 512]; 385 - let n = k.sync_read_chunk(name, tail_offset, &mut buf[..tail_size])?; 398 + let n = k.read_chunk(name, tail_offset, &mut buf[..tail_size])?; 386 399 400 + // ZipIndex::parse_eocd returns Result<_, &'static str>; 401 + // the From<&'static str> impl on Error converts automatically via ? 387 402 let (cd_offset, cd_size) = ZipIndex::parse_eocd(&buf[..n], file_size)?; 388 403 389 404 let mut cd_buf = Vec::new(); 390 405 cd_buf 391 406 .try_reserve_exact(cd_size as usize) 392 - .map_err(|_| "CD too large")?; 407 + .map_err(|_| Error::new(ErrorKind::OutOfMemory, "title_scan: CD alloc"))?; 393 408 cd_buf.resize(cd_size as usize, 0); 394 409 395 410 let mut total = 0usize; 396 411 while total < cd_buf.len() { 397 - let rd = k.sync_read_chunk(name, cd_offset + total as u32, &mut cd_buf[total..])?; 412 + let rd = k.read_chunk(name, cd_offset + total as u32, &mut cd_buf[total..])?; 398 413 if rd == 0 { 399 - return Err("CD truncated"); 414 + return Err(Error::new( 415 + ErrorKind::InvalidData, 416 + "title_scan: CD truncated", 417 + )); 400 418 } 401 419 total += rd; 402 420 } ··· 410 428 let container = smol_epub::zip::extract_entry( 411 429 zip.entry(ci), 412 430 zip.entry(ci).local_offset, 413 - |off, b| k.sync_read_chunk(name, off, b), 431 + |off, b| { 432 + k.read_chunk(name, off, b) 433 + .map_err(|e: Error| -> &'static str { e.into() }) 434 + }, 414 435 )?; 415 436 let len = epub::parse_container(&container, &mut opf_path_buf)?; 416 437 drop(container); ··· 419 440 epub::find_opf_in_zip(&zip, &mut opf_path_buf)? 420 441 }; 421 442 422 - let opf_path = 423 - core::str::from_utf8(&opf_path_buf[..opf_path_len]).map_err(|_| "bad OPF path")?; 443 + let opf_path = core::str::from_utf8(&opf_path_buf[..opf_path_len]) 444 + .map_err(|_| Error::new(ErrorKind::BadEncoding, "title_scan: OPF path"))?; 424 445 425 446 let opf_idx = zip 426 447 .find(opf_path) 427 448 .or_else(|| zip.find_icase(opf_path)) 428 - .ok_or("OPF not found")?; 449 + .ok_or(Error::new(ErrorKind::NotFound, "title_scan: OPF entry"))?; 429 450 430 451 let opf_data = smol_epub::zip::extract_entry( 431 452 zip.entry(opf_idx), 432 453 zip.entry(opf_idx).local_offset, 433 - |off, b| k.sync_read_chunk(name, off, b), 454 + |off, b| { 455 + k.read_chunk(name, off, b) 456 + .map_err(|e: Error| -> &'static str { e.into() }) 457 + }, 434 458 )?; 435 459 436 460 let opf_dir = opf_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); ··· 441 465 442 466 let title = meta.title_str(); 443 467 if title.is_empty() { 444 - return Err("no title in OPF"); 468 + return Err(Error::new( 469 + ErrorKind::ParseFailed, 470 + "title_scan: no title in OPF", 471 + )); 445 472 } 446 473 447 474 log::info!("titles: {} -> \"{}\"", name, title); 448 - let _ = k.sync_save_title(name, title); 475 + let _ = k.save_title(name, title); 449 476 k.dir_cache_mut().set_entry_title(idx, title.as_bytes()); 450 477 451 478 Ok(()) 452 479 })(); 453 480 454 - if let Err(e) = result { 481 + if let Err(e) = &result { 455 482 log::warn!("titles: {} failed: {}", name, e); 456 483 } 457 484
+4 -4
src/apps/home.rs
··· 102 102 103 103 pub fn load_recent(&mut self, k: &mut KernelHandle<'_>) { 104 104 let mut buf = [0u8; 32]; 105 - match k.sync_read_app_data_start(RECENT_FILE, &mut buf) { 105 + match k.read_app_data_start(RECENT_FILE, &mut buf) { 106 106 Ok((_, n)) if n > 0 => { 107 107 let n = n.min(32); 108 108 self.recent_book[..n].copy_from_slice(&buf[..n]); ··· 193 193 } 194 194 195 195 impl App<AppId> for HomeApp { 196 - async fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 196 + fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 197 197 ctx.clear_message(); 198 198 self.state = HomeState::Menu; 199 199 self.selected = 0; ··· 205 205 )); 206 206 } 207 207 208 - async fn on_resume(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 208 + fn on_resume(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 209 209 self.state = HomeState::Menu; 210 210 self.selected = 0; 211 211 self.needs_load_recent = true; ··· 221 221 if self.needs_load_recent { 222 222 let old_count = self.item_count; 223 223 let mut buf = [0u8; 32]; 224 - match k.sync_read_app_data_start(RECENT_FILE, &mut buf) { 224 + match k.read_app_data_start(RECENT_FILE, &mut buf) { 225 225 Ok((_, n)) if n > 0 => { 226 226 let n = n.min(32); 227 227 self.recent_book[..n].copy_from_slice(&buf[..n]);
+9 -9
src/apps/manager.rs
··· 164 164 self.home.load_recent(k); 165 165 } 166 166 167 - pub async fn enter_initial(&mut self, k: &mut KernelHandle<'_>) { 168 - self.home.on_enter(&mut self.launcher.ctx, k).await; 167 + pub fn enter_initial(&mut self, k: &mut KernelHandle<'_>) { 168 + self.home.on_enter(&mut self.launcher.ctx, k); 169 169 } 170 170 171 171 // power-button long-press must be intercepted by the scheduler ··· 248 248 } 249 249 } 250 250 251 - pub async fn apply_transition(&mut self, transition: Transition, k: &mut KernelHandle<'_>) { 251 + pub fn apply_transition(&mut self, transition: Transition, k: &mut KernelHandle<'_>) { 252 252 if let Some(nav) = self.launcher.apply(transition) { 253 253 log::info!("app: {:?} -> {:?}", nav.from, nav.to); 254 254 ··· 268 268 if nav.to != AppId::Upload { 269 269 if nav.resume { 270 270 with_app!(nav.to, self, |app| { 271 - app.on_resume(&mut self.launcher.ctx, k).await 271 + app.on_resume(&mut self.launcher.ctx, k) 272 272 }); 273 273 } else { 274 274 with_app!(nav.to, self, |app| { 275 - app.on_enter(&mut self.launcher.ctx, k).await 275 + app.on_enter(&mut self.launcher.ctx, k) 276 276 }); 277 277 } 278 278 } ··· 391 391 AppManager::dispatch_event(self, event, bm) 392 392 } 393 393 394 - async fn apply_transition(&mut self, t: Transition, k: &mut KernelHandle<'_>) { 395 - AppManager::apply_transition(self, t, k).await; 394 + fn apply_transition(&mut self, t: Transition, k: &mut KernelHandle<'_>) { 395 + AppManager::apply_transition(self, t, k); 396 396 } 397 397 398 398 async fn run_background(&mut self, k: &mut KernelHandle<'_>) { ··· 447 447 AppManager::load_home_recent(self, k); 448 448 } 449 449 450 - async fn enter_initial(&mut self, k: &mut KernelHandle<'_>) { 451 - AppManager::enter_initial(self, k).await; 450 + fn enter_initial(&mut self, k: &mut KernelHandle<'_>) { 451 + AppManager::enter_initial(self, k); 452 452 } 453 453 454 454 fn needs_special_mode(&self) -> bool {
+7
src/apps/mod.rs
··· 34 34 pub type Launcher = crate::kernel::app::Launcher<AppId>; 35 35 36 36 pub use crate::kernel::app::{App, AppContext, PendingSetting, RECENT_FILE, Redraw}; 37 + 38 + // Unified error types — available to all app code as `crate::apps::Error` etc. 39 + pub use crate::kernel::{Error, ErrorKind, Result, ResultExt}; 40 + 41 + // Backward-compatible alias; old app code referencing `StorageError` 42 + // keeps compiling — it is now `type StorageError = Error`. 43 + pub use crate::kernel::StorageError;
+136 -267
src/apps/reader/epub_pipeline.rs
··· 6 6 use smol_epub::cache; 7 7 use smol_epub::epub; 8 8 9 + use crate::error::{Error, ErrorKind}; 9 10 use crate::kernel::KernelHandle; 10 11 use crate::kernel::work_queue; 11 12 12 13 use super::{BgCacheState, CHAPTER_CACHE_MAX, EOCD_TAIL, PAGE_BUF, ReaderApp, ZipIndex}; 13 14 15 + // one cell shared between reader and writer; safe because 16 + // stream_strip_entry_async never borrows both simultaneously 17 + struct CellReader<'a, 'k>(&'a RefCell<&'a mut KernelHandle<'k>>, &'a str); 18 + struct CellWriter<'a, 'k>(&'a RefCell<&'a mut KernelHandle<'k>>, &'a str, &'a str); 19 + 20 + impl smol_epub::async_io::AsyncReadAt for CellReader<'_, '_> { 21 + async fn read_at(&mut self, offset: u32, buf: &mut [u8]) -> Result<usize, &'static str> { 22 + self.0 23 + .borrow_mut() 24 + .read_chunk(self.1, offset, buf) 25 + .map_err(|e: Error| -> &'static str { e.into() }) 26 + } 27 + } 28 + 29 + impl smol_epub::async_io::AsyncWriteChunk for CellWriter<'_, '_> { 30 + async fn write_chunk(&mut self, data: &[u8]) -> Result<(), &'static str> { 31 + self.0 32 + .borrow_mut() 33 + .append_app_subdir(self.1, self.2, data) 34 + .map_err(|e: Error| -> &'static str { e.into() }) 35 + } 36 + } 37 + 14 38 impl ReaderApp { 15 - pub(super) fn epub_init_zip(&mut self, k: &mut KernelHandle<'_>) -> Result<(), &'static str> { 39 + pub(super) fn epub_init_zip(&mut self, k: &mut KernelHandle<'_>) -> crate::error::Result<()> { 16 40 let (nb, nl) = self.name_copy(); 17 41 let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 18 42 19 - let epub_size = k.sync_file_size(name)?; 43 + let epub_size = k.file_size(name)?; 20 44 if epub_size < 22 { 21 - return Err("epub: file too small"); 45 + return Err(Error::new( 46 + ErrorKind::InvalidData, 47 + "epub_init_zip: too small", 48 + )); 22 49 } 23 50 self.epub_file_size = epub_size; 24 51 self.epub_name_hash = cache::fnv1a(name.as_bytes()); ··· 26 53 27 54 let tail_size = (epub_size as usize).min(EOCD_TAIL); 28 55 let tail_offset = epub_size - tail_size as u32; 29 - let n = k.sync_read_chunk(name, tail_offset, &mut self.buf[..tail_size])?; 30 - let (cd_offset, cd_size) = ZipIndex::parse_eocd(&self.buf[..n], epub_size)?; 56 + let n = k.read_chunk(name, tail_offset, &mut self.buf[..tail_size])?; 57 + let (cd_offset, cd_size) = ZipIndex::parse_eocd(&self.buf[..n], epub_size) 58 + .map_err(|_| Error::new(ErrorKind::ParseFailed, "epub_init_zip: EOCD"))?; 31 59 32 60 log::info!( 33 61 "epub: CD at offset {} size {} ({} file bytes)", ··· 39 67 let mut cd_buf = Vec::new(); 40 68 cd_buf 41 69 .try_reserve_exact(cd_size as usize) 42 - .map_err(|_| "epub: CD too large for memory")?; 70 + .map_err(|_| Error::new(ErrorKind::OutOfMemory, "epub_init_zip: CD alloc"))?; 43 71 cd_buf.resize(cd_size as usize, 0); 44 72 super::read_full(k, name, cd_offset, &mut cd_buf)?; 45 73 self.zip.clear(); 46 - self.zip.parse_central_directory(&cd_buf)?; 74 + self.zip 75 + .parse_central_directory(&cd_buf) 76 + .map_err(|_| Error::new(ErrorKind::ParseFailed, "epub_init_zip: CD parse"))?; 47 77 drop(cd_buf); 48 78 49 79 log::info!("epub: {} entries in ZIP", self.zip.count()); ··· 51 81 Ok(()) 52 82 } 53 83 54 - pub(super) fn epub_init_opf(&mut self, k: &mut KernelHandle<'_>) -> Result<(), &'static str> { 84 + pub(super) fn epub_init_opf(&mut self, k: &mut KernelHandle<'_>) -> crate::error::Result<()> { 55 85 let (nb, nl) = self.name_copy(); 56 86 let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 57 87 58 88 let mut opf_path_buf = [0u8; epub::OPF_PATH_CAP]; 59 89 let opf_path_len = if let Some(container_idx) = self.zip.find("META-INF/container.xml") { 60 - let container_data = super::extract_zip_entry(k, name, &self.zip, container_idx)?; 61 - let len = epub::parse_container(&container_data, &mut opf_path_buf)?; 90 + let container_data = super::extract_zip_entry(k, name, &self.zip, container_idx) 91 + .map_err(|_| Error::new(ErrorKind::ReadFailed, "epub_init_opf: container read"))?; 92 + let len = epub::parse_container(&container_data, &mut opf_path_buf).map_err(|_| { 93 + Error::new(ErrorKind::ParseFailed, "epub_init_opf: container parse") 94 + })?; 62 95 drop(container_data); 63 96 len 64 97 } else { 65 98 log::warn!("epub: no container.xml, scanning for .opf"); 66 - epub::find_opf_in_zip(&self.zip, &mut opf_path_buf)? 99 + epub::find_opf_in_zip(&self.zip, &mut opf_path_buf) 100 + .map_err(|_| Error::new(ErrorKind::NotFound, "epub_init_opf: no .opf in zip"))? 67 101 }; 68 102 69 103 let opf_path = core::str::from_utf8(&opf_path_buf[..opf_path_len]) 70 - .map_err(|_| "epub: bad opf path")?; 104 + .map_err(|_| Error::new(ErrorKind::BadEncoding, "epub_init_opf: OPF path"))?; 71 105 72 106 log::info!("epub: OPF at {}", opf_path); 73 107 ··· 75 109 .zip 76 110 .find(opf_path) 77 111 .or_else(|| self.zip.find_icase(opf_path)) 78 - .ok_or("epub: opf not found in zip")?; 79 - let opf_data = super::extract_zip_entry(k, name, &self.zip, opf_idx)?; 112 + .ok_or(Error::new(ErrorKind::NotFound, "epub_init_opf: OPF entry"))?; 113 + let opf_data = super::extract_zip_entry(k, name, &self.zip, opf_idx) 114 + .map_err(|_| Error::new(ErrorKind::ReadFailed, "epub_init_opf: OPF read"))?; 80 115 81 116 let opf_dir = opf_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); 82 117 epub::parse_opf( ··· 85 120 &self.zip, 86 121 &mut self.meta, 87 122 &mut self.spine, 88 - )?; 123 + ) 124 + .map_err(|_| Error::new(ErrorKind::ParseFailed, "epub_init_opf: OPF parse"))?; 89 125 90 126 // defer TOC to NeedToc to avoid stack overflow while OPF is live 91 127 self.toc_source = epub::find_toc_source(&opf_data, opf_dir, &self.zip); ··· 104 140 self.title[..n].copy_from_slice(&self.meta.title[..n]); 105 141 self.title_len = n; 106 142 107 - if let Err(e) = k.sync_save_title(name, self.meta.title_str()) { 143 + if let Err(e) = k.save_title(name, self.meta.title_str()) { 108 144 log::warn!("epub: failed to save title mapping: {}", e); 109 145 } 110 146 } ··· 117 153 pub(super) fn epub_check_cache( 118 154 &mut self, 119 155 k: &mut KernelHandle<'_>, 120 - ) -> Result<bool, &'static str> { 156 + ) -> crate::error::Result<bool> { 121 157 let dir_buf = self.cache_dir; 122 158 let dir = cache::dir_name_str(&dir_buf); 123 159 124 160 // read into self.buf to avoid ~2 KB stack temporaries 125 161 let meta_cap = cache::META_MAX_SIZE.min(self.buf.len()); 126 - if let Ok(n) = 127 - k.sync_read_app_subdir_chunk(dir, cache::META_FILE, 0, &mut self.buf[..meta_cap]) 162 + if let Ok(n) = k.read_app_subdir_chunk(dir, cache::META_FILE, 0, &mut self.buf[..meta_cap]) 128 163 && let Ok(count) = cache::parse_cache_meta( 129 164 &self.buf[..n], 130 165 self.epub_file_size, ··· 142 177 } 143 178 144 179 log::info!("epub: building cache for {} chapters", self.spine.len()); 145 - k.sync_ensure_app_subdir(dir)?; 180 + k.ensure_app_subdir(dir)?; 146 181 self.cache_chapter = 0; 147 182 Ok(false) 148 183 } ··· 150 185 pub(super) fn epub_finish_cache( 151 186 &mut self, 152 187 k: &mut KernelHandle<'_>, 153 - ) -> Result<bool, &'static str> { 188 + ) -> crate::error::Result<bool> { 154 189 let dir_buf = self.cache_dir; 155 190 let dir = cache::dir_name_str(&dir_buf); 156 191 let spine_len = self.spine.len(); ··· 162 197 &self.chapter_sizes[..spine_len], 163 198 &mut meta_buf, 164 199 ); 165 - k.sync_write_app_subdir(dir, cache::META_FILE, &meta_buf[..meta_len])?; 200 + k.write_app_subdir(dir, cache::META_FILE, &meta_buf[..meta_len])?; 166 201 167 202 self.chapters_cached = true; 168 203 log::info!("epub: cache complete"); 169 204 Ok(false) 170 205 } 171 206 172 - // synchronously cache a single chapter by index; skipped if already cached 173 - pub(super) fn epub_cache_single_chapter( 207 + // async streaming chapter cache; used for both initial and background 208 + // caching. decompresses, strips html, and writes chunks to sd without 209 + // ever holding full xhtml in ram. yields between decompression 210 + // iterations so the scheduler's select(run_background, input) can 211 + // interrupt on user input (e.g. pressing back during book open) 212 + pub(super) async fn epub_cache_chapter_async( 174 213 &mut self, 175 214 k: &mut KernelHandle<'_>, 176 215 ch: usize, 177 - ) -> Result<(), &'static str> { 216 + ) -> crate::error::Result<()> { 178 217 if ch >= self.spine.len() || self.ch_cached[ch] { 179 218 return Ok(()); 180 219 } ··· 183 222 let dir = cache::dir_name_str(&dir_buf); 184 223 let (nb, nl) = self.name_copy(); 185 224 let epub_name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 186 - 187 225 let entry_idx = self.spine.items[ch] as usize; 188 226 let entry = *self.zip.entry(entry_idx); 189 - 190 227 let ch_file = cache::chapter_file_name(ch as u16); 191 228 let ch_str = cache::chapter_file_str(&ch_file); 192 229 193 - k.sync_write_app_subdir(dir, ch_str, &[])?; 194 - let text_size = { 195 - let k_cell = RefCell::new(&mut *k); 196 - cache::stream_strip_entry( 197 - &entry, 198 - entry.local_offset, 199 - |offset, buf| k_cell.borrow_mut().sync_read_chunk(epub_name, offset, buf), 200 - |chunk| { 201 - k_cell 202 - .borrow_mut() 203 - .sync_append_app_subdir(dir, ch_str, chunk) 204 - }, 205 - )? 206 - }; 230 + // truncate stale data before streaming begins 231 + k.write_app_subdir(dir, ch_str, &[])?; 232 + 233 + let k_cell = RefCell::new(&mut *k); 234 + 235 + let mut reader = CellReader(&k_cell, epub_name); 236 + let mut writer = CellWriter(&k_cell, dir, ch_str); 237 + 238 + let text_size = smol_epub::async_io::stream_strip_entry_async( 239 + &entry, 240 + entry.local_offset, 241 + &mut reader, 242 + &mut writer, 243 + ) 244 + .await 245 + .map_err(|msg| Error::from(msg).with_source("epub_cache_chapter_async: stream"))?; 207 246 208 247 self.chapter_sizes[ch] = text_size; 209 248 self.ch_cached[ch] = true; 210 249 211 250 log::info!( 212 - "epub: sync-cached ch{}/{} = {} bytes", 251 + "epub: cached ch{}/{} = {} bytes", 213 252 ch, 214 253 self.spine.len(), 215 254 text_size ··· 217 256 Ok(()) 218 257 } 219 258 220 - // extract chapter XHTML from ZIP and dispatch to worker for HTML stripping 221 - pub(super) fn epub_dispatch_chapter_strip( 222 - &mut self, 223 - k: &mut KernelHandle<'_>, 224 - ) -> Result<bool, &'static str> { 225 - let spine_len = self.spine.len(); 226 - 227 - // advance past chapters that were already sync-cached 228 - while (self.cache_chapter as usize) < spine_len 229 - && self.ch_cached[self.cache_chapter as usize] 230 - { 231 - self.cache_chapter += 1; 232 - } 233 - 234 - // priority: sync-cache chapters adjacent to the reading position 235 - // before continuing the sequential scan, so forward/backward 236 - // chapter navigation is always instant 237 - let reading_ch = self.chapter as usize; 238 - for &adj in &[reading_ch + 1, reading_ch.saturating_sub(1)] { 239 - if adj < spine_len && adj != reading_ch && !self.ch_cached[adj] { 240 - log::info!( 241 - "epub: priority cache ch{} (adjacent to ch{})", 242 - adj, 243 - reading_ch, 244 - ); 245 - if let Err(e) = self.epub_cache_single_chapter(k, adj) { 246 - log::warn!("epub: priority cache ch{} failed: {}", adj, e); 247 - } 248 - } 249 - } 250 - 251 - let ch = self.cache_chapter as usize; 252 - if ch >= spine_len { 253 - return self.epub_finish_cache(k); 254 - } 255 - 256 - // large chapters need ~2x their uncompressed size in heap 257 - // (extract Vec + strip output Vec simultaneously); on a 140 KB 258 - // heap anything over ~32 KB risks OOM in the worker; fall back 259 - // to the streaming pipeline which uses fixed ~51 KB overhead 260 - const ASYNC_THRESHOLD: u32 = 32768; 261 - let entry_idx = self.spine.items[ch] as usize; 262 - let uncomp = self.zip.entry(entry_idx).uncomp_size; 263 - if uncomp > ASYNC_THRESHOLD { 264 - log::info!( 265 - "epub: ch{}/{} large ({} bytes), sync-caching", 266 - ch, 267 - spine_len, 268 - uncomp, 269 - ); 270 - self.epub_cache_single_chapter(k, ch)?; 271 - self.cache_chapter += 1; 272 - return Ok(true); 273 - } 274 - 275 - let dir_buf = self.cache_dir; 276 - let dir = cache::dir_name_str(&dir_buf); 277 - let ch_file = cache::chapter_file_name(ch as u16); 278 - let ch_str = cache::chapter_file_str(&ch_file); 279 - 280 - // truncate any stale data before the worker produces output 281 - k.sync_write_app_subdir(dir, ch_str, &[])?; 282 - 283 - let (nb, nl) = self.name_copy(); 284 - let epub_name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 285 - 286 - // extract full XHTML into memory; if OOM fall back to sync 287 - let xhtml = match super::extract_zip_entry(k, epub_name, &self.zip, entry_idx) { 288 - Ok(data) => data, 289 - Err(e) => { 290 - log::info!( 291 - "epub: ch{}/{} extract failed ({}), sync-caching", 292 - ch, 293 - spine_len, 294 - e, 295 - ); 296 - self.epub_cache_single_chapter(k, ch)?; 297 - self.cache_chapter += 1; 298 - return Ok(true); 299 - } 300 - }; 301 - 302 - log::info!( 303 - "epub: dispatch ch{}/{} ({} bytes XHTML) to worker", 304 - ch, 305 - spine_len, 306 - xhtml.len() 307 - ); 308 - 309 - let task = work_queue::WorkTask::StripChapter { 310 - chapter_idx: ch as u16, 311 - xhtml, 312 - }; 313 - if !work_queue::submit(self.work_gen, task) { 314 - return Err("cache: worker channel full"); 315 - } 316 - Ok(true) 317 - } 318 - 319 - // poll worker for a completed chapter-strip result 320 - pub(super) fn epub_recv_chapter_strip( 321 - &mut self, 322 - k: &mut KernelHandle<'_>, 323 - ) -> Result<Option<bool>, &'static str> { 324 - let result = match work_queue::try_recv() { 325 - Some(r) if r.is_current() => r, 326 - Some(_) => return Ok(None), // stale generation -- discard 327 - None => return Ok(None), // worker still busy 328 - }; 329 - 330 - match result.outcome { 331 - work_queue::WorkOutcome::ChapterReady { chapter_idx, text } => { 332 - let ch = chapter_idx as usize; 333 - let text_size = text.len() as u32; 334 - 335 - // if the user sync-cached this chapter while the worker 336 - // was processing, skip the SD write 337 - if !self.ch_cached[ch] { 338 - let dir_buf = self.cache_dir; 339 - let dir = cache::dir_name_str(&dir_buf); 340 - let ch_file = cache::chapter_file_name(chapter_idx); 341 - let ch_str = cache::chapter_file_str(&ch_file); 342 - 343 - k.sync_write_app_subdir(dir, ch_str, &text)?; 344 - self.chapter_sizes[ch] = text_size; 345 - } 346 - self.ch_cached[ch] = true; 347 - drop(text); 348 - 349 - log::info!( 350 - "epub: cached ch{}/{} = {} bytes", 351 - ch, 352 - self.spine.len(), 353 - text_size 354 - ); 355 - 356 - self.cache_chapter += 1; 357 - 358 - if (self.cache_chapter as usize) < self.spine.len() { 359 - Ok(Some(true)) 360 - } else { 361 - self.epub_finish_cache(k)?; 362 - Ok(Some(false)) 363 - } 364 - } 365 - work_queue::WorkOutcome::ChapterFailed { chapter_idx, error } => { 366 - let ch = chapter_idx as usize; 367 - log::warn!( 368 - "epub: worker failed ch{} ({}), falling back to sync", 369 - ch, 370 - error, 371 - ); 372 - // streaming pipeline uses fixed ~51 KB overhead -- won't OOM 373 - if let Err(e) = self.epub_cache_single_chapter(k, ch) { 374 - log::warn!("epub: sync fallback also failed ch{}: {}", ch, e); 375 - } 376 - self.cache_chapter += 1; 377 - 378 - if (self.cache_chapter as usize) < self.spine.len() { 379 - Ok(Some(true)) 380 - } else { 381 - self.epub_finish_cache(k)?; 382 - Ok(Some(false)) 383 - } 384 - } 385 - _ => { 386 - // unexpected result type -- discard and keep waiting 387 - log::warn!("epub: unexpected result while waiting for chapter strip"); 388 - Ok(None) 389 - } 390 - } 391 - } 392 - 393 259 pub(super) fn epub_index_chapter(&mut self) { 394 260 self.reset_paging(); 395 - // force reload -- ch_cache may hold a different chapter's data 261 + // force reload; ch_cache may hold a different chapter's data 396 262 // with the same byte count (try_cache_chapter only checks len) 397 263 self.ch_cache = Vec::new(); 398 264 let ch = self.chapter as usize; ··· 446 312 let mut pos = 0usize; 447 313 while pos < ch_size { 448 314 let chunk = (ch_size - pos).min(PAGE_BUF); 449 - match k.sync_read_app_subdir_chunk( 315 + match k.read_app_subdir_chunk( 450 316 dir, 451 317 ch_str, 452 318 pos as u32, ··· 470 336 true 471 337 } 472 338 473 - // run one step of background caching; returns true if self.buf was dirtied 474 - pub(super) fn bg_cache_step(&mut self, k: &mut KernelHandle<'_>) -> bool { 339 + // run one step of background caching; async because CacheChapter 340 + // awaits epub_cache_chapter_async which yields during deflate 341 + pub(super) async fn bg_cache_step(&mut self, k: &mut KernelHandle<'_>) { 475 342 match self.bg_cache { 476 343 BgCacheState::CacheChapter => { 477 - match self.epub_dispatch_chapter_strip(k) { 478 - Ok(true) => self.bg_cache = BgCacheState::WaitChapter, 479 - Ok(false) => { 480 - // all chapters cached; start image scan from 481 - // the current reading chapter 482 - self.img_cache_ch = self.chapter; 483 - self.img_cache_offset = 0; 484 - self.img_scan_wrapped = false; 485 - self.bg_cache = BgCacheState::CacheImage; 486 - } 487 - Err(e) => { 488 - log::warn!("bg: ch dispatch failed: {}, skipping", e); 489 - self.cache_chapter += 1; 490 - // stay in CacheChapter; next tick tries the next one 344 + let spine_len = self.spine.len(); 345 + 346 + // skip chapters already cached 347 + while (self.cache_chapter as usize) < spine_len 348 + && self.ch_cached[self.cache_chapter as usize] 349 + { 350 + self.cache_chapter += 1; 351 + } 352 + 353 + // priority: cache chapters adjacent to reading position 354 + // before continuing the sequential scan; forward/backward 355 + // nav stays instant 356 + let reading_ch = self.chapter as usize; 357 + for &adj in &[reading_ch + 1, reading_ch.saturating_sub(1)] { 358 + if adj < spine_len && adj != reading_ch && !self.ch_cached[adj] { 359 + log::info!( 360 + "epub: priority cache ch{} (adjacent to ch{})", 361 + adj, 362 + reading_ch, 363 + ); 364 + if let Err(e) = self.epub_cache_chapter_async(k, adj).await { 365 + log::warn!("epub: priority ch{} failed: {}", adj, e); 366 + } 491 367 } 492 368 } 493 - false 494 - } 495 - BgCacheState::WaitChapter => { 496 - match self.epub_recv_chapter_strip(k) { 497 - Ok(Some(true)) => { 498 - // after caching a chapter, try dispatching a nearby 499 - // image before continuing with the next chapter 369 + 370 + let ch = self.cache_chapter as usize; 371 + if ch >= spine_len { 372 + let _ = self.epub_finish_cache(k); 373 + self.img_cache_ch = self.chapter; 374 + self.img_cache_offset = 0; 375 + self.img_scan_wrapped = false; 376 + self.bg_cache = BgCacheState::CacheImage; 377 + return; 378 + } 379 + 380 + match self.epub_cache_chapter_async(k, ch).await { 381 + Ok(()) => { 382 + self.cache_chapter += 1; 383 + // try nearby image dispatch before next chapter 500 384 if self.try_dispatch_nearby_image(k) { 501 385 self.bg_cache = BgCacheState::WaitNearbyImage; 502 - } else { 503 - self.bg_cache = BgCacheState::CacheChapter; 504 386 } 505 - } 506 - Ok(Some(false)) => { 507 - self.img_cache_ch = self.chapter; 508 - self.img_cache_offset = 0; 509 - self.img_scan_wrapped = false; 510 - self.bg_cache = BgCacheState::CacheImage; 387 + // else stay in CacheChapter 511 388 } 512 - Ok(None) => {} 513 389 Err(e) => { 514 - log::warn!("bg: ch recv failed: {}, continuing", e); 515 - self.bg_cache = BgCacheState::CacheChapter; 390 + log::warn!("bg: ch{} failed: {}, skipping", ch, e); 391 + self.cache_chapter += 1; 516 392 } 517 393 } 518 - false 519 394 } 395 + 520 396 BgCacheState::WaitNearbyImage => { 521 397 match self.epub_recv_image_result(k) { 522 398 Ok(Some(_)) => { ··· 532 408 self.bg_cache = BgCacheState::CacheChapter; 533 409 } 534 410 } 535 - false 536 411 } 537 412 BgCacheState::CacheImage => { 538 413 match self.epub_find_and_dispatch_image(k) { ··· 549 424 // stay in CacheImage; next tick scans for the next one 550 425 } 551 426 } 552 - // image scanning uses the prefetch buffer, leaving 553 - // self.buf (current page data) untouched 554 - false 555 427 } 556 - BgCacheState::WaitImage => { 557 - match self.epub_recv_image_result(k) { 558 - Ok(Some(_)) => self.bg_cache = BgCacheState::CacheImage, 559 - Ok(None) => {} 560 - Err(e) => { 561 - log::warn!("bg: image recv error: {}", e); 562 - self.bg_cache = BgCacheState::CacheImage; 563 - } 428 + BgCacheState::WaitImage => match self.epub_recv_image_result(k) { 429 + Ok(Some(_)) => self.bg_cache = BgCacheState::CacheImage, 430 + Ok(None) => {} 431 + Err(e) => { 432 + log::warn!("bg: image recv error: {}", e); 433 + self.bg_cache = BgCacheState::CacheImage; 564 434 } 565 - false 566 - } 567 - BgCacheState::Idle => false, 435 + }, 436 + BgCacheState::Idle => {} 568 437 } 569 438 } 570 439 }
+97 -45
src/apps/reader/images.rs
··· 17 17 use smol_epub::html_strip::{IMG_REF, MARKER}; 18 18 use smol_epub::zip::{self, ZipIndex}; 19 19 20 + use crate::error::{Error, ErrorKind}; 20 21 use crate::kernel::KernelHandle; 21 22 use crate::kernel::work_queue; 22 23 ··· 113 114 return; 114 115 } 115 116 117 + // background precache will decode this image eventually; 118 + // skip the blocking inline decode so the page renders 119 + // immediately without it. once the user is in Ready state 120 + // defer_image_decode is cleared and a page revisit will 121 + // either hit the cache or do the full decode. 122 + if self.defer_image_decode { 123 + log::info!( 124 + "reader: deferring image decode (bg will handle {})", 125 + full_path 126 + ); 127 + return; 128 + } 129 + 116 130 let zip_idx = match self 117 131 .zip 118 132 .find(full_path) ··· 131 145 132 146 let data_offset = { 133 147 let mut hdr = [0u8; 30]; 134 - if k.sync_read_chunk(epub_name, entry.local_offset, &mut hdr) 148 + if k.read_chunk(epub_name, entry.local_offset, &mut hdr) 135 149 .is_err() 136 150 { 137 151 log::warn!("reader: failed to read ZIP local header"); ··· 157 171 } else if entry.method == zip::METHOD_STORED { 158 172 let mut magic = [0u8; 8]; 159 173 let n = k 160 - .sync_read_chunk(epub_name, data_offset, &mut magic) 174 + .read_chunk(epub_name, data_offset, &mut magic) 161 175 .unwrap_or(0); 162 176 ( 163 177 n >= 2 && magic[0] == 0xFF && magic[1] == 0xD8, ··· 180 194 181 195 let do_decode = |k_ref: &mut KernelHandle<'_>| -> Result<DecodedImage, &'static str> { 182 196 let k_cell = RefCell::new(k_ref); 197 + let read_err = |e: Error| -> &'static str { e.into() }; 183 198 if is_jpeg && entry.method == zip::METHOD_STORED { 184 199 smol_epub::jpeg::decode_jpeg_sd( 185 - |off, buf| k_cell.borrow_mut().sync_read_chunk(epub_name, off, buf), 200 + |off, buf| { 201 + k_cell 202 + .borrow_mut() 203 + .read_chunk(epub_name, off, buf) 204 + .map_err(read_err) 205 + }, 186 206 data_offset, 187 207 entry.uncomp_size, 188 208 TEXT_W as u16, ··· 190 210 ) 191 211 } else if is_jpeg { 192 212 smol_epub::jpeg::decode_jpeg_deflate_sd( 193 - |off, buf| k_cell.borrow_mut().sync_read_chunk(epub_name, off, buf), 213 + |off, buf| { 214 + k_cell 215 + .borrow_mut() 216 + .read_chunk(epub_name, off, buf) 217 + .map_err(read_err) 218 + }, 194 219 data_offset, 195 220 entry.comp_size, 196 221 entry.uncomp_size, ··· 199 224 ) 200 225 } else if entry.method == zip::METHOD_STORED { 201 226 smol_epub::png::decode_png_sd( 202 - |off, buf| k_cell.borrow_mut().sync_read_chunk(epub_name, off, buf), 227 + |off, buf| { 228 + k_cell 229 + .borrow_mut() 230 + .read_chunk(epub_name, off, buf) 231 + .map_err(read_err) 232 + }, 203 233 data_offset, 204 234 entry.uncomp_size, 205 235 TEXT_W as u16, ··· 207 237 ) 208 238 } else { 209 239 smol_epub::png::decode_png_deflate_sd( 210 - |off, buf| k_cell.borrow_mut().sync_read_chunk(epub_name, off, buf), 240 + |off, buf| { 241 + k_cell 242 + .borrow_mut() 243 + .read_chunk(epub_name, off, buf) 244 + .map_err(read_err) 245 + }, 211 246 data_offset, 212 247 entry.comp_size, 213 248 TEXT_W as u16, ··· 263 298 k: &mut KernelHandle<'_>, 264 299 ch: usize, 265 300 start_offset: usize, 266 - ) -> Result<ScanResult, &'static str> { 301 + ) -> crate::error::Result<ScanResult> { 267 302 if ch >= cache::MAX_CACHE_CHAPTERS || !self.ch_cached[ch] { 268 303 return Ok(ScanResult::NoneFound); 269 304 } ··· 285 320 let mut offset = start_offset; 286 321 while offset < ch_size { 287 322 let read_len = PAGE_BUF.min(ch_size - offset); 288 - let n = k.sync_read_app_subdir_chunk( 323 + let n = k.read_app_subdir_chunk( 289 324 dir, 290 325 ch_str, 291 326 offset as u32, ··· 341 376 let resume = (offset + path_start + path_len) as u32; 342 377 343 378 // already cached or skip-marked 344 - if k.sync_file_size_app_subdir(dir, img_file).is_ok() { 379 + if k.file_size_app_subdir(dir, img_file).is_ok() { 345 380 i = path_start + path_len; 346 381 continue; 347 382 } ··· 351 386 352 387 if !is_jpeg && !is_png { 353 388 log::info!("precache: skip unsupported: {}", full_path); 354 - let _ = k.sync_write_app_subdir(dir, img_file, &[]); 389 + let _ = k.write_app_subdir(dir, img_file, &[]); 355 390 i = path_start + path_len; 356 391 continue; 357 392 } ··· 397 432 } 398 433 Err(e) => { 399 434 log::warn!("precache: streaming failed: {}", e); 400 - let _ = k.sync_write_app_subdir(dir, img_file, &[]); 435 + let _ = k.write_app_subdir(dir, img_file, &[]); 401 436 } 402 437 } 403 438 return Ok(ScanResult::DecodedInline { ··· 405 440 }); 406 441 } 407 442 443 + // wait for worker to have capacity before expensive 444 + // extraction; caller sees Dispatched + worker busy 445 + // and transitions to WaitImage, retrying after drain 446 + if !work_queue::is_idle() { 447 + return Ok(ScanResult::Dispatched { 448 + resume_offset: (offset + i) as u32, 449 + }); 450 + } 451 + 408 452 // small images: extract to memory for worker dispatch 409 453 let data = match super::extract_zip_entry(k, epub_name, &self.zip, zip_idx) { 410 454 Ok(d) => d, 411 455 Err(e) => { 412 456 log::warn!("precache: extract failed: {}", e); 413 - let _ = k.sync_write_app_subdir(dir, img_file, &[]); 457 + let _ = k.write_app_subdir(dir, img_file, &[]); 414 458 i = path_start + path_len; 415 459 continue; 416 460 } ··· 430 474 resume_offset: resume, 431 475 }); 432 476 } 433 - return Err("cache: worker channel full"); 477 + // queue full despite idle check; skip this image, 478 + // it will be decoded on demand if the user views it 479 + log::warn!("precache: worker queue full, skipping {}", full_path); 480 + i = path_start + path_len; 481 + continue; 434 482 } 435 483 436 484 // advance with overlap so markers at chunk boundaries are not missed ··· 449 497 pub(super) fn epub_find_and_dispatch_image( 450 498 &mut self, 451 499 k: &mut KernelHandle<'_>, 452 - ) -> Result<bool, &'static str> { 500 + ) -> crate::error::Result<bool> { 453 501 let spine_len = self.spine.len(); 454 502 455 503 while (self.img_cache_ch as usize) < spine_len { ··· 493 541 pub(super) fn epub_recv_image_result( 494 542 &mut self, 495 543 k: &mut KernelHandle<'_>, 496 - ) -> Result<Option<bool>, &'static str> { 544 + ) -> crate::error::Result<Option<bool>> { 497 545 let result = match work_queue::try_recv() { 498 546 Some(r) if r.is_current() => r, 499 - Some(_) => return Ok(None), // stale generation -- discard 547 + Some(_) => return Ok(None), // stale generation; discard 500 548 None => return Ok(None), 501 549 }; 502 550 ··· 524 572 log::warn!("precache: image {:#010X} failed: {}", path_hash, error); 525 573 Ok(Some(true)) 526 574 } 527 - _ => { 528 - log::warn!("precache: unexpected result while waiting for image decode"); 529 - Ok(None) 530 - } 531 575 } 532 576 } 533 577 ··· 602 646 is_jpeg: bool, 603 647 max_w: u16, 604 648 max_h: u16, 605 - ) -> Result<DecodedImage, &'static str> { 649 + ) -> crate::error::Result<DecodedImage> { 606 650 let mut hdr = [0u8; 30]; 607 - k.sync_read_chunk(epub_name, entry.local_offset, &mut hdr) 608 - .map_err(|_| "read local header failed")?; 609 - let skip = ZipIndex::local_header_data_skip(&hdr)?; 651 + k.read_chunk(epub_name, entry.local_offset, &mut hdr)?; 652 + let skip = ZipIndex::local_header_data_skip(&hdr) 653 + .map_err(|_| Error::new(ErrorKind::ParseFailed, "decode_image: local header"))?; 610 654 let data_offset = entry.local_offset + skip; 611 655 612 - if is_jpeg && entry.method == zip::METHOD_STORED { 656 + let read_err = |_: Error| -> &'static str { "read failed" }; 657 + 658 + let result = if is_jpeg && entry.method == zip::METHOD_STORED { 613 659 smol_epub::jpeg::decode_jpeg_sd( 614 - |off, buf| k.sync_read_chunk(epub_name, off, buf), 660 + |off, buf| k.read_chunk(epub_name, off, buf).map_err(read_err), 615 661 data_offset, 616 662 entry.uncomp_size, 617 663 max_w, ··· 619 665 ) 620 666 } else if is_jpeg { 621 667 smol_epub::jpeg::decode_jpeg_deflate_sd( 622 - |off, buf| k.sync_read_chunk(epub_name, off, buf), 668 + |off, buf| k.read_chunk(epub_name, off, buf).map_err(read_err), 623 669 data_offset, 624 670 entry.comp_size, 625 671 entry.uncomp_size, ··· 628 674 ) 629 675 } else if entry.method == zip::METHOD_STORED { 630 676 smol_epub::png::decode_png_sd( 631 - |off, buf| k.sync_read_chunk(epub_name, off, buf), 677 + |off, buf| k.read_chunk(epub_name, off, buf).map_err(read_err), 632 678 data_offset, 633 679 entry.uncomp_size, 634 680 max_w, ··· 636 682 ) 637 683 } else { 638 684 smol_epub::png::decode_png_deflate_sd( 639 - |off, buf| k.sync_read_chunk(epub_name, off, buf), 685 + |off, buf| k.read_chunk(epub_name, off, buf).map_err(read_err), 640 686 data_offset, 641 687 entry.comp_size, 642 688 max_w, 643 689 max_h, 644 690 ) 645 - } 691 + }; 692 + result.map_err(|msg| Error::from(msg).with_source("decode_image_streaming")) 646 693 } 647 694 648 695 pub(super) fn load_cached_image( 649 696 k: &mut KernelHandle<'_>, 650 697 dir: &str, 651 698 name: &str, 652 - ) -> Result<DecodedImage, &'static str> { 653 - let size = k 654 - .sync_file_size_app_subdir(dir, name) 655 - .map_err(|_| "no cache file")?; 699 + ) -> crate::error::Result<DecodedImage> { 700 + let size = k.file_size_app_subdir(dir, name)?; 656 701 if size < 5 { 657 - return Err("cache file too small"); 702 + return Err(Error::new( 703 + ErrorKind::InvalidData, 704 + "load_cached_image: too small", 705 + )); 658 706 } 659 707 let mut header = [0u8; 4]; 660 - k.sync_read_app_subdir_chunk(dir, name, 0, &mut header) 661 - .map_err(|_| "read header failed")?; 708 + k.read_app_subdir_chunk(dir, name, 0, &mut header)?; 662 709 let width = u16::from_le_bytes([header[0], header[1]]); 663 710 let height = u16::from_le_bytes([header[2], header[3]]); 664 711 if width == 0 || height == 0 { 665 - return Err("zero dimensions in cache"); 712 + return Err(Error::new( 713 + ErrorKind::InvalidData, 714 + "load_cached_image: zero dimensions", 715 + )); 666 716 } 667 717 let stride = (width as usize).div_ceil(8); 668 718 let data_len = stride * height as usize; 669 719 if size as usize != 4 + data_len { 670 - return Err("cache size mismatch"); 720 + return Err(Error::new( 721 + ErrorKind::InvalidData, 722 + "load_cached_image: size mismatch", 723 + )); 671 724 } 672 725 let mut data = Vec::new(); 673 726 data.try_reserve_exact(data_len) 674 - .map_err(|_| "OOM for cached image")?; 727 + .map_err(|_| Error::new(ErrorKind::OutOfMemory, "load_cached_image"))?; 675 728 data.resize(data_len, 0); 676 - k.sync_read_app_subdir_chunk(dir, name, 4, &mut data) 677 - .map_err(|_| "read data failed")?; 729 + k.read_app_subdir_chunk(dir, name, 4, &mut data)?; 678 730 Ok(DecodedImage { 679 731 width, 680 732 height, ··· 688 740 dir: &str, 689 741 name: &str, 690 742 img: &DecodedImage, 691 - ) -> Result<(), &'static str> { 743 + ) -> crate::error::Result<()> { 692 744 let mut header = [0u8; 4]; 693 745 header[0..2].copy_from_slice(&img.width.to_le_bytes()); 694 746 header[2..4].copy_from_slice(&img.height.to_le_bytes()); 695 - k.sync_write_app_subdir(dir, name, &header)?; 696 - k.sync_append_app_subdir(dir, name, &img.data)?; 747 + k.write_app_subdir(dir, name, &header)?; 748 + k.append_app_subdir(dir, name, &img.data)?; 697 749 Ok(()) 698 750 }
+123 -50
src/apps/reader/mod.rs
··· 23 23 use crate::board::action::{Action, ActionEvent}; 24 24 use crate::board::{SCREEN_H, SCREEN_W}; 25 25 use crate::drivers::strip::StripBuffer; 26 + use crate::error::{Error, ErrorKind}; 26 27 use crate::fonts; 27 28 use crate::kernel::KernelHandle; 28 29 use crate::kernel::QuickAction; 29 30 use crate::kernel::bookmarks; 30 31 use crate::kernel::work_queue; 31 - use crate::ui::{Alignment, BUTTON_BAR_H, CONTENT_TOP, Region, StackFmt}; 32 + use crate::ui::{Alignment, BUTTON_BAR_H, CONTENT_TOP, Region, StackFmt, draw_progress_bar}; 32 33 use smol_epub::DecodedImage; 33 34 use smol_epub::cache; 34 35 use smol_epub::epub::{self, EpubMeta, EpubSpine, EpubToc, TocSource}; ··· 79 80 ); 80 81 81 82 pub(super) const LOADING_REGION: Region = Region::new(MARGIN, TEXT_Y, 464, 20); 83 + pub(super) const LOADING_BAR_REGION: Region = Region::new(MARGIN, TEXT_Y + 26, 200, 8); 82 84 83 85 pub const QA_FONT_SIZE: u8 = 1; 84 86 pub(super) const QA_PREV_CHAPTER: u8 = 3; ··· 87 89 88 90 pub(super) const QA_MAX: usize = 4; 89 91 90 - #[derive(Clone, Copy, PartialEq)] 92 + #[derive(Clone, Copy, PartialEq, Debug)] 91 93 pub(super) enum State { 92 94 NeedBookmark, 93 95 NeedInit, ··· 101 103 Error, 102 104 } 103 105 106 + impl State { 107 + pub(super) fn loading_pct(self) -> u8 { 108 + match self { 109 + Self::NeedBookmark => 0, 110 + Self::NeedInit => 5, 111 + Self::NeedOpf => 15, 112 + Self::NeedToc => 30, 113 + Self::NeedCache => 45, 114 + Self::NeedIndex => 70, 115 + Self::NeedPage => 90, 116 + Self::Ready | Self::ShowToc | Self::Error => 100, 117 + } 118 + } 119 + } 120 + 104 121 // background caching progress, runs independently of the reading 105 122 // state so the user can read while chapters/images are cached 106 123 #[derive(Clone, Copy, PartialEq)] ··· 108 125 // nothing to do 109 126 Idle, 110 127 CacheChapter, 111 - WaitChapter, 112 128 WaitNearbyImage, 113 129 CacheImage, 114 130 WaitImage, ··· 190 206 pub(super) prefetch_page: usize, 191 207 192 208 pub(super) state: State, 193 - pub(super) error: Option<&'static str>, 209 + pub(super) error: Option<Error>, 194 210 pub(super) show_position: bool, 195 211 196 212 pub(super) is_epub: bool, ··· 218 234 pub(super) ch_cache: Vec<u8>, 219 235 pub(super) page_img: Option<DecodedImage>, 220 236 pub(super) fullscreen_img: bool, 237 + pub(super) defer_image_decode: bool, 221 238 pub(super) toc: EpubToc, 222 239 pub(super) toc_source: Option<TocSource>, 223 240 pub(super) toc_selected: usize, ··· 289 306 290 307 page_img: None, 291 308 fullscreen_img: false, 309 + defer_image_decode: false, 292 310 293 311 toc: EpubToc::new(), 294 312 toc_source: None, ··· 325 343 self.is_epub && self.bg_cache != BgCacheState::Idle 326 344 } 327 345 328 - // run one step of background caching while suspended 346 + // run one step of image work queue polling while suspended; 347 + // chapter caching is async and only runs during active background, 348 + // so this only handles the sync image recv states 329 349 pub fn bg_work_tick(&mut self, k: &mut KernelHandle<'_>) { 330 - if self.bg_cache != BgCacheState::Idle { 331 - self.bg_cache_step(k); 350 + match self.bg_cache { 351 + BgCacheState::WaitNearbyImage => match self.epub_recv_image_result(k) { 352 + Ok(Some(_)) => { 353 + if !self.try_dispatch_nearby_image(k) { 354 + self.bg_cache = BgCacheState::CacheChapter; 355 + } 356 + } 357 + Ok(None) => {} 358 + Err(e) => { 359 + log::warn!("bg: nearby image error (suspended): {}", e); 360 + self.bg_cache = BgCacheState::CacheChapter; 361 + } 362 + }, 363 + BgCacheState::WaitImage => match self.epub_recv_image_result(k) { 364 + Ok(Some(_)) => self.bg_cache = BgCacheState::CacheImage, 365 + Ok(None) => {} 366 + Err(e) => { 367 + log::warn!("bg: image recv error (suspended): {}", e); 368 + self.bg_cache = BgCacheState::CacheImage; 369 + } 370 + }, 371 + _ => {} 332 372 } 333 373 } 334 374 ··· 468 508 name: &str, 469 509 offset: u32, 470 510 buf: &mut [u8], 471 - ) -> Result<(), &'static str> { 511 + ) -> crate::error::Result<()> { 472 512 let mut total = 0usize; 473 513 while total < buf.len() { 474 - let n = k.sync_read_chunk(name, offset + total as u32, &mut buf[total..])?; 514 + let n = k.read_chunk(name, offset + total as u32, &mut buf[total..])?; 475 515 if n == 0 { 476 - return Err("epub: unexpected EOF"); 516 + return Err(Error::new( 517 + ErrorKind::ReadFailed, 518 + "read_full: unexpected EOF", 519 + )); 477 520 } 478 521 total += n; 479 522 } ··· 491 534 let entry = zip_index.entry(entry_idx); 492 535 let k = RefCell::new(k); 493 536 zip::extract_entry(entry, entry.local_offset, |offset, buf| { 494 - k.borrow_mut().sync_read_chunk(name, offset, buf) 537 + k.borrow_mut() 538 + .read_chunk(name, offset, buf) 539 + .map_err(|e: Error| -> &'static str { e.into() }) 495 540 }) 496 541 } 497 542 ··· 523 568 } 524 569 525 570 impl App<AppId> for ReaderApp { 526 - async fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 571 + fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 527 572 let msg = ctx.message(); 528 573 let len = msg.len().min(32); 529 574 self.filename[..len].copy_from_slice(&msg[..len]); ··· 549 594 self.chapter = 0; 550 595 self.error = None; 551 596 self.show_position = false; 597 + self.defer_image_decode = true; 552 598 self.goto_last_page = false; 553 599 self.restore_offset = None; 554 600 ··· 589 635 // task runs independently and our work_gen stays valid 590 636 } 591 637 592 - async fn on_resume(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 638 + fn on_resume(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 593 639 // Restore our generation so the worker considers in-flight 594 640 // results current again (another app may have submitted work 595 641 // under a different generation while we were suspended). ··· 616 662 State::NeedBookmark => { 617 663 self.bookmark_load(k.bookmark_cache()); 618 664 619 - let _ = k.sync_write_app_data(RECENT_FILE, &self.filename[..self.filename_len]); 665 + let _ = k.write_app_data(RECENT_FILE, &self.filename[..self.filename_len]); 620 666 621 667 if self.is_epub { 622 668 self.zip.clear(); ··· 633 679 634 680 State::NeedInit => match self.epub_init_zip(k) { 635 681 Ok(()) => { 636 - self.state = State::NeedOpf; // yield; CD heap freed 682 + self.state = State::NeedOpf; 683 + ctx.mark_dirty(LOADING_BAR_REGION); 637 684 } 638 685 Err(e) => { 639 686 log::info!("reader: epub init (zip) failed: {}", e); ··· 645 692 646 693 State::NeedOpf => match self.epub_init_opf(k) { 647 694 Ok(()) => { 648 - self.state = State::NeedToc; // yield; OPF heap freed 695 + self.state = State::NeedToc; 696 + ctx.mark_dirty(LOADING_BAR_REGION); 649 697 } 650 698 Err(e) => { 651 699 log::info!("reader: epub init (opf) failed: {}", e); ··· 684 732 ); 685 733 log::info!("epub: TOC has {} entries", self.toc.len()); 686 734 } 687 - Err(e) => { 688 - log::warn!("epub: failed to read TOC: {}", e); 735 + Err(_e) => { 736 + log::warn!("epub: failed to read TOC"); 689 737 } 690 738 } 691 739 } 692 740 self.rebuild_quick_actions(); 693 741 self.state = State::NeedCache; 694 - continue; 742 + // break instead of continue so the progress bar 743 + // renders before potentially heavy chapter caching; 744 + // for cached books this mark coalesces with Ready 745 + // because both complete during the initial waveform 746 + ctx.mark_dirty(LOADING_BAR_REGION); 695 747 } 696 748 697 749 State::NeedCache => match self.epub_check_cache(k) { ··· 700 752 continue; 701 753 } 702 754 Ok(false) => { 703 - // Cache only the current chapter synchronously 704 - // so the user can start reading immediately. 755 + // cache the current chapter; async version yields 756 + // during deflate so the scheduler's select can 757 + // interrupt if the user presses back 705 758 let ch = self.chapter as usize; 706 - match self.epub_cache_single_chapter(k, ch) { 759 + match self.epub_cache_chapter_async(k, ch).await { 707 760 Ok(()) => { 708 761 self.chapters_cached = true; 709 762 self.cache_chapter = 0; 710 763 711 - // Eagerly dispatch nearby images to 764 + // eagerly dispatch nearby images to 712 765 // the worker so they decode while the 713 - // user reads the first page. The 714 - // worker is idle at this point so the 715 - // dispatch is immediate. 766 + // user reads the first page 716 767 if self.try_dispatch_nearby_image(k) { 717 768 self.bg_cache = BgCacheState::WaitNearbyImage; 718 769 } else { ··· 723 774 continue; 724 775 } 725 776 Err(e) => { 726 - log::info!("reader: sync cache ch{} failed: {}", ch, e); 777 + log::info!("reader: cache ch{} failed: {}", ch, e); 727 778 self.error = Some(e); 728 779 self.state = State::Error; 729 780 ctx.mark_dirty(PAGE_REGION); ··· 739 790 }, 740 791 741 792 State::NeedIndex => { 742 - // Ensure the target chapter is cached before 793 + // ensure the target chapter is cached before 743 794 // indexing (it may not be if background caching 744 - // hasn't reached it yet). 795 + // hasn't reached it yet) 745 796 if self.is_epub 746 797 && self.chapters_cached 747 798 && !self.ch_cached[self.chapter as usize] 748 799 { 749 - if let Err(e) = self.epub_cache_single_chapter(k, self.chapter as usize) { 800 + // async version yields during deflate so the 801 + // scheduler's select can interrupt on input 802 + if let Err(e) = self 803 + .epub_cache_chapter_async(k, self.chapter as usize) 804 + .await 805 + { 750 806 self.error = Some(e); 751 807 self.state = State::Error; 752 808 ctx.mark_dirty(PAGE_REGION); ··· 766 822 if want_last { 767 823 match self.scan_to_last_page(k) { 768 824 Ok(()) => { 825 + self.defer_image_decode = false; 769 826 self.state = State::Ready; 770 827 ctx.mark_dirty(PAGE_REGION); 771 828 } ··· 803 860 self.page += 1; 804 861 } 805 862 if self.state != State::Error { 863 + self.defer_image_decode = false; 806 864 self.state = State::Ready; 807 865 ctx.mark_dirty(PAGE_REGION); 808 866 } 809 867 } else { 810 868 match self.load_and_prefetch(k) { 811 869 Ok(()) => { 870 + self.defer_image_decode = false; 812 871 self.state = State::Ready; 813 872 ctx.mark_dirty(PAGE_REGION); 814 873 } ··· 827 886 break; 828 887 } 829 888 830 - // background caching (runs while the user reads) 831 - // runs in any stable state -- page turns momentarily leave 832 - // Ready, but background work resumes on the next tick 833 - if matches!(self.state, State::Ready | State::ShowToc) 834 - && self.bg_cache != BgCacheState::Idle 889 + // background caching; runs whenever the page content is 890 + // settled and there is work to do. NeedIndex is included so 891 + // adjacent-chapter caching can overlap with page indexing 892 + // after a chapter jump. the scheduler wraps run_background 893 + // in select(run_background, input) so every .await inside 894 + // bg_cache_step is interruptible by user input. 895 + if matches!( 896 + self.state, 897 + State::Ready | State::ShowToc | State::NeedIndex | State::NeedPage 898 + ) && self.bg_cache != BgCacheState::Idle 835 899 { 836 - self.bg_cache_step(k); 900 + self.bg_cache_step(k).await; 837 901 } 838 902 } 839 903 ··· 842 906 match event { 843 907 ActionEvent::Press(Action::Back) => { 844 908 self.state = State::Ready; 845 - ctx.mark_dirty(PAGE_REGION); 909 + ctx.mark_dirty_immediate(PAGE_REGION); 846 910 return Transition::None; 847 911 } 848 912 ActionEvent::Press(Action::Next) | ActionEvent::Repeat(Action::Next) => { ··· 858 922 if self.toc_selected >= self.toc_scroll + vis { 859 923 self.toc_scroll = self.toc_selected + 1 - vis; 860 924 } 861 - ctx.mark_dirty(PAGE_REGION); 925 + ctx.mark_dirty_immediate(PAGE_REGION); 862 926 } 863 927 return Transition::None; 864 928 } ··· 877 941 if self.toc_selected < self.toc_scroll { 878 942 self.toc_scroll = self.toc_selected; 879 943 } 880 - ctx.mark_dirty(PAGE_REGION); 944 + ctx.mark_dirty_immediate(PAGE_REGION); 881 945 } 882 946 return Transition::None; 883 947 } ··· 893 957 self.page = 0; 894 958 self.goto_last_page = false; 895 959 self.state = State::NeedIndex; 896 - ctx.mark_dirty(PAGE_REGION); 960 + ctx.mark_dirty_immediate(PAGE_REGION); 897 961 } else { 898 962 log::warn!( 899 963 "toc: entry \"{}\" unresolved (spine_idx=0xFFFF), ignoring", 900 964 entry.title_str() 901 965 ); 902 966 self.state = State::Ready; 903 - ctx.mark_dirty(PAGE_REGION); 967 + ctx.mark_dirty_immediate(PAGE_REGION); 904 968 } 905 969 return Transition::None; 906 970 } ··· 917 981 self.show_position = true; 918 982 } 919 983 if self.page_forward() { 920 - ctx.mark_dirty(PAGE_REGION); 984 + ctx.mark_dirty_immediate(PAGE_REGION); 921 985 } 922 986 Transition::None 923 987 } ··· 926 990 self.show_position = true; 927 991 } 928 992 if self.page_backward() { 929 - ctx.mark_dirty(PAGE_REGION); 993 + ctx.mark_dirty_immediate(PAGE_REGION); 930 994 } 931 995 Transition::None 932 996 } ··· 934 998 ActionEvent::Release(Action::Next) | ActionEvent::Release(Action::Prev) => { 935 999 if self.show_position { 936 1000 self.show_position = false; 937 - ctx.mark_dirty(POSITION_OVERLAY); 1001 + ctx.mark_dirty_immediate(POSITION_OVERLAY); 938 1002 } 939 1003 Transition::None 940 1004 } 941 1005 942 1006 ActionEvent::Press(Action::Next) | ActionEvent::Repeat(Action::Next) => { 943 1007 if self.page_forward() { 944 - ctx.mark_dirty(PAGE_REGION); 1008 + ctx.mark_dirty_immediate(PAGE_REGION); 945 1009 } 946 1010 Transition::None 947 1011 } 948 1012 949 1013 ActionEvent::Press(Action::Prev) | ActionEvent::Repeat(Action::Prev) => { 950 1014 if self.page_backward() { 951 - ctx.mark_dirty(PAGE_REGION); 1015 + ctx.mark_dirty_immediate(PAGE_REGION); 952 1016 } 953 1017 Transition::None 954 1018 } 955 1019 956 1020 ActionEvent::Press(Action::NextJump) | ActionEvent::Repeat(Action::NextJump) => { 957 1021 if self.jump_forward() { 958 - ctx.mark_dirty(PAGE_REGION); 1022 + ctx.mark_dirty_immediate(PAGE_REGION); 959 1023 } 960 1024 Transition::None 961 1025 } 962 1026 963 1027 ActionEvent::Press(Action::PrevJump) | ActionEvent::Repeat(Action::PrevJump) => { 964 1028 if self.jump_backward() { 965 - ctx.mark_dirty(PAGE_REGION); 1029 + ctx.mark_dirty_immediate(PAGE_REGION); 966 1030 } 967 1031 Transition::None 968 1032 } ··· 1110 1174 ); 1111 1175 } 1112 1176 1113 - if let Some(msg) = self.error { 1114 - draw_chrome_text(strip, LOADING_REGION, msg, Alignment::CenterLeft, cf); 1177 + if let Some(e) = self.error { 1178 + let mut ebuf = StackFmt::<32>::new(); 1179 + let _ = write!(ebuf, "{}", e); 1180 + draw_chrome_text( 1181 + strip, 1182 + LOADING_REGION, 1183 + ebuf.as_str(), 1184 + Alignment::CenterLeft, 1185 + cf, 1186 + ); 1115 1187 return; 1116 1188 } 1117 1189 ··· 1139 1211 Alignment::CenterLeft, 1140 1212 cf, 1141 1213 ); 1214 + draw_progress_bar(strip, LOADING_BAR_REGION, self.state.loading_pct()); 1142 1215 return; 1143 1216 } 1144 1217
+7 -8
src/apps/reader/paging.rs
··· 99 99 pub(super) fn load_and_prefetch( 100 100 &mut self, 101 101 k: &mut KernelHandle<'_>, 102 - ) -> Result<(), &'static str> { 102 + ) -> crate::error::Result<()> { 103 103 if !self.ch_cache.is_empty() { 104 104 let start = (self.offsets[self.page] as usize).min(self.ch_cache.len()); 105 105 let end = (start + PAGE_BUF).min(self.ch_cache.len()); ··· 128 128 let dir = cache::dir_name_str(&dir_buf); 129 129 let ch_file = cache::chapter_file_name(self.chapter); 130 130 let ch_str = cache::chapter_file_str(&ch_file); 131 - let n = 132 - k.sync_read_app_subdir_chunk(dir, ch_str, self.offsets[self.page], &mut self.buf)?; 131 + let n = k.read_app_subdir_chunk(dir, ch_str, self.offsets[self.page], &mut self.buf)?; 133 132 self.buf_len = n; 134 133 } else if self.file_size == 0 { 135 - let (size, n) = k.sync_read_file_start(name, &mut self.buf)?; 134 + let (size, n) = k.read_file_start(name, &mut self.buf)?; 136 135 self.file_size = size; 137 136 self.buf_len = n; 138 137 log::info!("reader: opened {} ({} bytes)", name, size); ··· 143 142 return Ok(()); 144 143 } 145 144 } else { 146 - let n = k.sync_read_chunk(name, self.offsets[self.page], &mut self.buf)?; 145 + let n = k.read_chunk(name, self.offsets[self.page], &mut self.buf)?; 147 146 self.buf_len = n; 148 147 } 149 148 ··· 170 169 let dir = cache::dir_name_str(&dir_buf); 171 170 let ch_file = cache::chapter_file_name(self.chapter); 172 171 let ch_str = cache::chapter_file_str(&ch_file); 173 - k.sync_read_app_subdir_chunk(dir, ch_str, pf_offset, &mut self.prefetch) 172 + k.read_app_subdir_chunk(dir, ch_str, pf_offset, &mut self.prefetch) 174 173 } else { 175 - k.sync_read_chunk(name, pf_offset, &mut self.prefetch) 174 + k.read_chunk(name, pf_offset, &mut self.prefetch) 176 175 }; 177 176 match pf_result { 178 177 Ok(n) => { ··· 228 227 pub(super) fn scan_to_last_page( 229 228 &mut self, 230 229 k: &mut KernelHandle<'_>, 231 - ) -> Result<(), &'static str> { 230 + ) -> crate::error::Result<()> { 232 231 while !self.fully_indexed && self.total_pages < MAX_PAGES { 233 232 self.page = self.total_pages - 1; 234 233 self.load_and_prefetch(k)?;
+9 -9
src/apps/settings.rs
··· 92 92 self.settings = SystemSettings::defaults(); 93 93 self.wifi = WifiConfig::empty(); 94 94 95 - match k.sync_read_app_data_start(config::SETTINGS_FILE, &mut buf) { 95 + match k.read_app_data_start(config::SETTINGS_FILE, &mut buf) { 96 96 Ok((_size, n)) if n > 0 => { 97 97 parse_settings_txt(&buf[..n], &mut self.settings, &mut self.wifi); 98 98 self.settings.sanitize(); ··· 109 109 fn save(&self, k: &mut KernelHandle<'_>) -> bool { 110 110 let mut buf = [0u8; 512]; 111 111 let len = write_settings_txt(&self.settings, &self.wifi, &mut buf); 112 - match k.sync_write_app_data(config::SETTINGS_FILE, &buf[..len]) { 112 + match k.write_app_data(config::SETTINGS_FILE, &buf[..len]) { 113 113 Ok(_) => { 114 114 log::info!("settings: saved to {}", config::SETTINGS_FILE); 115 115 true ··· 249 249 } 250 250 251 251 impl App<AppId> for SettingsApp { 252 - async fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 252 + fn on_enter(&mut self, ctx: &mut AppContext, _k: &mut KernelHandle<'_>) { 253 253 self.selected = 0; 254 254 self.save_needed = false; 255 255 ctx.mark_dirty(Region::new( ··· 269 269 let old = self.selected; 270 270 self.selected = wrap_next(self.selected, NUM_ITEMS); 271 271 if self.selected != old { 272 - ctx.mark_dirty(self.row_region(old)); 273 - ctx.mark_dirty(self.row_region(self.selected)); 272 + ctx.mark_dirty_immediate(self.row_region(old)); 273 + ctx.mark_dirty_immediate(self.row_region(self.selected)); 274 274 } 275 275 Transition::None 276 276 } ··· 279 279 let old = self.selected; 280 280 self.selected = wrap_prev(self.selected, NUM_ITEMS); 281 281 if self.selected != old { 282 - ctx.mark_dirty(self.row_region(old)); 283 - ctx.mark_dirty(self.row_region(self.selected)); 282 + ctx.mark_dirty_immediate(self.row_region(old)); 283 + ctx.mark_dirty_immediate(self.row_region(self.selected)); 284 284 } 285 285 Transition::None 286 286 } 287 287 288 288 ActionEvent::Press(Action::NextJump) | ActionEvent::Repeat(Action::NextJump) => { 289 289 self.increment(); 290 - ctx.mark_dirty(self.value_region(self.selected)); 290 + ctx.mark_dirty_immediate(self.value_region(self.selected)); 291 291 Transition::None 292 292 } 293 293 294 294 ActionEvent::Press(Action::PrevJump) | ActionEvent::Repeat(Action::PrevJump) => { 295 295 self.decrement(); 296 - ctx.mark_dirty(self.value_region(self.selected)); 296 + ctx.mark_dirty_immediate(self.value_region(self.selected)); 297 297 Transition::None 298 298 } 299 299
+5 -4
src/apps/upload.rs
··· 530 530 } 531 531 Err(e) => { 532 532 info!("upload: delete failed for '{}': {}", name, e); 533 - send_error_response(&mut socket, e).await; 533 + send_error_response(&mut socket, "delete failed").await; 534 534 close_socket(&mut socket).await; 535 535 return ServerEvent::DeleteFailed; 536 536 } ··· 616 616 617 617 info!("upload: receiving file '{}'", name_str); 618 618 619 - storage::write_file(sd, name_str, &[])?; 619 + storage::write_file(sd, name_str, &[]).map_err(|_| "write failed")?; 620 620 621 621 // holdback last end_marker.len() bytes to detect boundary spanning two reads 622 622 ··· 625 625 loop { 626 626 if let Some(pos) = find_subsequence(&work[..filled], end_marker) { 627 627 if pos > 0 { 628 - storage::append_root_file(sd, name_str, &work[..pos])?; 628 + storage::append_root_file(sd, name_str, &work[..pos]) 629 + .map_err(|_| "write failed")?; 629 630 total_written += pos as u32; 630 631 } 631 632 info!("upload: complete, {} bytes written", total_written); ··· 634 635 635 636 if filled > end_marker.len() { 636 637 let safe = filled - end_marker.len(); 637 - storage::append_root_file(sd, name_str, &work[..safe])?; 638 + storage::append_root_file(sd, name_str, &work[..safe]).map_err(|_| "write failed")?; 638 639 total_written += safe as u32; 639 640 640 641 work.copy_within(safe..filled, 0);
+3 -1
src/lib.rs
··· 5 5 extern crate alloc; 6 6 7 7 // kernel crate re-exports -- keeps crate::board, crate::drivers, 8 - // crate::kernel paths working in app code without import changes 8 + // crate::kernel, crate::error paths working in app code without 9 + // import changes 9 10 pub use pulp_kernel::board; 10 11 pub use pulp_kernel::drivers; 12 + pub use pulp_kernel::error; 11 13 pub use pulp_kernel::kernel; 12 14 13 15 pub mod apps;