From 3cbe2e22fe15fb89c1986352f50a88d19e5baf7e Mon Sep 17 00:00:00 2001 From: arthomnix Date: Mon, 10 Feb 2025 21:19:25 +0000 Subject: [PATCH] eepy-launcher: app deletion and multiple apps support --- Cargo.toml | 3 +- eepy-gui/Cargo.toml | 3 +- eepy-gui/src/element/button.rs | 27 +++--- eepy-launcher/src/main.rs | 155 +++++++++++++++++++++++++-------- eepy-launcher/src/serial.rs | 39 +++++++-- eepy-sys/Cargo.toml | 3 +- eepy-sys/src/header.rs | 10 ++- eepy-sys/src/misc.rs | 9 ++ 8 files changed, 192 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9cb8136..361bf62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,4 +42,5 @@ tar = "0.4" zstd = "0.13" strum = { version = "0.27", default-features = false, features = ["derive"] } tickv = "2.0" -siphasher = "1.0" \ No newline at end of file +siphasher = "1.0" +fugit = "0.3" \ No newline at end of file diff --git a/eepy-gui/Cargo.toml b/eepy-gui/Cargo.toml index 0061771..b5e3095 100644 --- a/eepy-gui/Cargo.toml +++ b/eepy-gui/Cargo.toml @@ -4,10 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] -eepy-sys = { path = "../eepy-sys", features = ["embedded-graphics"] } +eepy-sys = { path = "../eepy-sys", features = ["embedded-graphics", "fugit"] } tp370pgh01 = { path = "../tp370pgh01" } embedded-graphics.workspace = true defmt = { workspace = true, optional = true } +fugit.workspace = true [features] defmt-derive = ["defmt", "tp370pgh01/defmt", "embedded-graphics/defmt"] \ No newline at end of file diff --git a/eepy-gui/src/element/button.rs b/eepy-gui/src/element/button.rs index ac4c5d4..f625bfa 100644 --- a/eepy-gui/src/element/button.rs +++ b/eepy-gui/src/element/button.rs @@ -4,7 +4,9 @@ use embedded_graphics::pixelcolor::BinaryColor; use embedded_graphics::primitives::{CornerRadii, PrimitiveStyle, Rectangle, RoundedRectangle}; use embedded_graphics::text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder}; use embedded_graphics::text::renderer::TextRenderer; +use fugit::ExtU32; use eepy_sys::input_common::{Event, TouchEventType}; +use eepy_sys::misc::{now, Instant}; use crate::draw_target::EpdDrawTarget; use crate::element::{Gui, DEFAULT_PRIMITIVE_STYLE, DEFAULT_TEXT_STYLE}; @@ -17,6 +19,7 @@ const CENTRE_STYLE: TextStyle = TextStyleBuilder::new() #[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] pub struct ButtonOutput { pub clicked: bool, + pub long_clicked: bool, pub needs_refresh: bool, } @@ -30,7 +33,7 @@ pub struct Button<'a> { pub touch_feedback: bool, pub touch_feedback_immediate_release: bool, - began_click: bool, + click_begin_time: Option, inverted: bool, should_uninvert: bool, } @@ -44,7 +47,7 @@ impl<'a> Button<'a> { char_style, touch_feedback, touch_feedback_immediate_release, - began_click: false, + click_begin_time: None, inverted: false, should_uninvert: false, } @@ -59,7 +62,7 @@ impl<'a> Button<'a> { char_style, touch_feedback, touch_feedback_immediate_release, - began_click: false, + click_begin_time: None, inverted: false, should_uninvert: false, } @@ -73,7 +76,7 @@ impl<'a> Button<'a> { char_style: DEFAULT_TEXT_STYLE, touch_feedback: true, touch_feedback_immediate_release, - began_click: false, + click_begin_time: None, inverted: false, should_uninvert: false, } @@ -122,17 +125,17 @@ impl<'a> Gui for Button<'a> { if let Event::Touch(ev) = ev { if self.rect.contains(ev.eg_point()) { - match (self.began_click, ev.ev_type) { - (false, TouchEventType::Down) => { - self.began_click = true; + match (self.click_begin_time, ev.ev_type) { + (None, TouchEventType::Down) => { + self.click_begin_time = Some(now()); if self.touch_feedback { self.invert(); self.draw_init(target); ret.needs_refresh = true; } }, - (true, TouchEventType::Up) => { - self.began_click = false; + (Some(t), TouchEventType::Up) => { + self.click_begin_time = None; if self.inverted { if self.touch_feedback_immediate_release { self.invert(); @@ -142,12 +145,16 @@ impl<'a> Gui for Button<'a> { self.should_uninvert = true; } } + ret.clicked = true; + if now().checked_duration_since(t).unwrap() > 500.millis::<1, 1_000_000>() { + ret.long_clicked = true; + } }, _ => {}, } } else { - self.began_click = false; + self.click_begin_time = None; if self.inverted { if self.touch_feedback_immediate_release { self.invert(); diff --git a/eepy-launcher/src/main.rs b/eepy-launcher/src/main.rs index 01bd49a..b1fb304 100644 --- a/eepy-launcher/src/main.rs +++ b/eepy-launcher/src/main.rs @@ -6,22 +6,24 @@ mod serial; extern crate panic_halt; use core::arch::asm; +use core::mem::offset_of; use core::sync::atomic::Ordering; use embedded_graphics::geometry::AnchorPoint; use embedded_graphics::pixelcolor::BinaryColor; use embedded_graphics::prelude::*; use embedded_graphics::primitives::{Circle, Line, PrimitiveStyle, Rectangle}; +use embedded_graphics::text::Text; use usb_device::bus::UsbBusAllocator; use eepy_gui::draw_target::EpdDrawTarget; use eepy_gui::element::button::Button; -use eepy_gui::element::Gui; +use eepy_gui::element::{Gui, DEFAULT_TEXT_STYLE}; use eepy_gui::element::slider::Slider; use eepy_sys::exec::exec; use eepy_sys::input::{has_event, next_event, set_touch_enabled}; use eepy_sys::input_common::{Event, TouchEventType}; -use eepy_sys::header::{ProgramSlotHeader, Programs}; +use eepy_sys::header::{slot, slot_ptr, ProgramSlotHeader, Programs}; use eepy_sys::misc::get_serial; -use eepy_sys::usb; +use eepy_sys::{flash, usb}; use eepy_sys::usb::UsbBus; use usb_device::prelude::*; use usbd_serial::SerialPort; @@ -35,12 +37,6 @@ static HEADER: ProgramSlotHeader = ProgramSlotHeader::partial( main, ); -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum Page { - MainPage, - ScratchpadPage, -} - struct MainPage { scratchpad_button: Button<'static>, app_buttons: [Option<(Button<'static>, u8)>; 32], @@ -79,7 +75,7 @@ impl MainPage { } impl Gui for MainPage { - type Output = Option; + type Output = Option; fn draw_init(&self, draw_target: &mut EpdDrawTarget) { self.scratchpad_button.draw_init(draw_target); @@ -95,7 +91,7 @@ impl Gui for MainPage { let s = self.scratchpad_button.tick(draw_target, ev); if s.clicked { - return Some(Page::ScratchpadPage); + return Some(PageType::ScratchpadPage); } else if s.needs_refresh { draw_target.refresh(true); } @@ -104,7 +100,9 @@ impl Gui for MainPage { if let Some((button, s)) = b { let response = button.tick(draw_target, ev); - if response.clicked { + if response.long_clicked { + return Some(PageType::AppInfoPage(*s)); + } else if response.clicked { unsafe { cleanup_usb() }; exec(*s); } @@ -160,7 +158,7 @@ impl ScratchpadPage { } impl Gui for ScratchpadPage { - type Output = Option; + type Output = Option; fn draw_init(&self, draw_target: &mut EpdDrawTarget) { self.exit_button.draw_init(draw_target); @@ -176,7 +174,7 @@ impl Gui for ScratchpadPage { let e = self.exit_button.tick(draw_target, ev); if e.clicked { - return Some(Page::MainPage); + return Some(PageType::MainPage); } refresh |= e.needs_refresh; @@ -242,32 +240,114 @@ impl Gui for ScratchpadPage { } } -struct MainGui { - current_page: Page, - main_page: MainPage, - scratchpad_page: ScratchpadPage, +struct AppInfoPage { + slot: u8, + back_button: Button<'static>, + delete_button: Button<'static>, } -impl MainGui { - fn new() -> Self { +impl AppInfoPage { + fn new(slot: u8) -> Self { Self { - current_page: Page::MainPage, - main_page: MainPage::new(), - scratchpad_page: ScratchpadPage::new(), + slot, + back_button: Button::with_default_style_auto_sized(Point::new(10, 386), "Back", true), + delete_button: Button::with_default_style_auto_sized(Point::new(10, 60), "Delete program", true), + } + } +} + +impl Gui for AppInfoPage { + type Output = Option; + + fn draw_init(&self, target: &mut EpdDrawTarget) { + let header = unsafe { slot(self.slot) }; + + Text::new("Program: ", Point::new(10, 20), DEFAULT_TEXT_STYLE) + .draw(target) + .unwrap(); + + if let Ok(name) = unsafe { (*header).name() } { + Text::new(name, Point::new(120, 20), DEFAULT_TEXT_STYLE) + .draw(target) + .unwrap(); + } else { + Text::new("", Point::new(120, 20), DEFAULT_TEXT_STYLE) + .draw(target) + .unwrap(); + } + + + Text::new("Version: ", Point::new(10, 40), DEFAULT_TEXT_STYLE) + .draw(target) + .unwrap(); + + if let Ok(vers) = unsafe { (*header).version() } { + Text::new(vers, Point::new(120, 40), DEFAULT_TEXT_STYLE) + .draw(target) + .unwrap(); + } else { + Text::new("", Point::new(120, 40), DEFAULT_TEXT_STYLE) + .draw(target) + .unwrap(); + } + + self.back_button.draw_init(target); + self.delete_button.draw_init(target); + } + + fn tick(&mut self, target: &mut EpdDrawTarget, ev: Event) -> Self::Output { + let b = self.back_button.tick(target, ev); + if b.clicked { + return Some(PageType::MainPage); + } + + let d = self.delete_button.tick(target, ev); + if d.clicked { + let ptr = unsafe { slot_ptr(self.slot) }; + let mut buf = [0u8; 256]; + unsafe { buf.copy_from_slice(core::slice::from_raw_parts(ptr, 256)) }; + let offset = offset_of!(ProgramSlotHeader, len); + buf[offset..offset + 4].copy_from_slice(&[0; 4]); + + unsafe { flash::program(self.slot as u32 * 512 * 1024, &buf) }; + return Some(PageType::MainPage); } + + None + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum PageType { + MainPage, + ScratchpadPage, + AppInfoPage(u8), +} + +enum MainGui { + MainPage(MainPage), + ScratchpadPage(ScratchpadPage), + AppInfoPage(AppInfoPage), +} + +impl MainGui { + fn new() -> Self { + Self::MainPage(MainPage::new()) } - fn get_current_page(&self) -> &dyn Gui> { - match self.current_page { - Page::MainPage => &self.main_page, - Page::ScratchpadPage => &self.scratchpad_page, + fn get_current_page(&self) -> &dyn Gui> { + match self { + MainGui::MainPage(page) => page, + MainGui::ScratchpadPage(page) => page, + MainGui::AppInfoPage(page) => page, } } - fn get_current_page_mut(&mut self) -> &mut dyn Gui> { - match self.current_page { - Page::MainPage => &mut self.main_page, - Page::ScratchpadPage => &mut self.scratchpad_page, + fn get_current_page_mut(&mut self) -> &mut dyn Gui> { + match self { + MainGui::MainPage(page) => page, + MainGui::ScratchpadPage(page) => page, + MainGui::AppInfoPage(page) => page, } } } @@ -281,7 +361,12 @@ impl Gui for MainGui { fn tick(&mut self, draw_target: &mut EpdDrawTarget, ev: Event) -> Self::Output { if let Some(page) = self.get_current_page_mut().tick(draw_target, ev) { - self.current_page = page; + *self = match page { + PageType::MainPage => MainGui::MainPage(MainPage::new()), + PageType::ScratchpadPage => MainGui::ScratchpadPage(ScratchpadPage::new()), + PageType::AppInfoPage(slot) => MainGui::AppInfoPage(AppInfoPage::new(slot)), + }; + draw_target.clear(BinaryColor::Off).unwrap(); self.draw_init(draw_target); draw_target.refresh(false); @@ -339,9 +424,9 @@ extern "C" fn main() { } if NEEDS_REFRESH_PROGRAMS.swap(false, Ordering::Relaxed) { - gui.main_page.refresh_buttons(); - if gui.current_page == Page::MainPage { - gui.draw_init(&mut draw_target); + if let MainGui::MainPage(page) = &mut gui { + page.refresh_buttons(); + page.draw_init(&mut draw_target); draw_target.refresh(false); } } else if NEEDS_REFRESH.swap(false, Ordering::Relaxed) { diff --git a/eepy-launcher/src/serial.rs b/eepy-launcher/src/serial.rs index 0326417..9ae59f7 100644 --- a/eepy-launcher/src/serial.rs +++ b/eepy-launcher/src/serial.rs @@ -5,6 +5,7 @@ use usb_device::device::UsbDevice; use usbd_serial::SerialPort; use eepy_serial::{Response, SerialCommand}; use eepy_sys::flash::erase_and_program; +use eepy_sys::header::{slot, slot_ptr}; use eepy_sys::image::refresh; use eepy_sys::IMAGE_BYTES; use eepy_sys::input::{next_event, set_touch_enabled}; @@ -29,6 +30,23 @@ enum SerialState { }, } +fn erase_cycles(slot: u8) -> u32 { + let c = unsafe { u32::from_ne_bytes(*slot_ptr(slot).cast()) }; + if c == u32::MAX { + 0 + } else { + c + } +} + +fn best_slot() -> Option { + (1u8..=31) + .filter(|s| unsafe { !(*slot(*s)).is_valid() }) + .map(|s| (s, erase_cycles(s))) + .min_by_key(|(_s, e)| *e) + .map(|(s, _e)| s) +} + unsafe fn write_flash(buf: &[u8], slot: u8, page: usize) { erase_and_program((slot as u32) * 512 * 1024 + (page as u32) * 4096, buf); } @@ -106,8 +124,12 @@ pub(crate) extern "C" fn usb_handler() { } else { match SerialCommand::try_from(cmd_buf[0]) { Ok(SerialCommand::GetProgramSlot) => { - write_all(serial, &[Response::Ack as u8, 1]); - PROG_SLOT.store(1, Ordering::Relaxed); + if let Some(slot) = best_slot() { + write_all(serial, &[Response::Ack as u8, slot]); + PROG_SLOT.store(slot, Ordering::Relaxed); + } else { + write_all(serial, &[Response::ProgramSlotsFull as u8]); + } }, Ok(SerialCommand::UploadProgram) => { if PROG_SLOT.load(Ordering::Relaxed) == 0 { @@ -149,6 +171,8 @@ pub(crate) extern "C" fn usb_handler() { } SerialState::FlashingProgram { index, page, num_pages, remainder } => { + let slot = PROG_SLOT.load(Ordering::Relaxed); + // Write page 0 last - this is the header, so we only want to write it once everything // else is written successfully // Keep page 0 in the first 4096 bytes of BUF for the end @@ -184,17 +208,20 @@ pub(crate) extern "C" fn usb_handler() { *index = 0; // Actually write the flash page - // TODO: get next slot instead of always using slot 1 - // TODO: wear levelling debug("writing page"); - unsafe { write_flash(&buf[4096..8192], 1, *page) }; + unsafe { write_flash(&buf[4096..8192], slot, *page) }; *page += 1; // If this is the last page, also flash the first page which we didn't // do at the start if *page == num_pages { debug("finalising"); - unsafe { write_flash(&buf[0..4096], 1, 0) }; + + let erase_cycles = erase_cycles(slot); + buf[0..4].copy_from_slice(&(erase_cycles + 1).to_ne_bytes()); + + unsafe { write_flash(&buf[0..4096], slot, 0) }; + PROG_SLOT.store(0, Ordering::Relaxed); NEEDS_REFRESH_PROGRAMS.store(true, Ordering::Relaxed); info("Finished writing program"); diff --git a/eepy-sys/Cargo.toml b/eepy-sys/Cargo.toml index 367d426..eaf6778 100644 --- a/eepy-sys/Cargo.toml +++ b/eepy-sys/Cargo.toml @@ -14,7 +14,8 @@ critical-section = { workspace = true, optional = true } once_cell = { workspace = true, optional = true } strum.workspace = true tickv = { workspace = true, optional = true } +fugit = { workspace = true, optional = true } [features] -defmt = ["dep:defmt", "usb-device/defmt"] +defmt = ["dep:defmt", "usb-device/defmt", "fugit/defmt", "embedded-graphics/defmt"] critical-section-impl = ["critical-section", "critical-section/restore-state-bool", "once_cell"] \ No newline at end of file diff --git a/eepy-sys/src/header.rs b/eepy-sys/src/header.rs index 0ef0cdd..fac59e3 100644 --- a/eepy-sys/src/header.rs +++ b/eepy-sys/src/header.rs @@ -8,19 +8,23 @@ pub const PROGRAM_RAM_AREA_BASE: *mut u8 = 0x20020000 as *mut u8; pub const SLOT_SIZE: usize = 0x80000; -pub const unsafe fn slot(id: u8) -> *const ProgramSlotHeader { +pub const unsafe fn slot_ptr(id: u8) -> *const u8 { if id > 31 { panic!("slot ID must be between 0 and 31"); } if id == 0 { // "Slot 0" is used for the launcher, which is stored 128K into the flash - XIP_BASE.add(128 * 1024).cast() + XIP_BASE.add(128 * 1024) } else { - XIP_BASE.add(SLOT_SIZE * id as usize).cast() + XIP_BASE.add(SLOT_SIZE * id as usize) } } +pub const unsafe fn slot(id: u8) -> *const ProgramSlotHeader { + slot_ptr(id).cast() +} + pub struct Programs { id: u8, diff --git a/eepy-sys/src/misc.rs b/eepy-sys/src/misc.rs index f1ceb64..825fbed 100644 --- a/eepy-sys/src/misc.rs +++ b/eepy-sys/src/misc.rs @@ -103,4 +103,13 @@ pub fn get_time_micros() -> u64 { } ((high_word as u64) << 32) | low_word as u64 +} + + +#[cfg(feature = "fugit")] +pub type Instant = fugit::TimerInstantU64<1_000_000>; + +#[cfg(feature = "fugit")] +pub fn now() -> Instant { + Instant::from_ticks(get_time_micros()) } \ No newline at end of file -- 2.51.2