From 97ba5d7b9acd4686fc8fa04282822fd4cb7bc054 Mon Sep 17 00:00:00 2001 From: Real Root Date: Sun, 9 Feb 2025 20:28:19 +0100 Subject: [PATCH 1/2] add timer --- firmware/src/state.rs | 55 ++++++++++++++++++++++++++++++++++- watchful-ui/src/lib.rs | 65 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/firmware/src/state.rs b/firmware/src/state.rs index 74dcd61..1dfea51 100644 --- a/firmware/src/state.rs +++ b/firmware/src/state.rs @@ -2,7 +2,7 @@ use defmt::info; use embassy_futures::select::{select, select3, Either, Either3}; use embassy_time::{Duration, Instant, Ticker, Timer}; use embedded_graphics::prelude::*; -use watchful_ui::{FirmwareDetails, MenuAction, MenuView, TimeView, WorkoutView}; +use watchful_ui::{FirmwareDetails, MenuAction, MenuView, TimerView, TimeView, WorkoutView}; use crate::device::Device; @@ -41,6 +41,7 @@ pub enum WatchState { Time(TimeState), Menu(MenuState), // FindPhone, + Timer(TimerState), Workout(WorkoutState), } @@ -56,6 +57,7 @@ impl defmt::Format for WatchState { Self::Idle(_) => defmt::write!(fmt, "Idle"), Self::Time(_) => defmt::write!(fmt, "Time"), Self::Menu(_) => defmt::write!(fmt, "Menu"), + Self::Timer(_) => defmt::write!(fmt, "Timer"), Self::Workout(_) => defmt::write!(fmt, "Workout"), } } @@ -68,6 +70,7 @@ impl WatchState { WatchState::Time(state) => state.draw(device).await, WatchState::Menu(state) => state.draw(device).await, WatchState::Workout(state) => state.draw(device).await, + WatchState::Timer(state) => state.draw(device).await, } } @@ -77,6 +80,7 @@ impl WatchState { WatchState::Time(state) => state.next(device).await, WatchState::Menu(state) => state.next(device).await, WatchState::Workout(state) => state.next(device).await, + WatchState::Timer(state) => state.next(device).await, } } } @@ -206,6 +210,10 @@ impl MenuState { defmt::info!("Not implemented"); WatchState::Workout(WorkoutState {}) } + MenuAction::Timer => { + defmt::info!("Not implemented"); + WatchState::Timer(TimerState {}) + } MenuAction::FindPhone => { defmt::info!("Not implemented"); WatchState::Time(TimeState::new(device, Timeout::new(IDLE_TIMEOUT)).await) @@ -272,6 +280,51 @@ impl WorkoutState { } } +#[derive(PartialEq)] +pub struct TimerState {} + +impl TimerState { + pub async fn draw(&mut self, _device: &mut Device<'_>) {} + pub async fn next(&mut self, device: &mut Device<'_>) -> WatchState { + let screen = &mut device.screen; + let button = &mut device.button; + let mut ticker = Ticker::every(Duration::from_secs(1)); + let vibrator = &mut device.vibrator; + + // Hardcoded for now + let mut seconds = 5; + let timer = async { + loop { + TimerView::new(time::Duration::new(seconds, 0), true) + .draw(screen.display()) + .unwrap(); + screen.on(); + ticker.next().await; + seconds -= 1; + + if seconds <= 0 { + break; + } + } + + TimerView::new(time::Duration::ZERO, false) + .draw(screen.display()) + .unwrap(); + screen.on(); + vibrator.on_for(1500).await; + }; + + let next = match select(button.wait(), timer).await { + Either::First(_) => { + vibrator.off(); + WatchState::Menu(MenuState::new(MenuView::main())) + }, + Either::Second(_) => WatchState::Timer(TimerState {}), + }; + next + } +} + async fn firmware_details(battery: &crate::device::Battery<'_>, validated: bool) -> FirmwareDetails { const CARGO_NAME: &str = env!("CARGO_PKG_NAME"); const CARGO_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/watchful-ui/src/lib.rs b/watchful-ui/src/lib.rs index 9dc6c45..72699de 100644 --- a/watchful-ui/src/lib.rs +++ b/watchful-ui/src/lib.rs @@ -16,7 +16,7 @@ use u8g2_fonts::{fonts, U8g2TextStyle}; const WIDTH: u32 = 240; const HEIGHT: u32 = 240; -const GRID_ITEMS: u32 = 3; +const GRID_ITEMS: u32 = 4; fn watch_text_style(color: Rgb) -> U8g2TextStyle { //U8g2TextStyle::new(fonts::u8g2_font_unifont_t_symbols, Rgb::YELLOW) @@ -187,10 +187,58 @@ impl WorkoutView { } } +#[derive(PartialEq, Copy, Clone)] +pub struct TimerView { + pub remaining: time::Duration, + pub running: bool, +} + +impl TimerView { + pub fn new(remaining: time::Duration, running: bool) -> Self { + Self { remaining, running } + } + pub fn draw>(&self, display: &mut D) -> Result<(), D::Error> { + display.clear(Rgb::BLACK)?; + + let mut buf: heapless::String<16> = heapless::String::new(); + write!( + buf, + //"{:02}:{:02}:{:02}", + "{:02}:{:02}", + //self.remaining.whole_hours(), + self.remaining.whole_minutes(), + self.remaining.whole_seconds() % 60, + //self.remaining.subsec_milliseconds() / 100 + ) + .unwrap(); + + let cd = Text::with_text_style( + &buf, + display.bounding_box().center(), + watch_text_style(Rgb::CSS_DARK_CYAN), + TextStyleBuilder::new() + .alignment(embedded_graphics::text::Alignment::Center) + .baseline(embedded_graphics::text::Baseline::Alphabetic) + .build(), + ); + + let display_area = display.bounding_box(); + LinearLayout::vertical(Chain::new(cd)) + .with_spacing(spacing::FixedMargin(10)) + .with_alignment(horizontal::Center) + .arrange() + .align_to(&display_area, horizontal::Center, vertical::Center) + .draw(display)?; + + Ok(()) + } +} + #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum MenuAction { Workout, + Timer, FindPhone, Settings, FirmwareSettings, @@ -203,6 +251,7 @@ pub enum MenuAction { pub enum MenuView { Main { workout: MenuItem, + timer: MenuItem, find_phone: MenuItem, settings: MenuItem, }, @@ -221,8 +270,9 @@ impl MenuView { pub fn main() -> Self { Self::Main { workout: MenuItem::new("Workout", 0), - find_phone: MenuItem::new("Find Phone", 1), - settings: MenuItem::new("Settings", 2), + timer: MenuItem::new("Timer", 1), + find_phone: MenuItem::new("Find Phone", 2), + settings: MenuItem::new("Settings", 3), } } @@ -238,7 +288,7 @@ impl MenuView { let valid = details.validated; Self::Firmware { details, - item: MenuItem::new(if valid { "Validated" } else { "Validate" }, 2), + item: MenuItem::new(if valid { "Validated" } else { "Validate" }, 3), } } @@ -248,10 +298,12 @@ impl MenuView { match self { Self::Main { workout, + timer, find_phone, settings, } => { workout.draw(display)?; + timer.draw(display)?; find_phone.draw(display)?; settings.draw(display)?; } @@ -275,11 +327,14 @@ impl MenuView { match self { Self::Main { workout, + timer, find_phone, settings, } => { if workout.is_clicked(input) { Some(MenuAction::Workout) + } else if timer.is_clicked(input) { + Some(MenuAction::Timer) } else if find_phone.is_clicked(input) { Some(MenuAction::FindPhone) } else if settings.is_clicked(input) { @@ -336,7 +391,7 @@ impl MenuItem { self.text, Point::new( (WIDTH as i32) / 2, - self.idx as i32 * (HEIGHT as i32 / GRID_ITEMS as i32) + 47, + self.idx as i32 * (HEIGHT as i32 / GRID_ITEMS as i32) + 40, ), menu_text_style(Rgb::CSS_CORNSILK), TextStyleBuilder::new() From 344301b85dc8bd32b3c4662cd15678cbb06c6558 Mon Sep 17 00:00:00 2001 From: Real Root Date: Sun, 9 Feb 2025 20:29:11 +0100 Subject: [PATCH 2/2] add .gitignore --- .gitignore | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab951f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# ---> Rust +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/