firmware for my Touchscreen E-Paper Input Module for Framework Laptop 16
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

eepy-launcher: app deletion and multiple apps support

+192 -57
+2 -1
Cargo.toml
··· 42 42 zstd = "0.13" 43 43 strum = { version = "0.27", default-features = false, features = ["derive"] } 44 44 tickv = "2.0" 45 - siphasher = "1.0" 45 + siphasher = "1.0" 46 + fugit = "0.3"
+2 -1
eepy-gui/Cargo.toml
··· 4 4 edition = "2021" 5 5 6 6 [dependencies] 7 - eepy-sys = { path = "../eepy-sys", features = ["embedded-graphics"] } 7 + eepy-sys = { path = "../eepy-sys", features = ["embedded-graphics", "fugit"] } 8 8 tp370pgh01 = { path = "../tp370pgh01" } 9 9 embedded-graphics.workspace = true 10 10 defmt = { workspace = true, optional = true } 11 + fugit.workspace = true 11 12 12 13 [features] 13 14 defmt-derive = ["defmt", "tp370pgh01/defmt", "embedded-graphics/defmt"]
+17 -10
eepy-gui/src/element/button.rs
··· 4 4 use embedded_graphics::primitives::{CornerRadii, PrimitiveStyle, Rectangle, RoundedRectangle}; 5 5 use embedded_graphics::text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder}; 6 6 use embedded_graphics::text::renderer::TextRenderer; 7 + use fugit::ExtU32; 7 8 use eepy_sys::input_common::{Event, TouchEventType}; 9 + use eepy_sys::misc::{now, Instant}; 8 10 use crate::draw_target::EpdDrawTarget; 9 11 use crate::element::{Gui, DEFAULT_PRIMITIVE_STYLE, DEFAULT_TEXT_STYLE}; 10 12 ··· 17 19 #[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] 18 20 pub struct ButtonOutput { 19 21 pub clicked: bool, 22 + pub long_clicked: bool, 20 23 pub needs_refresh: bool, 21 24 } 22 25 ··· 30 33 pub touch_feedback: bool, 31 34 pub touch_feedback_immediate_release: bool, 32 35 33 - began_click: bool, 36 + click_begin_time: Option<Instant>, 34 37 inverted: bool, 35 38 should_uninvert: bool, 36 39 } ··· 44 47 char_style, 45 48 touch_feedback, 46 49 touch_feedback_immediate_release, 47 - began_click: false, 50 + click_begin_time: None, 48 51 inverted: false, 49 52 should_uninvert: false, 50 53 } ··· 59 62 char_style, 60 63 touch_feedback, 61 64 touch_feedback_immediate_release, 62 - began_click: false, 65 + click_begin_time: None, 63 66 inverted: false, 64 67 should_uninvert: false, 65 68 } ··· 73 76 char_style: DEFAULT_TEXT_STYLE, 74 77 touch_feedback: true, 75 78 touch_feedback_immediate_release, 76 - began_click: false, 79 + click_begin_time: None, 77 80 inverted: false, 78 81 should_uninvert: false, 79 82 } ··· 122 125 123 126 if let Event::Touch(ev) = ev { 124 127 if self.rect.contains(ev.eg_point()) { 125 - match (self.began_click, ev.ev_type) { 126 - (false, TouchEventType::Down) => { 127 - self.began_click = true; 128 + match (self.click_begin_time, ev.ev_type) { 129 + (None, TouchEventType::Down) => { 130 + self.click_begin_time = Some(now()); 128 131 if self.touch_feedback { 129 132 self.invert(); 130 133 self.draw_init(target); 131 134 ret.needs_refresh = true; 132 135 } 133 136 }, 134 - (true, TouchEventType::Up) => { 135 - self.began_click = false; 137 + (Some(t), TouchEventType::Up) => { 138 + self.click_begin_time = None; 136 139 if self.inverted { 137 140 if self.touch_feedback_immediate_release { 138 141 self.invert(); ··· 142 145 self.should_uninvert = true; 143 146 } 144 147 } 148 + 145 149 ret.clicked = true; 150 + if now().checked_duration_since(t).unwrap() > 500.millis::<1, 1_000_000>() { 151 + ret.long_clicked = true; 152 + } 146 153 }, 147 154 _ => {}, 148 155 } 149 156 } else { 150 - self.began_click = false; 157 + self.click_begin_time = None; 151 158 if self.inverted { 152 159 if self.touch_feedback_immediate_release { 153 160 self.invert();
+120 -35
eepy-launcher/src/main.rs
··· 6 6 extern crate panic_halt; 7 7 8 8 use core::arch::asm; 9 + use core::mem::offset_of; 9 10 use core::sync::atomic::Ordering; 10 11 use embedded_graphics::geometry::AnchorPoint; 11 12 use embedded_graphics::pixelcolor::BinaryColor; 12 13 use embedded_graphics::prelude::*; 13 14 use embedded_graphics::primitives::{Circle, Line, PrimitiveStyle, Rectangle}; 15 + use embedded_graphics::text::Text; 14 16 use usb_device::bus::UsbBusAllocator; 15 17 use eepy_gui::draw_target::EpdDrawTarget; 16 18 use eepy_gui::element::button::Button; 17 - use eepy_gui::element::Gui; 19 + use eepy_gui::element::{Gui, DEFAULT_TEXT_STYLE}; 18 20 use eepy_gui::element::slider::Slider; 19 21 use eepy_sys::exec::exec; 20 22 use eepy_sys::input::{has_event, next_event, set_touch_enabled}; 21 23 use eepy_sys::input_common::{Event, TouchEventType}; 22 - use eepy_sys::header::{ProgramSlotHeader, Programs}; 24 + use eepy_sys::header::{slot, slot_ptr, ProgramSlotHeader, Programs}; 23 25 use eepy_sys::misc::get_serial; 24 - use eepy_sys::usb; 26 + use eepy_sys::{flash, usb}; 25 27 use eepy_sys::usb::UsbBus; 26 28 use usb_device::prelude::*; 27 29 use usbd_serial::SerialPort; ··· 35 37 main, 36 38 ); 37 39 38 - #[derive(Copy, Clone, Debug, Eq, PartialEq)] 39 - enum Page { 40 - MainPage, 41 - ScratchpadPage, 42 - } 43 - 44 40 struct MainPage { 45 41 scratchpad_button: Button<'static>, 46 42 app_buttons: [Option<(Button<'static>, u8)>; 32], ··· 79 75 } 80 76 81 77 impl Gui for MainPage { 82 - type Output = Option<Page>; 78 + type Output = Option<PageType>; 83 79 84 80 fn draw_init(&self, draw_target: &mut EpdDrawTarget) { 85 81 self.scratchpad_button.draw_init(draw_target); ··· 95 91 96 92 let s = self.scratchpad_button.tick(draw_target, ev); 97 93 if s.clicked { 98 - return Some(Page::ScratchpadPage); 94 + return Some(PageType::ScratchpadPage); 99 95 } else if s.needs_refresh { 100 96 draw_target.refresh(true); 101 97 } ··· 104 100 if let Some((button, s)) = b { 105 101 let response = button.tick(draw_target, ev); 106 102 107 - if response.clicked { 103 + if response.long_clicked { 104 + return Some(PageType::AppInfoPage(*s)); 105 + } else if response.clicked { 108 106 unsafe { cleanup_usb() }; 109 107 exec(*s); 110 108 } ··· 160 158 } 161 159 162 160 impl Gui for ScratchpadPage { 163 - type Output = Option<Page>; 161 + type Output = Option<PageType>; 164 162 165 163 fn draw_init(&self, draw_target: &mut EpdDrawTarget) { 166 164 self.exit_button.draw_init(draw_target); ··· 176 174 177 175 let e = self.exit_button.tick(draw_target, ev); 178 176 if e.clicked { 179 - return Some(Page::MainPage); 177 + return Some(PageType::MainPage); 180 178 } 181 179 refresh |= e.needs_refresh; 182 180 ··· 242 240 } 243 241 } 244 242 245 - struct MainGui { 246 - current_page: Page, 247 - main_page: MainPage, 248 - scratchpad_page: ScratchpadPage, 243 + struct AppInfoPage { 244 + slot: u8, 245 + back_button: Button<'static>, 246 + delete_button: Button<'static>, 247 + } 248 + 249 + impl AppInfoPage { 250 + fn new(slot: u8) -> Self { 251 + Self { 252 + slot, 253 + back_button: Button::with_default_style_auto_sized(Point::new(10, 386), "Back", true), 254 + delete_button: Button::with_default_style_auto_sized(Point::new(10, 60), "Delete program", true), 255 + } 256 + } 257 + } 258 + 259 + impl Gui for AppInfoPage { 260 + type Output = Option<PageType>; 261 + 262 + fn draw_init(&self, target: &mut EpdDrawTarget) { 263 + let header = unsafe { slot(self.slot) }; 264 + 265 + Text::new("Program: ", Point::new(10, 20), DEFAULT_TEXT_STYLE) 266 + .draw(target) 267 + .unwrap(); 268 + 269 + if let Ok(name) = unsafe { (*header).name() } { 270 + Text::new(name, Point::new(120, 20), DEFAULT_TEXT_STYLE) 271 + .draw(target) 272 + .unwrap(); 273 + } else { 274 + Text::new("<invalid>", Point::new(120, 20), DEFAULT_TEXT_STYLE) 275 + .draw(target) 276 + .unwrap(); 277 + } 278 + 279 + 280 + Text::new("Version: ", Point::new(10, 40), DEFAULT_TEXT_STYLE) 281 + .draw(target) 282 + .unwrap(); 283 + 284 + if let Ok(vers) = unsafe { (*header).version() } { 285 + Text::new(vers, Point::new(120, 40), DEFAULT_TEXT_STYLE) 286 + .draw(target) 287 + .unwrap(); 288 + } else { 289 + Text::new("<invalid>", Point::new(120, 40), DEFAULT_TEXT_STYLE) 290 + .draw(target) 291 + .unwrap(); 292 + } 293 + 294 + self.back_button.draw_init(target); 295 + self.delete_button.draw_init(target); 296 + } 297 + 298 + fn tick(&mut self, target: &mut EpdDrawTarget, ev: Event) -> Self::Output { 299 + let b = self.back_button.tick(target, ev); 300 + if b.clicked { 301 + return Some(PageType::MainPage); 302 + } 303 + 304 + let d = self.delete_button.tick(target, ev); 305 + if d.clicked { 306 + let ptr = unsafe { slot_ptr(self.slot) }; 307 + let mut buf = [0u8; 256]; 308 + unsafe { buf.copy_from_slice(core::slice::from_raw_parts(ptr, 256)) }; 309 + let offset = offset_of!(ProgramSlotHeader, len); 310 + buf[offset..offset + 4].copy_from_slice(&[0; 4]); 311 + 312 + unsafe { flash::program(self.slot as u32 * 512 * 1024, &buf) }; 313 + return Some(PageType::MainPage); 314 + } 315 + 316 + None 317 + } 318 + } 319 + 320 + #[derive(Copy, Clone, Debug, Eq, PartialEq)] 321 + enum PageType { 322 + MainPage, 323 + ScratchpadPage, 324 + AppInfoPage(u8), 325 + } 326 + 327 + enum MainGui { 328 + MainPage(MainPage), 329 + ScratchpadPage(ScratchpadPage), 330 + AppInfoPage(AppInfoPage), 249 331 } 250 332 251 333 impl MainGui { 252 334 fn new() -> Self { 253 - Self { 254 - current_page: Page::MainPage, 255 - main_page: MainPage::new(), 256 - scratchpad_page: ScratchpadPage::new(), 257 - } 335 + Self::MainPage(MainPage::new()) 258 336 } 259 337 260 - fn get_current_page(&self) -> &dyn Gui<Output = Option<Page>> { 261 - match self.current_page { 262 - Page::MainPage => &self.main_page, 263 - Page::ScratchpadPage => &self.scratchpad_page, 338 + fn get_current_page(&self) -> &dyn Gui<Output = Option<PageType>> { 339 + match self { 340 + MainGui::MainPage(page) => page, 341 + MainGui::ScratchpadPage(page) => page, 342 + MainGui::AppInfoPage(page) => page, 264 343 } 265 344 } 266 345 267 - fn get_current_page_mut(&mut self) -> &mut dyn Gui<Output = Option<Page>> { 268 - match self.current_page { 269 - Page::MainPage => &mut self.main_page, 270 - Page::ScratchpadPage => &mut self.scratchpad_page, 346 + fn get_current_page_mut(&mut self) -> &mut dyn Gui<Output = Option<PageType>> { 347 + match self { 348 + MainGui::MainPage(page) => page, 349 + MainGui::ScratchpadPage(page) => page, 350 + MainGui::AppInfoPage(page) => page, 271 351 } 272 352 } 273 353 } ··· 281 361 282 362 fn tick(&mut self, draw_target: &mut EpdDrawTarget, ev: Event) -> Self::Output { 283 363 if let Some(page) = self.get_current_page_mut().tick(draw_target, ev) { 284 - self.current_page = page; 364 + *self = match page { 365 + PageType::MainPage => MainGui::MainPage(MainPage::new()), 366 + PageType::ScratchpadPage => MainGui::ScratchpadPage(ScratchpadPage::new()), 367 + PageType::AppInfoPage(slot) => MainGui::AppInfoPage(AppInfoPage::new(slot)), 368 + }; 369 + 285 370 draw_target.clear(BinaryColor::Off).unwrap(); 286 371 self.draw_init(draw_target); 287 372 draw_target.refresh(false); ··· 339 424 } 340 425 341 426 if NEEDS_REFRESH_PROGRAMS.swap(false, Ordering::Relaxed) { 342 - gui.main_page.refresh_buttons(); 343 - if gui.current_page == Page::MainPage { 344 - gui.draw_init(&mut draw_target); 427 + if let MainGui::MainPage(page) = &mut gui { 428 + page.refresh_buttons(); 429 + page.draw_init(&mut draw_target); 345 430 draw_target.refresh(false); 346 431 } 347 432 } else if NEEDS_REFRESH.swap(false, Ordering::Relaxed) {
+33 -6
eepy-launcher/src/serial.rs
··· 5 5 use usbd_serial::SerialPort; 6 6 use eepy_serial::{Response, SerialCommand}; 7 7 use eepy_sys::flash::erase_and_program; 8 + use eepy_sys::header::{slot, slot_ptr}; 8 9 use eepy_sys::image::refresh; 9 10 use eepy_sys::IMAGE_BYTES; 10 11 use eepy_sys::input::{next_event, set_touch_enabled}; ··· 27 28 num_pages: Option<usize>, 28 29 remainder: Option<usize>, 29 30 }, 31 + } 32 + 33 + fn erase_cycles(slot: u8) -> u32 { 34 + let c = unsafe { u32::from_ne_bytes(*slot_ptr(slot).cast()) }; 35 + if c == u32::MAX { 36 + 0 37 + } else { 38 + c 39 + } 40 + } 41 + 42 + fn best_slot() -> Option<u8> { 43 + (1u8..=31) 44 + .filter(|s| unsafe { !(*slot(*s)).is_valid() }) 45 + .map(|s| (s, erase_cycles(s))) 46 + .min_by_key(|(_s, e)| *e) 47 + .map(|(s, _e)| s) 30 48 } 31 49 32 50 unsafe fn write_flash(buf: &[u8], slot: u8, page: usize) { ··· 106 124 } else { 107 125 match SerialCommand::try_from(cmd_buf[0]) { 108 126 Ok(SerialCommand::GetProgramSlot) => { 109 - write_all(serial, &[Response::Ack as u8, 1]); 110 - PROG_SLOT.store(1, Ordering::Relaxed); 127 + if let Some(slot) = best_slot() { 128 + write_all(serial, &[Response::Ack as u8, slot]); 129 + PROG_SLOT.store(slot, Ordering::Relaxed); 130 + } else { 131 + write_all(serial, &[Response::ProgramSlotsFull as u8]); 132 + } 111 133 }, 112 134 Ok(SerialCommand::UploadProgram) => { 113 135 if PROG_SLOT.load(Ordering::Relaxed) == 0 { ··· 149 171 } 150 172 151 173 SerialState::FlashingProgram { index, page, num_pages, remainder } => { 174 + let slot = PROG_SLOT.load(Ordering::Relaxed); 175 + 152 176 // Write page 0 last - this is the header, so we only want to write it once everything 153 177 // else is written successfully 154 178 // Keep page 0 in the first 4096 bytes of BUF for the end ··· 184 208 *index = 0; 185 209 186 210 // Actually write the flash page 187 - // TODO: get next slot instead of always using slot 1 188 - // TODO: wear levelling 189 211 debug("writing page"); 190 - unsafe { write_flash(&buf[4096..8192], 1, *page) }; 212 + unsafe { write_flash(&buf[4096..8192], slot, *page) }; 191 213 192 214 *page += 1; 193 215 // If this is the last page, also flash the first page which we didn't 194 216 // do at the start 195 217 if *page == num_pages { 196 218 debug("finalising"); 197 - unsafe { write_flash(&buf[0..4096], 1, 0) }; 219 + 220 + let erase_cycles = erase_cycles(slot); 221 + buf[0..4].copy_from_slice(&(erase_cycles + 1).to_ne_bytes()); 222 + 223 + unsafe { write_flash(&buf[0..4096], slot, 0) }; 224 + PROG_SLOT.store(0, Ordering::Relaxed); 198 225 199 226 NEEDS_REFRESH_PROGRAMS.store(true, Ordering::Relaxed); 200 227 info("Finished writing program");
+2 -1
eepy-sys/Cargo.toml
··· 14 14 once_cell = { workspace = true, optional = true } 15 15 strum.workspace = true 16 16 tickv = { workspace = true, optional = true } 17 + fugit = { workspace = true, optional = true } 17 18 18 19 [features] 19 - defmt = ["dep:defmt", "usb-device/defmt"] 20 + defmt = ["dep:defmt", "usb-device/defmt", "fugit/defmt", "embedded-graphics/defmt"] 20 21 critical-section-impl = ["critical-section", "critical-section/restore-state-bool", "once_cell"]
+7 -3
eepy-sys/src/header.rs
··· 8 8 9 9 pub const SLOT_SIZE: usize = 0x80000; 10 10 11 - pub const unsafe fn slot(id: u8) -> *const ProgramSlotHeader { 11 + pub const unsafe fn slot_ptr(id: u8) -> *const u8 { 12 12 if id > 31 { 13 13 panic!("slot ID must be between 0 and 31"); 14 14 } 15 15 16 16 if id == 0 { 17 17 // "Slot 0" is used for the launcher, which is stored 128K into the flash 18 - XIP_BASE.add(128 * 1024).cast() 18 + XIP_BASE.add(128 * 1024) 19 19 } else { 20 - XIP_BASE.add(SLOT_SIZE * id as usize).cast() 20 + XIP_BASE.add(SLOT_SIZE * id as usize) 21 21 } 22 + } 23 + 24 + pub const unsafe fn slot(id: u8) -> *const ProgramSlotHeader { 25 + slot_ptr(id).cast() 22 26 } 23 27 24 28
+9
eepy-sys/src/misc.rs
··· 103 103 } 104 104 105 105 ((high_word as u64) << 32) | low_word as u64 106 + } 107 + 108 + 109 + #[cfg(feature = "fugit")] 110 + pub type Instant = fugit::TimerInstantU64<1_000_000>; 111 + 112 + #[cfg(feature = "fugit")] 113 + pub fn now() -> Instant { 114 + Instant::from_ticks(get_time_micros()) 106 115 }