···33//! The XTEink X4 uses DIO flash mode, freeing GPIO12 (SPIHD) and GPIO13 (SPIWP)
44//! for general use. esp-hal 1.0 doesn't generate peripheral types for GPIO12-17
55//! on ESP32-C3, so we drive the pin via direct register writes.
66+//!
77+//! Only implements `embedded_hal::digital::OutputPin` — enough for SPI CS.
88+69const GPIO_OUT_W1TS: u32 = 0x6000_4008; // Set output high (write-1-to-set)
710const GPIO_OUT_W1TC: u32 = 0x6000_400C; // Set output low (write-1-to-clear)
811const GPIO_ENABLE_W1TS: u32 = 0x6000_4024; // Enable output (write-1-to-set)
912const IO_MUX_BASE: u32 = 0x6000_9000; // IO_MUX register base
1013const IO_MUX_PIN_STRIDE: u32 = 0x04; // Each pin has a 4-byte register
11141212-// Minimal output-only GPIO driver using direct register access.
1515+/// Minimal output-only GPIO driver using direct register access.
1616+///
1717+/// Implements `OutputPin` + `ErrorType` from embedded-hal so it can
1818+/// be used as a chip-select pin with `RefCellDevice` / `ExclusiveDevice`.
1319pub struct RawOutputPin {
1420 mask: u32, // Bit mask for this pin (1 << pin_number)
1521}
16221723impl RawOutputPin {
1818- // Configure a GPIO as push-pull output, initially HIGH.
1919- //
2020- // Safety: Caller must ensure
2121- // - The pin is physically available (not connected to active flash lines)
2222- // - No other driver is controlling the same pin
2424+ /// Configure a GPIO as push-pull output, initially HIGH.
2525+ ///
2626+ /// # Safety
2727+ /// Caller must ensure:
2828+ /// - The pin is physically available (not connected to active flash lines)
2929+ /// - No other driver is controlling the same pin
2330 pub unsafe fn new(pin: u8) -> Self {
2431 let mask = 1u32 << pin;
25322633 // Configure IO_MUX: select GPIO function (function 1), enable output
2734 let mux_reg = (IO_MUX_BASE + pin as u32 * IO_MUX_PIN_STRIDE) as *mut u32;
2828- // Bits [14:12] = FUN_DRV (drive strength, default 2)
2929- // Bits [11:10] = 0 (no pull-up/down)
3030- // Bit [9] = FUN_IE (input enable) = 0
3131- // Bits [2:0] = MCU_SEL (function select) = 1 (GPIO)
3232- //
3333- // read-modify-write to preserve reserved bits, but set function to GPIO.
3434- let val = mux_reg.read_volatile();
3535- let val = (val & !0b111) | 1; // MCU_SEL = 1 (GPIO function)
3636- mux_reg.write_volatile(val);
37353838- // enable output for this pin
3939- // GPIO_FUNCn_OUT_SEL_CFG register (base 0x60004554, stride 4)
4040- let out_sel = (0x6000_4554 + pin as u32 * 4) as *mut u32;
4141- out_sel.write_volatile(0x80); // SIG_OUT = 128 (simple GPIO output)
3636+ // Read-modify-write to preserve reserved bits, set function to GPIO.
3737+ // Bits [2:0] = MCU_SEL = 1 (GPIO function)
3838+ unsafe {
3939+ let val = mux_reg.read_volatile();
4040+ let val = (val & !0b111) | 1;
4141+ mux_reg.write_volatile(val);
42424343- // Enable output
4444- (GPIO_ENABLE_W1TS as *mut u32).write_volatile(mask);
4343+ // Configure GPIO matrix: enable output for this pin
4444+ // GPIO_FUNCn_OUT_SEL_CFG register (base 0x60004554, stride 4)
4545+ let out_sel = (0x6000_4554 + pin as u32 * 4) as *mut u32;
4646+ out_sel.write_volatile(0x80); // SIG_OUT = 128 (simple GPIO output)
4747+4848+ // Enable output
4949+ (GPIO_ENABLE_W1TS as *mut u32).write_volatile(mask);
45504646- // Drive HIGH initially (CS deasserted)
4747- (GPIO_OUT_W1TS as *mut u32).write_volatile(mask);
5151+ // Drive HIGH initially (CS deasserted)
5252+ (GPIO_OUT_W1TS as *mut u32).write_volatile(mask);
5353+ }
48544955 Self { mask }
5056 }
+1-1
src/board/sdcard.rs
···2626pub const SD_INIT_FREQ_HZ: u32 = 400_000;
27272828// Normal operating frequency after init
2929-// TODO: Put this somewhere else?
2929+// TODO: Put this somewhere else?
3030pub const SD_NORMAL_FREQ_HZ: u32 = 20_000_000;
31313232// Wrapper that holds the SdCard + VolumeManager together.
+3-4
src/board/strip.rs
···11-// Strip-based rendering buffer
11+// Strip-based rendering buffer
22//
33// Instead of holding a full 48KB framebuffer in SRAM, we render
44// through a small strip buffer (~4KB) and stream each strip to
55-// the display controller via SPI.
55+// the display controller via SPI.
66// The display is divided into horizontal bands of physical rows.
77// For each band:
88// 1. Clear the strip buffer to white
···26262727pub const STRIP_BUF_SIZE: usize = PHYS_BYTES_PER_ROW * STRIP_ROWS as usize; // 4000
2828pub const STRIP_COUNT: u16 = HEIGHT / STRIP_ROWS; // 12
2929-30293130// A small rendering buffer that covers a physical rectangle of the display.
3231//
···168167 }
169168}
170169171171-// embedded-graphics integration
170170+// embedded-graphics integration
172171173172impl OriginDimensions for StripBuffer {
174173 // Report FULL logical display size.
+29
src/drivers/battery.rs
···11+//! Battery monitoring for XTEink X4
22+//!
33+//! GPIO0 reads battery voltage through an on-board voltage divider (1:1, 100K/100K).
44+//! ADC with 11dB attenuation reads 0-2500mV; multiply by 2 for actual battery voltage.
55+//! Li-ion cell: 4200mV = 100%, 3000mV = 0%.
66+77+/// Voltage divider ratio (100K/100K = 2:1)
88+const DIVIDER_MULT: u32 = 2;
99+1010+/// Li-ion voltage bounds in millivolts
1111+const VBAT_FULL_MV: u32 = 4200;
1212+const VBAT_EMPTY_MV: u32 = 3000;
1313+1414+/// Convert ADC millivolts (post-calibration) to actual battery millivolts.
1515+pub fn adc_to_battery_mv(adc_mv: u16) -> u16 {
1616+ (adc_mv as u32 * DIVIDER_MULT) as u16
1717+}
1818+1919+/// Battery voltage to charge percentage (0-100), linear approximation.
2020+pub fn battery_percentage(battery_mv: u16) -> u8 {
2121+ let mv = battery_mv as u32;
2222+ if mv >= VBAT_FULL_MV {
2323+ 100
2424+ } else if mv <= VBAT_EMPTY_MV {
2525+ 0
2626+ } else {
2727+ ((mv - VBAT_EMPTY_MV) * 100 / (VBAT_FULL_MV - VBAT_EMPTY_MV)) as u8
2828+ }
2929+}
+18-3
src/drivers/input.rs
···44// single "one button at a time" deal:
55// - Row 1 ADC (GPIO1): Right, Left, Confirm, Back via resistance ladder
66// - Row 2 ADC (GPIO2): Volume Up/Down via resistance ladder
77-// - Power button (GPIO3): Digital input, active low
77+// - Power button (GPIO3): Interrupt-driven, read via board::power_button_is_low()
88// NOTE: Because each resistance ladder can only report one press at a time,
99// we collapse everything into `Option<Button>` per poll cycle.
1010···153153154154 /// Read raw button state from hardware (before debouncing).
155155 fn read_raw(&mut self) -> Option<Button> {
156156- // Power button has priority (digital, active low)
157157- if self.hw.power.is_low() {
156156+ // Power button: interrupt-driven, read level from shared static.
157157+ // The GPIO interrupt already woke us via signal_button();
158158+ // here we just need the current pin state for debounce.
159159+ if crate::board::power_button_is_low() {
158160 return Some(Button::Power);
159161 }
160162···163165 let mv2: u16 = nb::block!(self.hw.adc.read_oneshot(&mut self.hw.row2)).unwrap();
164166165167 decode_ladder(mv1, ROW1_THRESHOLDS).or_else(|| decode_ladder(mv2, ROW2_THRESHOLDS))
168168+ }
169169+170170+ /// Read battery voltage in millivolts (ADC-calibrated, before divider correction).
171171+ /// The X4 has a voltage divider on GPIO0 — multiply by 2 for actual battery mV.
172172+ pub fn read_battery_mv(&mut self) -> u16 {
173173+ nb::block!(self.hw.adc.read_oneshot(&mut self.hw.battery)).unwrap()
174174+ }
175175+176176+ /// True when raw button activity was detected but debounce hasn't
177177+ /// confirmed it yet. Used by the idle timer to snap back to fast
178178+ /// polling so the confirmation arrives in ~10ms, not ~100ms.
179179+ pub fn is_debouncing(&self) -> bool {
180180+ self.candidate.is_some() && self.candidate != self.stable
166181 }
167182}
+1
src/drivers/mod.rs
···11+pub mod battery;
12pub mod input;
23pub mod storage;
34// pub mod display_driver;
+286-77
src/drivers/storage.rs
···11-//! High-level storage operations for reading files from SD card.
22-use embedded_sdmmc::{Error, Mode, SdCardError, VolumeIdx};
11+//! High-level file operations for the SD card.
22+//!
33+//! Uses embedded-sdmmc 0.9's RAII handles (Volume, Directory, File)
44+//! which close automatically on drop.
55+66+use embedded_sdmmc::{Mode, VolumeIdx};
3748use crate::board::sdcard::SdStorage;
5966-pub fn list_root_dir<SPI>(
77- sd: &mut SdStorage<SPI>,
88- mut f: impl FnMut(&str, u32, bool),
99-) -> Result<u32, Error<SdCardError>>
1010-where
1111- SPI: embedded_hal::spi::SpiDevice,
1212-{
1313- let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?;
1414- let root = volume.open_root_dir()?;
1010+/// A single directory entry, small enough to keep a page on the stack.
1111+#[derive(Clone, Copy)]
1212+pub struct DirEntry {
1313+ pub name: [u8; 13],
1414+ pub name_len: u8,
1515+ pub is_dir: bool,
1616+ pub size: u32,
1717+}
1818+1919+impl DirEntry {
2020+ pub const EMPTY: Self = Self {
2121+ name: [0u8; 13],
2222+ name_len: 0,
2323+ is_dir: false,
2424+ size: 0,
2525+ };
2626+2727+ pub fn name_str(&self) -> &str {
2828+ core::str::from_utf8(&self.name[..self.name_len as usize]).unwrap_or("?")
2929+ }
3030+}
3131+3232+/// Result of a paginated directory listing.
3333+pub struct DirPage {
3434+ pub total: usize,
3535+ pub count: usize,
3636+}
3737+3838+// ── Directory cache ────────────────────────────────────────────
3939+//
4040+// FAT directory iteration has no seek — every list_page() must scan
4141+// from the first entry and skip. For scroll position 40, that's
4242+// 40 entries read and discarded. With 100ms+ per SD transaction,
4343+// scrolling feels sluggish.
4444+//
4545+// The cache reads ALL entries once and serves pages from RAM.
4646+// Subsequent scrolls are pure memory copies — instant.
4747+//
4848+// Memory: 128 entries × 20 bytes = 2.5KB (of 400KB SRAM).
4949+5050+/// Maximum directory entries we'll cache.
5151+pub const MAX_DIR_ENTRIES: usize = 128;
15521616- let mut count = 0u32;
1717- root.iterate_dir(|entry| {
1818- let name = entry.name.base_name();
1919- let ext = entry.name.extension();
2020- let is_dir = entry.attributes.is_directory();
2121- let size = entry.size;
5353+/// In-memory cache of a directory's entries.
5454+///
5555+/// Created once in main.rs, lives for the lifetime of the program.
5656+/// `ensure_loaded()` fills it from SD on first access; `page()`
5757+/// serves slices without touching hardware.
5858+pub struct DirCache {
5959+ entries: [DirEntry; MAX_DIR_ENTRIES],
6060+ count: usize,
6161+ valid: bool,
6262+}
22632323- // Format "NAME.EXT" into a stack buffer (8.3 = max 12 chars)
2424- let mut buf = [0u8; 13];
2525- let mut pos = 0;
6464+impl DirCache {
6565+ pub const fn new() -> Self {
6666+ Self {
6767+ entries: [DirEntry::EMPTY; MAX_DIR_ENTRIES],
6868+ count: 0,
6969+ valid: false,
7070+ }
7171+ }
26722727- for &b in name {
2828- if b == b' ' {
2929- break;
7373+ /// Load all entries from the root directory if not already cached.
7474+ /// Returns Ok(()) if cache is warm (already valid), or after a
7575+ /// successful SD read. Returns Err only on SD failure.
7676+ pub fn ensure_loaded<SPI>(&mut self, sd: &SdStorage<SPI>) -> Result<(), &'static str>
7777+ where
7878+ SPI: embedded_hal::spi::SpiDevice,
7979+ {
8080+ if self.valid {
8181+ return Ok(());
8282+ }
8383+8484+ let volume = sd
8585+ .volume_mgr
8686+ .open_volume(VolumeIdx(0))
8787+ .map_err(|_| "open volume failed")?;
8888+ let root = volume.open_root_dir().map_err(|_| "open root dir failed")?;
8989+9090+ let mut count = 0usize;
9191+ root.iterate_dir(|entry| {
9292+ if entry.name.base_name()[0] == b'.' {
9393+ return;
3094 }
3131- if pos < buf.len() {
3232- buf[pos] = b;
3333- pos += 1;
9595+ if count < MAX_DIR_ENTRIES {
9696+ let mut name_buf = [0u8; 13];
9797+ let name_len = format_83_name(&entry.name, &mut name_buf);
9898+ self.entries[count] = DirEntry {
9999+ name: name_buf,
100100+ name_len: name_len as u8,
101101+ is_dir: entry.attributes.is_directory(),
102102+ size: entry.size,
103103+ };
104104+ count += 1;
34105 }
106106+ })
107107+ .map_err(|_| "iterate dir failed")?;
108108+109109+ self.count = count;
110110+ self.valid = true;
111111+ Ok(())
112112+ }
113113+114114+ /// Copy a page of entries into `buf`, starting at `skip`.
115115+ /// Pure memory operation — no SD access.
116116+ pub fn page(&self, skip: usize, buf: &mut [DirEntry]) -> DirPage {
117117+ let available = self.count.saturating_sub(skip);
118118+ let count = available.min(buf.len());
119119+ if count > 0 {
120120+ buf[..count].copy_from_slice(&self.entries[skip..skip + count]);
35121 }
122122+ DirPage {
123123+ total: self.count,
124124+ count,
125125+ }
126126+ }
361273737- if ext[0] != b' ' {
3838- if pos < buf.len() {
3939- buf[pos] = b'.';
4040- pos += 1;
4141- }
4242- for &b in ext {
4343- if b == b' ' {
4444- break;
4545- }
4646- if pos < buf.len() {
4747- buf[pos] = b;
4848- pos += 1;
4949- }
5050- }
128128+ /// Mark cache as stale. Next `ensure_loaded()` will re-read from SD.
129129+ pub fn invalidate(&mut self) {
130130+ self.valid = false;
131131+ }
132132+133133+ /// Total cached entries (0 if not loaded).
134134+ pub fn total(&self) -> usize {
135135+ self.count
136136+ }
137137+138138+ /// Whether the cache has been loaded.
139139+ pub fn is_valid(&self) -> bool {
140140+ self.valid
141141+ }
142142+}
143143+144144+/// List one page of entries from root directory.
145145+///
146146+/// Skips the first `skip` entries, then fills `buf` with up to `buf.len()` entries.
147147+/// Returns total entry count and how many were written to buf.
148148+pub fn list_page<SPI>(
149149+ sd: &SdStorage<SPI>,
150150+ skip: usize,
151151+ buf: &mut [DirEntry],
152152+) -> Result<DirPage, &'static str>
153153+where
154154+ SPI: embedded_hal::spi::SpiDevice,
155155+{
156156+ let volume = sd
157157+ .volume_mgr
158158+ .open_volume(VolumeIdx(0))
159159+ .map_err(|_| "open volume failed")?;
160160+ let root = volume.open_root_dir().map_err(|_| "open root dir failed")?;
161161+162162+ let mut total = 0usize;
163163+ let mut written = 0usize;
164164+ let page_size = buf.len();
165165+166166+ root.iterate_dir(|entry| {
167167+ // Skip dot entries
168168+ if entry.name.base_name()[0] == b'.' {
169169+ return;
51170 }
521715353- if let Ok(formatted) = core::str::from_utf8(&buf[..pos]) {
5454- f(formatted, size, is_dir);
172172+ if total >= skip && written < page_size {
173173+ let mut name_buf = [0u8; 13];
174174+ let name_len = format_83_name(&entry.name, &mut name_buf);
175175+ buf[written] = DirEntry {
176176+ name: name_buf,
177177+ name_len: name_len as u8,
178178+ is_dir: entry.attributes.is_directory(),
179179+ size: entry.size,
180180+ };
181181+ written += 1;
55182 }
183183+ total += 1;
184184+ })
185185+ .map_err(|_| "iterate dir failed")?;
561865757- count += 1;
5858- })?;
187187+ Ok(DirPage {
188188+ total,
189189+ count: written,
190190+ })
191191+}
192192+193193+/// List files in the root directory, calling `cb` for each entry.
194194+/// Returns the number of entries found.
195195+pub fn list_root_dir<SPI>(
196196+ sd: &SdStorage<SPI>,
197197+ mut cb: impl FnMut(&str, bool, u32),
198198+) -> Result<usize, &'static str>
199199+where
200200+ SPI: embedded_hal::spi::SpiDevice,
201201+{
202202+ let volume = sd
203203+ .volume_mgr
204204+ .open_volume(VolumeIdx(0))
205205+ .map_err(|_| "open volume failed")?;
206206+ let root = volume.open_root_dir().map_err(|_| "open root dir failed")?;
207207+208208+ let mut count = 0usize;
209209+ root.iterate_dir(|entry| {
210210+ let mut name_buf = [0u8; 13];
211211+ let name_len = format_83_name(&entry.name, &mut name_buf);
212212+ if let Ok(name) = core::str::from_utf8(&name_buf[..name_len]) {
213213+ cb(name, entry.attributes.is_directory(), entry.size);
214214+ count += 1;
215215+ }
216216+ })
217217+ .map_err(|_| "iterate dir failed")?;
5921860219 Ok(count)
61220}
622216363-pub fn file_size<SPI>(
6464- sd: &mut SdStorage<SPI>,
6565- name: &str,
6666-) -> Result<u32, Error<SdCardError>>
222222+/// Get the size of a file in the root directory.
223223+pub fn file_size<SPI>(sd: &SdStorage<SPI>, name: &str) -> Result<u32, &'static str>
67224where
68225 SPI: embedded_hal::spi::SpiDevice,
69226{
7070- let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?;
7171- let root = volume.open_root_dir()?;
7272- let file = root.open_file_in_dir(name, Mode::ReadOnly)?;
227227+ let volume = sd
228228+ .volume_mgr
229229+ .open_volume(VolumeIdx(0))
230230+ .map_err(|_| "open volume failed")?;
231231+ let root = volume.open_root_dir().map_err(|_| "open root dir failed")?;
232232+ let file = root
233233+ .open_file_in_dir(name, Mode::ReadOnly)
234234+ .map_err(|_| "open file failed")?;
235235+73236 Ok(file.length())
74237}
752387676-/// Read an entire file into a buffer. Returns bytes read.
239239+/// Read an entire file (or up to buf.len() bytes) into a buffer.
240240+/// Returns the number of bytes read.
77241pub fn read_file<SPI>(
7878- sd: &mut SdStorage<SPI>,
242242+ sd: &SdStorage<SPI>,
79243 name: &str,
80244 buf: &mut [u8],
8181-) -> Result<usize, Error<SdCardError>>
245245+) -> Result<usize, &'static str>
82246where
83247 SPI: embedded_hal::spi::SpiDevice,
84248{
8585- let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?;
8686- let root = volume.open_root_dir()?;
8787- let file = root.open_file_in_dir(name, Mode::ReadOnly)?;
249249+ let volume = sd
250250+ .volume_mgr
251251+ .open_volume(VolumeIdx(0))
252252+ .map_err(|_| "open volume failed")?;
253253+ let root = volume.open_root_dir().map_err(|_| "open root dir failed")?;
254254+ let mut file = root
255255+ .open_file_in_dir(name, Mode::ReadOnly)
256256+ .map_err(|_| "open file failed")?;
8825789258 let mut total = 0;
90259 while !file.is_eof() && total < buf.len() {
9191- let n = file.read(&mut buf[total..])?;
260260+ let n = file.read(&mut buf[total..]).map_err(|_| "read failed")?;
92261 if n == 0 {
93262 break;
94263 }
···98267 Ok(total)
99268}
100269101101-/// Read a chunk of a file starting at `offset`. Returns bytes read.
270270+/// Read a chunk of a file starting at `offset`.
271271+/// Returns the number of bytes read.
102272pub fn read_file_chunk<SPI>(
103103- sd: &mut SdStorage<SPI>,
273273+ sd: &SdStorage<SPI>,
104274 name: &str,
105275 offset: u32,
106276 buf: &mut [u8],
107107-) -> Result<usize, Error<SdCardError>>
277277+) -> Result<usize, &'static str>
108278where
109279 SPI: embedded_hal::spi::SpiDevice,
110280{
111111- let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?;
112112- let root = volume.open_root_dir()?;
113113- let file = root.open_file_in_dir(name, Mode::ReadOnly)?;
114114- file.seek_from_start(offset)?;
281281+ let volume = sd
282282+ .volume_mgr
283283+ .open_volume(VolumeIdx(0))
284284+ .map_err(|_| "open volume failed")?;
285285+ let root = volume.open_root_dir().map_err(|_| "open root dir failed")?;
286286+ let mut file = root
287287+ .open_file_in_dir(name, Mode::ReadOnly)
288288+ .map_err(|_| "open file failed")?;
289289+290290+ file.seek_from_start(offset).map_err(|_| "seek failed")?;
115291116292 let mut total = 0;
117293 while !file.is_eof() && total < buf.len() {
118118- let n = file.read(&mut buf[total..])?;
294294+ let n = file.read(&mut buf[total..]).map_err(|_| "read failed")?;
119295 if n == 0 {
120296 break;
121297 }
···126302}
127303128304/// Write data to a file (create or truncate).
129129-/// File name must be in 8.3 format.
130130-pub fn write_file<SPI>(
131131- sd: &mut SdStorage<SPI>,
132132- name: &str,
133133- data: &[u8],
134134-) -> Result<(), Error<SdCardError>>
305305+pub fn write_file<SPI>(sd: &SdStorage<SPI>, name: &str, data: &[u8]) -> Result<(), &'static str>
135306where
136307 SPI: embedded_hal::spi::SpiDevice,
137308{
138138- let volume = sd.volume_mgr.open_volume(VolumeIdx(0))?;
139139- let root = volume.open_root_dir()?;
140140- let file = root.open_file_in_dir(name, Mode::ReadWriteCreateOrTruncate)?;
141141- file.write(data)?;
142142- file.flush()?;
309309+ let volume = sd
310310+ .volume_mgr
311311+ .open_volume(VolumeIdx(0))
312312+ .map_err(|_| "open volume failed")?;
313313+ let root = volume.open_root_dir().map_err(|_| "open root dir failed")?;
314314+ let mut file = root
315315+ .open_file_in_dir(name, Mode::ReadWriteCreateOrTruncate)
316316+ .map_err(|_| "open file for write failed")?;
317317+318318+ file.write(data).map_err(|_| "write failed")?;
319319+ file.flush().map_err(|_| "flush failed")?;
320320+143321 Ok(())
144322}
323323+324324+/// Format a ShortFileName (8.3) into a human-readable "NAME.EXT" string.
325325+/// Returns the number of bytes written to `out`.
326326+fn format_83_name(sfn: &embedded_sdmmc::ShortFileName, out: &mut [u8; 13]) -> usize {
327327+ let base = sfn.base_name();
328328+ let ext = sfn.extension();
329329+330330+ let mut pos = 0;
331331+332332+ // Copy base name, trimming trailing spaces
333333+ for &b in base.iter() {
334334+ if b == b' ' {
335335+ break;
336336+ }
337337+ out[pos] = b;
338338+ pos += 1;
339339+ }
340340+341341+ // Add extension if non-empty
342342+ let ext_trimmed: &[u8] = &ext[..ext.iter().position(|&b| b == b' ').unwrap_or(ext.len())];
343343+ if !ext_trimmed.is_empty() {
344344+ out[pos] = b'.';
345345+ pos += 1;
346346+ for &b in ext_trimmed {
347347+ out[pos] = b;
348348+ pos += 1;
349349+ }
350350+ }
351351+352352+ pos
353353+}
+1-9
src/kernel/mod.rs
···11//! Minimal kernel for pulp-os
22-//!
33-//! Provides:
44-//! - Job scheduler with priority queues
55-//! - Adaptive polling for power efficiency
66-//! - Sleep/wake primitives
7288-pub mod poll;
93pub mod scheduler;
104pub mod wake;
1151212-pub use poll::{AdaptivePoller, BASE_TICK_MS, PollRate};
1313-pub use scheduler::{Job, Priority, PushError, Scheduler};
1414-pub use wake::{WakeReason, signal_button, signal_display, signal_timer, sleep_until_wake};
66+pub use scheduler::{Job, Scheduler};