From 375b5d1d47cfcc2eb123b839dc9bfd610fc38ded Mon Sep 17 00:00:00 2001 From: sawyer bristol Date: Tue, 4 Nov 2025 13:43:48 -0700 Subject: [PATCH] init --- Cargo.lock | 13 +++- Cargo.toml | 3 +- abi/Cargo.toml | 3 +- abi/src/lib.rs | 85 +++++++++++++++++++++++- abi_sys/Cargo.toml | 1 - abi_sys/src/lib.rs | 13 ++-- kernel/src/abi.rs | 54 ++++++++++----- selection_ui/Cargo.toml | 10 +++ selection_ui/src/lib.rs | 120 ++++++++++++++++++++++++++++++++++ user-apps/gallery/src/main.rs | 63 ++++++++---------- user-apps/gif/Cargo.toml | 1 + user-apps/gif/src/main.rs | 21 ++++-- 12 files changed, 318 insertions(+), 69 deletions(-) create mode 100644 selection_ui/Cargo.toml create mode 100644 selection_ui/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 88f54ea..d12ac45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,6 @@ version = "0.1.0" dependencies = [ "abi_sys", "embedded-graphics", - "embedded-sdmmc", "once_cell", "rand_core 0.9.3", ] @@ -31,7 +30,6 @@ dependencies = [ "cbindgen", "defmt 0.3.100", "embedded-graphics", - "embedded-sdmmc", "strum", ] @@ -1289,6 +1287,7 @@ version = "0.1.0" dependencies = [ "abi", "embedded-graphics", + "selection_ui", "tinygif", ] @@ -2191,6 +2190,16 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +[[package]] +name = "selection_ui" +version = "0.1.0" +dependencies = [ + "abi", + "embedded-graphics", + "embedded-layout", + "embedded-text", +] + [[package]] name = "semver" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 9e88380..05749bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "kernel", "abi_sys", "abi", + "selection_ui", "user-apps/calculator", "user-apps/snake", "user-apps/gallery", @@ -26,4 +27,4 @@ panic = "abort" [profile.dev] lto = true -opt-level = "z" +opt-level = "s" diff --git a/abi/Cargo.toml b/abi/Cargo.toml index a197d01..591ddaa 100644 --- a/abi/Cargo.toml +++ b/abi/Cargo.toml @@ -4,8 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -embedded-sdmmc = { version = "0.9.0", default-features = false } -embedded-graphics = "0.8.1" abi_sys = { path = "../abi_sys" } +embedded-graphics = "0.8.1" once_cell = { version = "1", default-features = false } rand_core = "0.9.3" diff --git a/abi/src/lib.rs b/abi/src/lib.rs index c18e83b..366059c 100644 --- a/abi/src/lib.rs +++ b/abi/src/lib.rs @@ -140,7 +140,9 @@ impl RngCore for Rng { } pub mod fs { - use embedded_sdmmc::DirEntry; + use core::fmt::Display; + + use alloc::{format, vec::Vec}; pub fn read_file(file: &str, read_from: usize, buf: &mut [u8]) -> usize { abi_sys::read_file( @@ -152,8 +154,85 @@ pub mod fs { ) } - pub fn list_dir(path: &str, files: &mut [Option]) -> usize { - abi_sys::list_dir(path.as_ptr(), path.len(), files.as_mut_ptr(), files.len()) + pub struct FileName<'a> { + full: &'a str, + base: &'a str, + ext: Option<&'a str>, + } + + impl<'a> FileName<'a> { + pub fn full_name(&self) -> &str { + self.full + } + + pub fn base(&self) -> &str { + self.base + } + + pub fn extension(&self) -> Option<&str> { + self.ext + } + } + + impl<'a> Display for FileName<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.full_name()) + } + } + + impl<'a> From<&'a str> for FileName<'a> { + fn from(s: &'a str) -> FileName<'a> { + let full = s; + + // Split on last dot for extension + let (base, ext) = match s.rfind('.') { + Some(idx) => (&s[..idx], Some(&s[idx + 1..])), + None => (s, None), + }; + + FileName { full, base, ext } + } + } + + const MAX_ENTRY_NAME_LEN: usize = 25; + const MAX_ENTRIES: usize = 25; + + #[derive(Clone, Copy)] + pub struct Entries([[u8; MAX_ENTRY_NAME_LEN]; MAX_ENTRIES]); + + impl Entries { + pub fn new() -> Self { + Self([[0; MAX_ENTRY_NAME_LEN]; MAX_ENTRIES]) + } + + /// Get list of file names after listing + pub fn entries<'a>(&'a self) -> Vec> { + self.0 + .iter() + .filter_map(|buf| { + let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + Some(core::str::from_utf8(&buf[..nul_pos]).ok()?.into()) + }) + .collect() + } + + fn as_ptrs(&mut self) -> [*mut u8; MAX_ENTRIES] { + let mut ptrs: [*mut u8; MAX_ENTRIES] = [core::ptr::null_mut(); MAX_ENTRIES]; + for (i, buf) in self.0.iter_mut().enumerate() { + ptrs[i] = buf.as_mut_ptr(); + } + ptrs + } + } + + pub fn list_dir(path: &str, entries: &mut Entries) -> usize { + abi_sys::list_dir( + path.as_ptr(), + path.len(), + entries.as_ptrs().as_mut_ptr(), + MAX_ENTRIES, + MAX_ENTRY_NAME_LEN, + ) } pub fn file_len(str: &str) -> usize { diff --git a/abi_sys/Cargo.toml b/abi_sys/Cargo.toml index e30198a..5a83d0b 100644 --- a/abi_sys/Cargo.toml +++ b/abi_sys/Cargo.toml @@ -12,7 +12,6 @@ defmt = ["dep:defmt"] strum = { version = "0.27.2", default-features = false, features = ["derive"] } bitflags = "2.9.4" embedded-graphics = "0.8.1" -embedded-sdmmc = { version = "0.9.0", default-features = false } defmt = { version = "0.3", optional = true } [build-dependencies] diff --git a/abi_sys/src/lib.rs b/abi_sys/src/lib.rs index ed9e9f6..3243771 100644 --- a/abi_sys/src/lib.rs +++ b/abi_sys/src/lib.rs @@ -1,14 +1,13 @@ #![no_std] - #[cfg(feature = "alloc")] use core::alloc::Layout; +use core::ffi::c_char; use embedded_graphics::{ Pixel, pixelcolor::{Rgb565, raw::RawU16}, prelude::{IntoStorage, Point}, }; -use embedded_sdmmc::DirEntry; use strum::{EnumCount, EnumIter}; pub type EntryFn = fn(); @@ -396,21 +395,23 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) { pub type ListDir = extern "C" fn( str: *const u8, len: usize, - files: *mut Option, + entries: *mut *mut c_char, file_len: usize, + max_entry_str_len: usize, ) -> usize; #[unsafe(no_mangle)] pub extern "C" fn list_dir( str: *const u8, len: usize, - files: *mut Option, - file_len: usize, + entries: *mut *mut c_char, + entry_count: usize, + max_entry_str_len: usize, ) -> usize { unsafe { let ptr = CALL_ABI_TABLE[CallTable::ListDir as usize]; let f: ListDir = core::mem::transmute(ptr); - f(str, len, files, file_len) + f(str, len, entries, entry_count, max_entry_str_len) } } diff --git a/kernel/src/abi.rs b/kernel/src/abi.rs index 9f64223..4515ac0 100644 --- a/kernel/src/abi.rs +++ b/kernel/src/abi.rs @@ -3,11 +3,11 @@ use abi_sys::{ PrintAbi, ReadFile, RngRequest, SleepMsAbi, keyboard::*, }; use alloc::{string::ToString, vec::Vec}; -use core::{alloc::GlobalAlloc, sync::atomic::Ordering}; +use core::{alloc::GlobalAlloc, ffi::c_char, ptr, sync::atomic::Ordering}; use embassy_rp::clocks::{RoscRng, clk_sys_freq}; use embassy_time::Instant; use embedded_graphics::draw_target::DrawTarget; -use embedded_sdmmc::{DirEntry, LfnBuffer}; +use embedded_sdmmc::LfnBuffer; use heapless::spsc::Queue; use crate::{ @@ -116,11 +116,27 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) { } } -fn get_dir_entries(dir: &Dir, files: &mut [Option]) -> usize { +unsafe fn copy_entry_to_user_buf(name: &[u8], dest: *mut c_char, max_str_len: usize) { + if !dest.is_null() { + let len = name.len().min(max_str_len - 1); + unsafe { + ptr::copy_nonoverlapping(name.as_ptr(), dest as *mut u8, len); + *dest.add(len) = 0; // nul terminator + } + } +} + +unsafe fn get_dir_entries(dir: &Dir, entries: &mut [*mut c_char], max_str_len: usize) -> usize { + let mut b = [0; 25]; + let mut buf = LfnBuffer::new(&mut b); let mut i = 0; - dir.iterate_dir(|entry| { - if i < files.len() { - files[i] = Some(entry.clone()); + dir.iterate_dir_lfn(&mut buf, |entry, lfn_name| { + if i < entries.len() { + if let Some(name) = lfn_name { + unsafe { copy_entry_to_user_buf(name.as_bytes(), entries[i], max_str_len) }; + } else { + unsafe { copy_entry_to_user_buf(entry.name.base_name(), entries[i], max_str_len) }; + } i += 1; } }) @@ -128,24 +144,30 @@ fn get_dir_entries(dir: &Dir, files: &mut [Option]) -> usize { i } -fn recurse_dir(dir: &Dir, dirs: &[&str], files: &mut [Option]) -> usize { +unsafe fn recurse_dir( + dir: &Dir, + dirs: &[&str], + entries: &mut [*mut c_char], + max_str_len: usize, +) -> usize { if dirs.is_empty() { - return get_dir_entries(dir, files); + return unsafe { get_dir_entries(dir, entries, max_str_len) }; } let dir = dir.open_dir(dirs[0]).unwrap(); - recurse_dir(&dir, &dirs[1..], files) + unsafe { recurse_dir(&dir, &dirs[1..], entries, max_str_len) } } const _: ListDir = list_dir; pub extern "C" fn list_dir( dir: *const u8, len: usize, - files: *mut Option, + entries: *mut *mut c_char, files_len: usize, + max_entry_str_len: usize, ) -> usize { // SAFETY: caller guarantees `ptr` is valid for `len` bytes - let files = unsafe { core::slice::from_raw_parts_mut(files, files_len) }; + let files = unsafe { core::slice::from_raw_parts_mut(entries, files_len) }; // SAFETY: caller guarantees `ptr` is valid for `len` bytes let dir = unsafe { core::str::from_raw_parts(dir, len) }; let dirs: Vec<&str> = dir.split('/').collect(); @@ -156,10 +178,12 @@ pub extern "C" fn list_dir( let mut wrote = 0; sd.access_root_dir(|root| { if dirs[0] == "" && dirs.len() >= 2 { - if dir == "/" { - wrote = get_dir_entries(&root, files); - } else { - wrote = recurse_dir(&root, &dirs[1..], files); + unsafe { + if dir == "/" { + wrote = get_dir_entries(&root, files, max_entry_str_len); + } else { + wrote = recurse_dir(&root, &dirs[1..], files, max_entry_str_len); + } } } }); diff --git a/selection_ui/Cargo.toml b/selection_ui/Cargo.toml new file mode 100644 index 0000000..263d6e3 --- /dev/null +++ b/selection_ui/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "selection_ui" +version = "0.1.0" +edition = "2024" + +[dependencies] +abi = { path = "../abi" } +embedded-graphics = "0.8.1" +embedded-layout = "0.4.2" +embedded-text = "0.7.3" diff --git a/selection_ui/src/lib.rs b/selection_ui/src/lib.rs new file mode 100644 index 0000000..0e7a09b --- /dev/null +++ b/selection_ui/src/lib.rs @@ -0,0 +1,120 @@ +#![no_std] + +extern crate alloc; + +use abi::{ + display::Display, + fs::{Entries, FileName}, + get_key, + keyboard::{KeyCode, KeyState}, +}; +use alloc::vec::Vec; +use embedded_graphics::{ + Drawable, + mono_font::{MonoTextStyle, ascii::FONT_10X20}, + pixelcolor::Rgb565, + prelude::{Dimensions, Point, Primitive, RgbColor, Size}, + primitives::{PrimitiveStyle, Rectangle}, + text::Text, +}; +use embedded_layout::{ + align::{horizontal, vertical}, + layout::linear::{FixedMargin, LinearLayout}, + prelude::*, +}; +use embedded_text::TextBox; + +pub struct SelectionUi<'a> { + selection: usize, + items: &'a Entries, + error: &'a str, + last_bounds: Option, +} + +impl<'a> SelectionUi<'a> { + pub fn new(items: &'a Entries, error: &'a str) -> Self { + Self { + selection: 0, + items, + error, + last_bounds: None, + } + } + + pub fn run_selection_ui(&mut self, display: &mut Display) -> Result, ()> { + self.draw(display)?; + let selection; + loop { + let key = get_key(); + if key.state == KeyState::Pressed { + if let Some(s) = self.update(display, key.key)? { + selection = Some(s); + break; + } + } + } + Ok(selection) + } + + /// updates the display with a new keypress. + /// returns selection idx if selected + pub fn update(&mut self, display: &mut Display, key: KeyCode) -> Result, ()> { + match key { + KeyCode::JoyUp => { + let _ = self.selection.saturating_sub(1); + } + KeyCode::JoyDown => { + let _ = self.selection.saturating_add(1); + } + KeyCode::Enter | KeyCode::JoyRight => return Ok(Some(self.selection)), + _ => (), + }; + self.draw(display)?; + Ok(None) + } + + fn draw(&mut self, display: &mut Display) -> Result<(), ()> { + let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); + let display_area = display.bounding_box(); + + let entries = self.items.entries(); + + if entries.is_empty() { + TextBox::new( + &self.error, + Rectangle::new( + Point::new(25, 25), + Size::new(display_area.size.width - 50, display_area.size.width - 50), + ), + text_style, + ) + .draw(display) + .unwrap(); + } + + let mut views: Vec>> = Vec::new(); + + for i in &entries { + views.push(Text::new(i.full_name(), Point::zero(), text_style)); + } + + let views_group = Views::new(views.as_mut_slice()); + + let layout = LinearLayout::vertical(views_group) + .with_alignment(horizontal::Center) + .with_spacing(FixedMargin(5)) + .arrange() + .align_to(&display_area, horizontal::Center, vertical::Center); + + // draw selected box + let selected_bounds = layout.inner().get(self.selection).ok_or(())?.bounding_box(); + Rectangle::new(selected_bounds.top_left, selected_bounds.size) + .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1)) + .draw(display)?; + + self.last_bounds = Some(layout.bounds()); + + layout.draw(display)?; + Ok(()) + } +} diff --git a/user-apps/gallery/src/main.rs b/user-apps/gallery/src/main.rs index 631ac37..eb8ca48 100644 --- a/user-apps/gallery/src/main.rs +++ b/user-apps/gallery/src/main.rs @@ -5,12 +5,12 @@ extern crate alloc; use abi::{ display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH}, - fs::{list_dir, read_file}, + fs::{Entries, list_dir, read_file}, get_key, keyboard::{KeyCode, KeyState}, print, }; -use alloc::{format, string::ToString, vec}; +use alloc::{format, vec}; use core::panic::PanicInfo; use embedded_graphics::{ Drawable, image::Image, mono_font::MonoTextStyle, mono_font::ascii::FONT_6X10, @@ -34,7 +34,6 @@ pub fn main() { let mut bmp_buf = vec![0_u8; 100_000]; let mut display = Display; - // Grid parameters let grid_cols = 3; let grid_rows = 3; let cell_width = SCREEN_WIDTH as i32 / grid_cols; @@ -42,50 +41,44 @@ pub fn main() { let mut images_drawn = 0; - let mut files = [const { None }; 18]; - let files_num = list_dir("/images", &mut files); + let mut entries = Entries::new(); + let files_num = list_dir("/images", &mut entries); - for file in &files[2..files_num] { + for file in &entries.entries()[2..files_num] { if images_drawn >= grid_cols * grid_rows { break; // only draw 3x3 } - if let Some(f) = file { - print!("file: {}", f.name); - if f.name.extension() == b"bmp" || f.name.extension() == b"BMP" { - let file = format!("/images/{}", f.name); + print!("file: {}", file); + if file.extension().unwrap_or("") == "bmp" || file.extension().unwrap_or("") == "BMP" { + let file_path = format!("/images/{}", file); - let read = read_file(&file, 0, &mut &mut bmp_buf[..]); - if read > 0 { - let bmp = Bmp::from_slice(&bmp_buf).expect("failed to parse bmp"); + let read = read_file(&file_path, 0, &mut &mut bmp_buf[..]); + if read > 0 { + let bmp = Bmp::from_slice(&bmp_buf).expect("failed to parse bmp"); - let row = images_drawn / grid_cols; - let col = images_drawn % grid_cols; - let cell_x = col * cell_width; - let cell_y = row * cell_height; + let row = images_drawn / grid_cols; + let col = images_drawn % grid_cols; + let cell_x = col * cell_width; + let cell_y = row * cell_height; - // Center image inside cell - let bmp_w = bmp.size().width as i32; - let bmp_h = bmp.size().height as i32; - let x = cell_x + (cell_width - bmp_w) / 2; - let y = cell_y + 5; // 5px top margin + // Center image inside cell + let bmp_w = bmp.size().width as i32; + let bmp_h = bmp.size().height as i32; + let x = cell_x + (cell_width - bmp_w) / 2; + let y = cell_y + 5; // 5px top margin - Image::new(&bmp, Point::new(x, y)) - .draw(&mut display) - .unwrap(); - - let text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); - let text_y = y + bmp_h + 2; // 2px gap under image - Text::new( - f.name.to_string().as_str(), - Point::new(cell_x + 2, text_y), - text_style, - ) + Image::new(&bmp, Point::new(x, y)) .draw(&mut display) .unwrap(); - images_drawn += 1; - } + let text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); + let text_y = y + bmp_h + 2; // 2px gap under image + Text::new(&file.base(), Point::new(cell_x + 2, text_y), text_style) + .draw(&mut display) + .unwrap(); + + images_drawn += 1; } } } diff --git a/user-apps/gif/Cargo.toml b/user-apps/gif/Cargo.toml index 6f76ceb..1c5ecd8 100644 --- a/user-apps/gif/Cargo.toml +++ b/user-apps/gif/Cargo.toml @@ -6,4 +6,5 @@ edition = "2024" [dependencies] abi = { path = "../../abi" } embedded-graphics = "0.8.1" +selection_ui = { path = "../../selection_ui" } tinygif = { git = "https://github.com/LegitCamper/tinygif" } diff --git a/user-apps/gif/src/main.rs b/user-apps/gif/src/main.rs index 1d21b4d..7dbdfbf 100644 --- a/user-apps/gif/src/main.rs +++ b/user-apps/gif/src/main.rs @@ -4,16 +4,17 @@ extern crate alloc; use abi::{ display::Display, - fs::{file_len, read_file}, + fs::{Entries, file_len, list_dir, read_file}, get_key, get_ms, keyboard::{KeyCode, KeyState}, print, sleep, }; -use alloc::vec; +use alloc::{format, vec, vec::Vec}; use core::panic::PanicInfo; use embedded_graphics::{ image::ImageDrawable, pixelcolor::Rgb565, prelude::Point, transform::Transform, }; +use selection_ui::SelectionUi; use tinygif::Gif; #[panic_handler] @@ -31,13 +32,25 @@ pub fn main() { print!("Starting Gif app"); let mut display = Display; - let size = file_len("/gifs/bad_apple.gif"); + let mut gifs = Entries::new(); + list_dir("/gifs", &mut gifs); + + gifs.entries() + .retain(|e| e.extension().unwrap_or("") == "gif"); + + let mut selection_ui = SelectionUi::new(&gifs, "No Gif files found in /gifs"); + let selection = selection_ui + .run_selection_ui(&mut display) + .expect("failed to draw") + .expect("Failed to get user selection"); + + let size = file_len(&format!("/gifs/{}.gif", gifs.entries()[selection])); let mut buf = vec![0_u8; size]; let read = read_file("/gifs/bad_apple.gif", 0, &mut buf); print!("read: {}, file size: {}", read, size); assert!(read == size); - let gif = Gif::::from_slice(&buf).unwrap(); + let gif = Gif::::from_slice(&buf).expect("Failed to parse gif"); let height = gif.height(); let mut frame_num = 0;