diff --git a/assets/happy-1.png b/assets/happy-1.png new file mode 100644 index 0000000..23c6d69 Binary files /dev/null and b/assets/happy-1.png differ diff --git a/assets/happy-1.raw b/assets/happy-1.raw new file mode 100644 index 0000000..3952cdb Binary files /dev/null and b/assets/happy-1.raw differ diff --git a/assets/happy-2.png b/assets/happy-2.png new file mode 100644 index 0000000..9faf8b5 Binary files /dev/null and b/assets/happy-2.png differ diff --git a/assets/happy-2.raw b/assets/happy-2.raw new file mode 100644 index 0000000..7be7278 Binary files /dev/null and b/assets/happy-2.raw differ diff --git a/assets/idle-blink-1.png b/assets/idle-blink-1.png new file mode 100644 index 0000000..c93a831 Binary files /dev/null and b/assets/idle-blink-1.png differ diff --git a/assets/idle-blink-1.raw b/assets/idle-blink-1.raw new file mode 100644 index 0000000..36d4d60 Binary files /dev/null and b/assets/idle-blink-1.raw differ diff --git a/assets/idle-blink-2.png b/assets/idle-blink-2.png new file mode 100644 index 0000000..d31a3ed Binary files /dev/null and b/assets/idle-blink-2.png differ diff --git a/assets/idle-blink-2.raw b/assets/idle-blink-2.raw new file mode 100644 index 0000000..1f9d0c3 Binary files /dev/null and b/assets/idle-blink-2.raw differ diff --git a/assets/idle-normal-1.png b/assets/idle-normal-1.png new file mode 100644 index 0000000..70876ff Binary files /dev/null and b/assets/idle-normal-1.png differ diff --git a/assets/idle-normal-1.raw b/assets/idle-normal-1.raw new file mode 100644 index 0000000..0317cc4 Binary files /dev/null and b/assets/idle-normal-1.raw differ diff --git a/assets/idle-normal-2.png b/assets/idle-normal-2.png new file mode 100644 index 0000000..8f19bef Binary files /dev/null and b/assets/idle-normal-2.png differ diff --git a/assets/idle-normal-2.raw b/assets/idle-normal-2.raw new file mode 100644 index 0000000..6889fcc Binary files /dev/null and b/assets/idle-normal-2.raw differ diff --git a/assets/sleep-1.png b/assets/sleep-1.png new file mode 100644 index 0000000..5cfb528 Binary files /dev/null and b/assets/sleep-1.png differ diff --git a/assets/sleep-1.raw b/assets/sleep-1.raw new file mode 100644 index 0000000..c735a4b Binary files /dev/null and b/assets/sleep-1.raw differ diff --git a/assets/sleep-2.png b/assets/sleep-2.png new file mode 100644 index 0000000..a83ce8e Binary files /dev/null and b/assets/sleep-2.png differ diff --git a/assets/sleep-2.raw b/assets/sleep-2.raw new file mode 100644 index 0000000..e5f7393 Binary files /dev/null and b/assets/sleep-2.raw differ diff --git a/src/display.rs b/src/display.rs index 9ae1926..eb8ec1b 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,15 +1,20 @@ +use embassy_time::{Duration, Instant, Timer}; use embedded_graphics::{ - mono_font::{MonoTextStyleBuilder, ascii::FONT_6X10}, - pixelcolor::BinaryColor, - prelude::Point, - text::{Baseline, Text}, - draw_target::DrawTarget, - Drawable, + Drawable, draw_target::DrawTarget, image::{Image, ImageRaw}, mono_font::{MonoTextStyleBuilder, iso_8859_1::FONT_10X20}, pixelcolor::BinaryColor, prelude::Point, text::{Baseline, Text} }; use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306, mode::BufferedGraphicsMode}; use embassy_rp::i2c::I2c; use core::fmt::Write; +use crate::wpm::WPM_CHANNEL; + +// Helper enum for all possible animation states +enum PuppyAnim { + Sleep, + Idle, + Happy, +} + /// Display type encapsulating the I2C interface, its size and graphics mode pub type Display = Ssd1306< I2CInterface>, @@ -33,39 +38,91 @@ pub fn init_display(bus: I2c<'static, embassy_rp::peripherals::I2C0, embassy_rp: } } -/// Temp function which updates the display with currently pressed keys -pub fn update_display( - display: &mut Option, - pressed_keys: &[(usize, usize, crate::keymap::Action)], -) -> Option<()> { +pub async fn run_display(display: &mut Option) -> Option<()> { if let Some(display) = display { - display.clear(BinaryColor::Off).ok()?; - - let text_style = MonoTextStyleBuilder::new() - .font(&FONT_6X10) - .text_color(BinaryColor::On) - .build(); - - if pressed_keys.is_empty() { - Text::with_baseline("No keys", Point::zero(), text_style, Baseline::Top) + let mut wpm = 0; + let mut frame = 0; + let mut blink_frame = 0; + let mut last_frame_ms = Instant::now(); + + loop { + // Choose state based on wpm + let (state, frame_delay_ms) = if wpm == 0 { + (PuppyAnim::Sleep, 250) + } else if wpm < 60 { + // faster tail wagging, connected to wpm + let delay = (3000 / wpm.max(10)).clamp(100, 300); + (PuppyAnim::Idle, delay) + } else { + // even faster tail wagging!! + let delay = (4000 / wpm).clamp(16, 100); + (PuppyAnim::Happy, delay) + }; + + if last_frame_ms.elapsed().as_millis() >= frame_delay_ms { + // used for determining when to blink + blink_frame += 1; + // switch between tail left and right + frame = (frame + 1) % 2; + last_frame_ms = Instant::now(); + } + + // Choose image based on current state and tail position + let image_data = match (state, frame) { + (PuppyAnim::Sleep, 0) => include_bytes!("../assets/sleep-1.raw"), + (PuppyAnim::Sleep, 1) => include_bytes!("../assets/sleep-2.raw"), + (PuppyAnim::Idle, 0) => { + // blink every once in a while + if blink_frame % 20 < 5 { + include_bytes!("../assets/idle-blink-1.raw") + } else { + include_bytes!("../assets/idle-normal-1.raw") + } + }, + (PuppyAnim::Idle, 1) => { + if blink_frame % 20 < 5 { + include_bytes!("../assets/idle-blink-2.raw") + } else { + include_bytes!("../assets/idle-normal-2.raw") + } + }, + (PuppyAnim::Happy, 0) => include_bytes!("../assets/happy-1.raw"), + (PuppyAnim::Happy, 1) => include_bytes!("../assets/happy-2.raw"), + _ => include_bytes!("../assets/idle-normal-1.raw"), + }; + + + display.clear(BinaryColor::Off).ok()?; + + // Load and draw the chosen image + let raw: ImageRaw = ImageRaw::new(image_data, 32); + let im = Image::new(&raw, Point::new(0, 0)); + im.draw(display).ok()?; + + let text_style = MonoTextStyleBuilder::new() + .font(&FONT_10X20) + .text_color(BinaryColor::On) + .build(); + + match WPM_CHANNEL.try_receive() { + Ok(current_wpm) => { + wpm = current_wpm; + }, + Err(_) => {}, + } + + let mut wpm_str = heapless::String::<32>::new(); + write!(&mut wpm_str, "WPM: {}", wpm).ok(); + + // Print current WPM + Text::with_baseline(&wpm_str, Point::new(32 + 4, 6), text_style, Baseline::Top) .draw(display) .ok()?; - } else { - let mut y_pos = 0; - for (row_idx, col_idx, keycode) in pressed_keys.iter().take(3) { - let mut msg = heapless::String::<32>::new(); - write!(&mut msg, "R{}C{}: 0x{:?}", row_idx, col_idx, keycode).ok(); - Text::with_baseline(&msg, Point::new(0, y_pos), text_style, Baseline::Top) - .draw(display) - .ok()?; - - y_pos += 10; - } + display.flush().ok()?; + + Timer::after(Duration::from_millis(16)).await; } - - display.flush().ok()?; - Some(()) } else { None } diff --git a/src/main.rs b/src/main.rs index deb5fea..be1fc39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod matrix; mod keymap; mod display; mod usb; +mod wpm; use defmt::{info, warn}; use embassy_executor::Spawner; @@ -15,9 +16,9 @@ use embassy_rp::{ peripherals::USB, usb::{Driver, InterruptHandler as UsbInterruptHandler}, }; -use embassy_time::{Duration, Timer}; +use embassy_time::{Duration, Instant, Timer}; use usbd_hid::descriptor::KeyboardReport; -use crate::keymap::{Action, LayerOperation, get_action}; +use crate::{keymap::{Action, LayerOperation, get_action}, wpm::{BucketWpmTracker, WPM_CHANNEL}}; use {defmt_rtt as _, panic_probe as _}; @@ -26,7 +27,6 @@ bind_interrupts!(struct Irqs { USBCTRL_IRQ => UsbInterruptHandler; }); - #[embassy_executor::main] async fn main(_spawner: Spawner) -> () { let p = embassy_rp::init(Default::default()); @@ -70,6 +70,10 @@ async fn main(_spawner: Spawner) -> () { let mut previous_modifier: u8 = 0; let mut active_layer: usize = 0; + // WPM tracking + let mut wpm_tracker = BucketWpmTracker::new(); + let mut last_wpm_send = Instant::now(); + loop { let scan_result = matrix.scan().await; @@ -130,11 +134,15 @@ async fn main(_spawner: Spawner) -> () { } } - // Update display - display::update_display(&mut display, &pressed_keys); - // Send HID report if keycodes or modifiers changed if keycodes != previous_keycodes || modifier != previous_modifier { + // Detect new keypresses for WPM tracking + for keycode in keycodes { + if keycode != 0 && !previous_keycodes.contains(&keycode) { + wpm_tracker.add_keypress(Instant::now().as_millis()); + } + } + if keycodes != [0; 6] || modifier != 0 { info!("Keys: {:?}, Mods: 0x{:02X}", keycodes, modifier); } else { @@ -157,6 +165,13 @@ async fn main(_spawner: Spawner) -> () { previous_modifier = modifier; } + // Send WPM to the channel every 100ms + if last_wpm_send.elapsed().as_millis() >= 100 { + let wpm = wpm_tracker.calculate_wpm(Instant::now().as_millis()); + WPM_CHANNEL.try_send(wpm).ok(); + last_wpm_send = Instant::now(); + } + Timer::after(Duration::from_millis(10)).await; } }; @@ -166,5 +181,9 @@ async fn main(_spawner: Spawner) -> () { reader.run(false, request_handler).await; }; - join(usb_fut, join(in_fut, out_fut)).await; + let disp_fut = async { + display::run_display(&mut display).await; + }; + + join(disp_fut, join(usb_fut, join(in_fut, out_fut))).await; } diff --git a/src/wpm.rs b/src/wpm.rs new file mode 100644 index 0000000..c7fb54c --- /dev/null +++ b/src/wpm.rs @@ -0,0 +1,55 @@ +use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel}; + +// Channel for sending WPM data +pub static WPM_CHANNEL: Channel = Channel::new(); + +pub struct BucketWpmTracker { + buckets: [u8; 15], // 15 one-second buckets + current_second: u64, +} + +impl BucketWpmTracker { + pub fn new() -> Self { + Self { + buckets: [0; 15], + current_second: 0, + } + } + + pub fn add_keypress(&mut self, timestamp_ms: u64) { + let second = timestamp_ms / 1000; + let bucket_idx = (second % 15) as usize; + + // On new second, clear old buckets + if second != self.current_second { + let seconds_elapsed = second.saturating_sub(self.current_second).min(15); + for i in 0..seconds_elapsed { + let idx = ((self.current_second + i + 1) % 15) as usize; + self.buckets[idx] = 0; + } + self.current_second = second; + } + + self.buckets[bucket_idx] = self.buckets[bucket_idx].saturating_add(1); + } + + pub fn calculate_wpm(&mut self, current_time_ms: u64) -> u64 { + let current_second = current_time_ms / 1000; + + // Clear buckets older than 15 seconds + if current_second != self.current_second { + let seconds_elapsed = current_second.saturating_sub(self.current_second).min(15); + for i in 0..seconds_elapsed { + let idx = ((self.current_second + i + 1) % 15) as usize; + self.buckets[idx] = 0; + } + self.current_second = current_second; + } + + let total_chars: u32 = self.buckets.iter().map(|&x| x as u32).sum(); + + // extrapolate 15s of data to 60s + // WPM = (chars / 5 chars per word) * (60 seconds / 15 seconds) + ((total_chars as f64 * 4f64) / 5f64) as u64 + } +} \ No newline at end of file