From a2f4a490cbdecb58ddb73ad6f037bb7de149e3b1 Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:02:46 -0400 Subject: [PATCH 1/8] pointing device support + iqs5xx --- rumcake-macros/src/keyboard.rs | 28 +++++ rumcake/Cargo.toml | 2 + rumcake/src/drivers/iqs5xx.rs | 107 ++++++++++++++++++ rumcake/src/drivers/mod.rs | 3 + rumcake/src/hw/mod.rs | 9 ++ rumcake/src/lib.rs | 8 +- rumcake/src/pointer.rs | 198 +++++++++++++++++++++++++++++++++ rumcake/src/usb.rs | 49 ++++++++ 8 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 rumcake/src/drivers/iqs5xx.rs create mode 100644 rumcake/src/pointer.rs diff --git a/rumcake-macros/src/keyboard.rs b/rumcake-macros/src/keyboard.rs index 2b93e4d..7a0f545 100644 --- a/rumcake-macros/src/keyboard.rs +++ b/rumcake-macros/src/keyboard.rs @@ -14,6 +14,7 @@ pub(crate) struct KeyboardSettings { no_matrix: bool, bluetooth: bool, usb: bool, + pointer: Option, storage: Option, simple_backlight: Option, simple_backlight_matrix: Option, @@ -27,6 +28,11 @@ pub(crate) struct KeyboardSettings { bootloader_double_tap_reset: Option>, } +#[derive(Debug, FromMeta)] +pub(crate) struct PointerSettings { + driver_setup_fn: Ident, +} + #[derive(Debug, FromMeta)] pub(crate) struct LightingSettings { id: Ident, @@ -361,6 +367,28 @@ pub(crate) fn keyboard_main( } } + if keyboard.usb && keyboard.pointer.is_some() { + initialization.extend(quote! { + // HID consumer + let pointer_class = ::rumcake::usb::setup_usb_hid_mouse_writer(&mut builder); + }); + spawning.extend(quote! { + // HID Consumer Report sending + spawner.spawn(::rumcake::usb_hid_mouse_write_task!(#kb_name, pointer_class)).unwrap(); + }); + } + + if let Some(args) = keyboard.pointer { + let setup_fn = args.driver_setup_fn; + initialization.extend(quote! { + let pointer_driver = #setup_fn().await; + }); + spawning.extend(quote! { + // HID Consumer Report sending + spawner.spawn(::rumcake::poll_pointing_device!(#kb_name, pointer_driver)).unwrap(); + }); + } + if keyboard.usb && (keyboard.via.is_some() || keyboard.vial.is_some()) { initialization.extend(quote! { static VIA_COMMAND_HANDLER: ::rumcake::usb::ViaCommandHandler<#kb_name> = ::rumcake::usb::ViaCommandHandler::new(); diff --git a/rumcake/Cargo.toml b/rumcake/Cargo.toml index 0796885..19c94ab 100644 --- a/rumcake/Cargo.toml +++ b/rumcake/Cargo.toml @@ -76,6 +76,7 @@ smart-leds = "0.3.0" # third party drivers is31fl3731 = { git = "https://github.com/Univa/is31fl3731", features = ["async"], optional = true } ssd1306 = { version = "0.8.2", optional = true } +iqs5xx = { version = "0.1.2", optional = true } rumcake-macros = { path = "../rumcake-macros" } @@ -135,4 +136,5 @@ split-central = ["nrf-softdevice?/ble-central", "nrf-softdevice?/ble-gatt-client ws2812-bitbang = [] is31fl3731 = ["dep:is31fl3731"] ssd1306 = ["dep:ssd1306"] +iqs5xx = ["dep:iqs5xx"] diff --git a/rumcake/src/drivers/iqs5xx.rs b/rumcake/src/drivers/iqs5xx.rs new file mode 100644 index 0000000..7c9837a --- /dev/null +++ b/rumcake/src/drivers/iqs5xx.rs @@ -0,0 +1,107 @@ +use defmt::Debug2Format; +use embassy_time::Delay; +use embedded_hal::blocking::i2c::{Write, WriteRead}; +use embedded_hal::digital::v2::{InputPin, OutputPin}; +pub use iqs5xx; +use iqs5xx::{Event, IQS5xx as IQS5xxDriver, Report}; + +use crate::pointer::mouse::{MouseButtonFlags, MouseEvent}; +use crate::pointer::touchpad::{Touchpad, TouchpadEvent}; +use crate::pointer::PointingDriver; + +struct IQS5xx { + driver: IQS5xxDriver, + touchpad_state: Touchpad, + event_handler: E, +} + +pub fn setup_driver( + i2c: I2C, + rdy: RDY, + rst: RST, + event_handler: E, +) -> IQS5xx { + let mut iqs = IQS5xx { + driver: IQS5xxDriver::new(i2c, 0, rdy, rst), + event_handler, + touchpad_state: Touchpad::new(), + }; + iqs.driver.reset(&mut Delay).unwrap(); + iqs.driver.poll_ready(&mut Delay).unwrap(); + iqs.driver.init().unwrap(); + iqs +} + +trait IQS5xxPointerDriver { + /// This function gets called a regular interval (usually every millisecond). You can + /// re-implement this if you the type you're implementing this trait on needs to update its + /// state over time. This can be useful if you want to implement more complicated touchpad + /// functionality which isn't already supported by the [`Touchpad`] struct. + fn tick(&mut self) {} + + fn handle_event(&mut self, state: &mut Touchpad, _report: Report, event: Event) { + match event { + iqs5xx::Event::Move { x, y } => { + state.register(TouchpadEvent::Movement(x as i8, y as i8)); + } + iqs5xx::Event::SingleTap { x, y } => { + state.register(TouchpadEvent::Tap(MouseButtonFlags::LEFT)); + } + iqs5xx::Event::PressHold { x, y } => { + state.register(TouchpadEvent::Hold(MouseButtonFlags::LEFT)); + state.register(TouchpadEvent::Movement(x as i8, y as i8)); + } + iqs5xx::Event::TwoFingerTap => { + state.register(TouchpadEvent::Tap(MouseButtonFlags::RIGHT)); + } + iqs5xx::Event::Scroll { x, y: _ } if x != 0 => { + state.register(TouchpadEvent::Scroll(x as i8, 0)); + } + iqs5xx::Event::Scroll { x: _, y } if y != 0 => { + state.register(TouchpadEvent::Scroll(0, y as i8)); + } + _ => {} + }; + } +} + +impl IQS5xx +where + RDY: InputPin, + RST: OutputPin, +{ + async fn tick(&mut self) -> impl Iterator + '_ { + self.touchpad_state.tick(); + self.event_handler.tick(); + + let report = self.driver.try_transact(|driver| driver.get_report()); + + match report { + Ok(Some(report)) => { + let event = iqs5xx::Event::from(&report); + self.event_handler + .handle_event(&mut self.touchpad_state, report, event); + } + Err(error) => { + defmt::warn!( + "[IQS5XX_DRIVER] Could not get report: {}", + Debug2Format(&error) + ); + } + _ => {} + } + + self.touchpad_state.events() + } +} + +impl PointingDriver + for IQS5xx +where + RDY: InputPin, + RST: OutputPin, +{ + async fn tick(&mut self) -> impl Iterator { + self.tick().await + } +} diff --git a/rumcake/src/drivers/mod.rs b/rumcake/src/drivers/mod.rs index a038972..eaf99a6 100644 --- a/rumcake/src/drivers/mod.rs +++ b/rumcake/src/drivers/mod.rs @@ -14,6 +14,9 @@ pub mod ssd1306; #[cfg(feature = "ws2812-bitbang")] pub mod ws2812_bitbang; +#[cfg(feature = "iqs5xx")] +pub mod iqs5xx; + /// Struct that allows you to use a serial driver (implementor of both [`embedded_io_async::Read`] /// and [`embedded_io_async::Write`]) with rumcake. This can be used for split keyboards. pub struct SerialSplitDriver { diff --git a/rumcake/src/hw/mod.rs b/rumcake/src/hw/mod.rs index 19940bb..937d361 100644 --- a/rumcake/src/hw/mod.rs +++ b/rumcake/src/hw/mod.rs @@ -31,6 +31,7 @@ use embedded_hal::digital::v2::OutputPin; use platform::RawMutex; use usbd_human_interface_device::device::consumer::MultipleConsumerReport; use usbd_human_interface_device::device::keyboard::NKROBootKeyboardReport; +use usbd_human_interface_device::device::mouse::WheelMouseReport; /// State that contains the current battery level. `rumcake` may or may not use this static /// internally, depending on what MCU is being used. The contents of this state is usually set by a @@ -86,6 +87,8 @@ pub static CURRENT_OUTPUT_STATE: State> = State::new( &crate::usb::KB_CURRENT_OUTPUT_STATE_LISTENER, #[cfg(feature = "usb")] &crate::usb::CONSUMER_CURRENT_OUTPUT_STATE_LISTENER, + #[cfg(feature = "usb")] + &crate::usb::MOUSE_CURRENT_OUTPUT_STATE_LISTENER, #[cfg(all(feature = "usb", feature = "via"))] &crate::usb::VIA_CURRENT_OUTPUT_STATE_LISTENER, #[cfg(feature = "bluetooth")] @@ -276,6 +279,12 @@ pub trait HIDDevice { &CONSUMER_REPORT_HID_SEND_CHANNEL } + fn get_mouse_report_send_channel() -> &'static Channel { + static MOUSE_REPORT_HID_SEND_CHANNEL: Channel = + Channel::new(); + &MOUSE_REPORT_HID_SEND_CHANNEL + } + #[cfg(feature = "via")] fn get_via_hid_send_channel() -> &'static Channel { static VIA_REPORT_HID_SEND_CHANNEL: Channel = Channel::new(); diff --git a/rumcake/src/lib.rs b/rumcake/src/lib.rs index 86225f0..50fd47a 100644 --- a/rumcake/src/lib.rs +++ b/rumcake/src/lib.rs @@ -115,6 +115,8 @@ pub use once_cell; pub use rumcake_macros::keyboard_main as keyboard; pub mod keyboard; +pub mod pointer; + mod math; #[cfg(feature = "storage")] @@ -148,6 +150,7 @@ pub mod drivers; pub mod tasks { pub use crate::hw::__output_switcher; pub use crate::keyboard::{__layout_collect, __matrix_poll}; + pub use crate::pointer::__poll_pointing_device; #[cfg(all(feature = "lighting", feature = "storage"))] pub use crate::lighting::__lighting_storage_task; @@ -158,7 +161,10 @@ pub mod tasks { pub use crate::display::__display_task; #[cfg(feature = "usb")] - pub use crate::usb::{__start_usb, __usb_hid_consumer_write_task, __usb_hid_kb_write_task}; + pub use crate::usb::{ + __start_usb, __usb_hid_consumer_write_task, __usb_hid_kb_write_task, + __usb_hid_mouse_write_task, + }; #[cfg(all(feature = "via", feature = "usb"))] pub use crate::usb::__usb_hid_via_read_task; diff --git a/rumcake/src/pointer.rs b/rumcake/src/pointer.rs new file mode 100644 index 0000000..4199ec8 --- /dev/null +++ b/rumcake/src/pointer.rs @@ -0,0 +1,198 @@ +//! Mouse/pointer traits and tasks + +use defmt::warn; +use embassy_time::{Duration, Ticker}; +use num::Saturating; +use usbd_human_interface_device::device::mouse::WheelMouseReport; + +use crate::hw::{HIDDevice, CURRENT_OUTPUT_STATE}; + +use self::mouse::{MouseButtonFlags, MouseEvent}; + +pub trait PointingDevice {} + +pub trait PointingDriver { + /// Get events from a pointer device. The implementor is free to wait for an event for an + /// indefinite amount of time, so that the task can sleep. + async fn tick(&mut self) -> impl Iterator; +} + +#[rumcake_macros::task] +pub async fn poll_pointing_device( + _k: K, + mut driver: impl PointingDriver, +) { + let mut ticker = Ticker::every(Duration::from_millis(1)); + let mouse_report_channel = K::get_mouse_report_send_channel(); + let mut buttons = MouseButtonFlags::empty(); + + loop { + let events = driver.tick().await; + let mut x = 0; + let mut y = 0; + let mut vertical_wheel = 0; + let mut horizontal_wheel = 0; + + for e in events { + match e { + MouseEvent::Press(bits) => { + buttons |= bits; + } + MouseEvent::Release(bits) => { + buttons &= bits; + } + MouseEvent::Movement(new_x, new_y) => { + x = x.saturating_add(new_x); + y = y.saturating_add(new_y); + } + MouseEvent::Scroll(x_amount, y_amount) => { + horizontal_wheel = horizontal_wheel.saturating_add(x_amount); + vertical_wheel = vertical_wheel.saturating_add(y_amount); + } + } + } + + // Use send instead of try_send to avoid dropped inputs. If USB and Bluetooth are both not + // connected, this channel can become filled, so we discard the report in that case. + if CURRENT_OUTPUT_STATE.get().await.is_some() { + mouse_report_channel + .send(WheelMouseReport { + buttons: buttons.bits(), + x, + y, + vertical_wheel, + horizontal_wheel, + }) + .await; + } else { + warn!("[POINTER] Discarding report"); + } + + ticker.next().await; + } +} + +pub mod mouse { + // TODO: move this logic into its own crate? + use bitflags::bitflags; + + bitflags! { + #[derive(Clone, Copy, PartialEq, Eq)] + pub struct MouseButtonFlags: u8 { + const LEFT = 0b00000001; + const RIGHT = 0b00000010; + const MIDDLE = 0b00000100; + const BACK = 0b00001000; + const FORWARD = 0b00010000; + const BUTTON6 = 0b00100000; + const BUTTON7 = 0b01000000; + const BUTTON8 = 0b10000000; + } + } + + #[derive(Clone, Copy)] + pub enum MouseEvent { + Press(MouseButtonFlags), + Release(MouseButtonFlags), + Movement(i8, i8), + Scroll(i8, i8), + } +} + +pub mod touchpad { + // TODO: move this logic into its own crate? + + use heapless::Vec; + + use super::mouse::{MouseButtonFlags, MouseEvent}; + + pub enum TouchpadEvent { + /// Tap of a button on a touchpad. + Tap(MouseButtonFlags), + + /// Holding a button. This should be registered continuously, for as long as the button is + /// held. + Hold(MouseButtonFlags), + + /// Touchpad movement. + Movement(i8, i8), + + /// Scrolling movement on a touchpad, which supports both vertical and horizontal + /// scrolling. + Scroll(i8, i8), + } + + pub struct Touchpad { + events: Vec, + release_on_next_tick: MouseButtonFlags, + holding: MouseButtonFlags, + hold_registered: bool, + } + + impl Default for Touchpad { + fn default() -> Self { + Self::new() + } + } + + impl Touchpad { + pub fn new() -> Self { + Self { + events: Vec::new(), + release_on_next_tick: MouseButtonFlags::empty(), + holding: MouseButtonFlags::empty(), + hold_registered: false, + } + } + + /// Call this at a regular interval + pub fn tick(&mut self) { + // Release held buttons if a hold wasn't registered last tick. + if !self.hold_registered { + let _ = self.events.push(MouseEvent::Release(self.holding)).is_ok(); + } + + // Clear existing events + self.events.clear(); + self.hold_registered = false; + + if !self.release_on_next_tick.is_empty() + && self + .events + .push(MouseEvent::Release(self.release_on_next_tick)) + .is_ok() + { + self.release_on_next_tick = MouseButtonFlags::empty(); + } + } + + pub fn register(&mut self, event: TouchpadEvent) { + match event { + TouchpadEvent::Tap(buttons) => { + self.release_on_next_tick = buttons; + let _ = self.events.push(MouseEvent::Press(buttons)); + } + TouchpadEvent::Hold(buttons) => { + self.hold_registered = true; + + if buttons != self.holding + && self.events.push(MouseEvent::Release(self.holding)).is_ok() + && self.events.push(MouseEvent::Press(buttons)).is_ok() + { + self.holding = buttons; + } + } + TouchpadEvent::Movement(new_x, new_y) => { + let _ = self.events.push(MouseEvent::Movement(new_x, new_y)); + } + TouchpadEvent::Scroll(new_x, new_y) => { + let _ = self.events.push(MouseEvent::Scroll(new_x, new_y)); + } + } + } + + pub fn events(&self) -> impl Iterator + '_ { + self.events.iter().copied() + } + } +} diff --git a/rumcake/src/usb.rs b/rumcake/src/usb.rs index 6726cd6..9936aef 100644 --- a/rumcake/src/usb.rs +++ b/rumcake/src/usb.rs @@ -21,6 +21,7 @@ use usbd_human_interface_device::device::consumer::{ use usbd_human_interface_device::device::keyboard::{ NKROBootKeyboardReport, NKRO_BOOT_KEYBOARD_REPORT_DESCRIPTOR, }; +use usbd_human_interface_device::device::mouse::{WheelMouseReport, WHEEL_MOUSE_REPORT_DESCRIPTOR}; use crate::hw::platform::RawMutex; use crate::hw::{HIDDevice, HIDOutput, CURRENT_OUTPUT_STATE}; @@ -91,6 +92,32 @@ pub fn setup_usb_hid_consumer_writer( ) } +/// Configure the HID report writer, for consumer commands. +/// +/// The HID writer produced should be passed to [`usb_hid_mouse_write_task`]. +pub fn setup_usb_hid_mouse_writer( + b: &mut Builder<'static, impl Driver<'static>>, +) -> HidWriter< + 'static, + impl Driver<'static>, + { <::ByteArray as StaticArray>::LEN }, +> { + // Keyboard HID setup + static MOUSE_STATE: StaticCell = StaticCell::new(); + let mouse_state = MOUSE_STATE.init(UsbState::new()); + let mouse_hid_config = Config { + request_handler: None, + report_descriptor: WHEEL_MOUSE_REPORT_DESCRIPTOR, + poll_ms: 1, + max_packet_size: 64, + }; + HidWriter::<_, { <::ByteArray as StaticArray>::LEN }>::new( + b, + mouse_state, + mouse_hid_config, + ) +} + #[rumcake_macros::task] pub async fn start_usb(mut usb: UsbDevice<'static, impl Driver<'static>>) { loop { @@ -170,6 +197,28 @@ pub async fn usb_hid_consumer_write_task( ); } +pub(crate) static MOUSE_CURRENT_OUTPUT_STATE_LISTENER: Signal = Signal::new(); + +#[rumcake_macros::task] +pub async fn usb_hid_mouse_write_task( + _k: K, + mut hid: HidWriter< + 'static, + impl Driver<'static>, + { <::ByteArray as StaticArray>::LEN }, + >, +) { + let channel = K::get_mouse_report_send_channel(); + + usb_task_inner!( + hid, + CONSUMER_CURRENT_OUTPUT_STATE_LISTENER, + channel, + "[USB] Writing mouse HID report to USB: {:?}", + "[USB] Couldn't write mouse HID report: {:?}" + ); +} + #[cfg(feature = "via")] pub struct ViaCommandHandler { _phantom: PhantomData, From a200ff0f4927f5ed86b75b3c0470faae145e07bc Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:12:07 -0400 Subject: [PATCH 2/8] add some doc comments for the iqs5xx driver --- rumcake/src/drivers/iqs5xx.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rumcake/src/drivers/iqs5xx.rs b/rumcake/src/drivers/iqs5xx.rs index 7c9837a..5e8c2b2 100644 --- a/rumcake/src/drivers/iqs5xx.rs +++ b/rumcake/src/drivers/iqs5xx.rs @@ -1,3 +1,11 @@ +//! Rumcaker driver implementations for [rwalkr's IQS5xx driver](`iqs5xx`) +//! +//! This provides implementations for [`PointingDriver`](`crate::pointer::PointingDriver`). +//! +//! To use this driver as a pointing device, you must implement [`IQS5xxPointerDriver`], and pass +//! it to [`setup_driver`]. Then the result of this can be passed to the [`poll_pointing_device`] +//! task. + use defmt::Debug2Format; use embassy_time::Delay; use embedded_hal::blocking::i2c::{Write, WriteRead}; @@ -33,7 +41,7 @@ pub fn setup_driver( } trait IQS5xxPointerDriver { - /// This function gets called a regular interval (usually every millisecond). You can + /// This function gets called at a regular interval (usually every millisecond). You can /// re-implement this if you the type you're implementing this trait on needs to update its /// state over time. This can be useful if you want to implement more complicated touchpad /// functionality which isn't already supported by the [`Touchpad`] struct. From 65143478baf2adb7656d79de2777c1bdb5e96301 Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:19:08 -0400 Subject: [PATCH 3/8] make iqs5xx driver trait pub --- rumcake/src/drivers/iqs5xx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rumcake/src/drivers/iqs5xx.rs b/rumcake/src/drivers/iqs5xx.rs index 5e8c2b2..6432fa8 100644 --- a/rumcake/src/drivers/iqs5xx.rs +++ b/rumcake/src/drivers/iqs5xx.rs @@ -40,7 +40,7 @@ pub fn setup_driver( iqs } -trait IQS5xxPointerDriver { +pub trait IQS5xxPointerDriver { /// This function gets called at a regular interval (usually every millisecond). You can /// re-implement this if you the type you're implementing this trait on needs to update its /// state over time. This can be useful if you want to implement more complicated touchpad From bb0411d135f15ba9e2054cdced6f59a36573901c Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:24:26 -0400 Subject: [PATCH 4/8] rename trait to be consistent with pointer module --- rumcake/src/drivers/iqs5xx.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rumcake/src/drivers/iqs5xx.rs b/rumcake/src/drivers/iqs5xx.rs index 6432fa8..fe92be0 100644 --- a/rumcake/src/drivers/iqs5xx.rs +++ b/rumcake/src/drivers/iqs5xx.rs @@ -40,7 +40,7 @@ pub fn setup_driver( iqs } -pub trait IQS5xxPointerDriver { +pub trait IQS5xxPointingDriver { /// This function gets called at a regular interval (usually every millisecond). You can /// re-implement this if you the type you're implementing this trait on needs to update its /// state over time. This can be useful if you want to implement more complicated touchpad @@ -73,7 +73,7 @@ pub trait IQS5xxPointerDriver { } } -impl IQS5xx +impl IQS5xx where RDY: InputPin, RST: OutputPin, @@ -103,7 +103,7 @@ where } } -impl PointingDriver +impl PointingDriver for IQS5xx where RDY: InputPin, From 1dfe41d1a994ebb17ed5c631661255c89bd6f7ef Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:39:13 -0400 Subject: [PATCH 5/8] make iqs5xx driver pub --- rumcake/src/drivers/iqs5xx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rumcake/src/drivers/iqs5xx.rs b/rumcake/src/drivers/iqs5xx.rs index fe92be0..daa433e 100644 --- a/rumcake/src/drivers/iqs5xx.rs +++ b/rumcake/src/drivers/iqs5xx.rs @@ -17,7 +17,7 @@ use crate::pointer::mouse::{MouseButtonFlags, MouseEvent}; use crate::pointer::touchpad::{Touchpad, TouchpadEvent}; use crate::pointer::PointingDriver; -struct IQS5xx { +pub struct IQS5xx { driver: IQS5xxDriver, touchpad_state: Touchpad, event_handler: E, From 3749adc65bd596ee4580031d9aa488d69e03cf8b Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:49:44 -0400 Subject: [PATCH 6/8] make it possible to create blocking i2c implementation for the stm32 `setup_i2c` macro --- rumcake-macros/src/hw/stm32.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/rumcake-macros/src/hw/stm32.rs b/rumcake-macros/src/hw/stm32.rs index 835b8d2..0796102 100644 --- a/rumcake-macros/src/hw/stm32.rs +++ b/rumcake-macros/src/hw/stm32.rs @@ -46,8 +46,8 @@ crate::parse_as_custom_fields! { i2c: Ident, scl: Ident, sda: Ident, - rx_dma: Ident, - tx_dma: Ident + rx_dma: OptionalItem, + tx_dma: OptionalItem } } @@ -73,6 +73,26 @@ pub fn setup_i2c( } }; + let rx_dma = if let OptionalItem::Some(rx_dma) = rx_dma { + quote! { + ::rumcake::hw::platform::embassy_stm32::peripherals::#rx_dma::steal() + } + } else { + quote! { + ::rumcake::hw::platform::embassy_stm32::dma::NoDma + } + }; + + let tx_dma = if let OptionalItem::Some(tx_dma) = tx_dma { + quote! { + ::rumcake::hw::platform::embassy_stm32::peripherals::#tx_dma::steal() + } + } else { + quote! { + ::rumcake::hw::platform::embassy_stm32::dma::NoDma + } + }; + quote! { unsafe { ::rumcake::hw::platform::embassy_stm32::bind_interrupts! { @@ -83,8 +103,8 @@ pub fn setup_i2c( let i2c = ::rumcake::hw::platform::embassy_stm32::peripherals::#i2c::steal(); let scl = ::rumcake::hw::platform::embassy_stm32::peripherals::#scl::steal(); let sda = ::rumcake::hw::platform::embassy_stm32::peripherals::#sda::steal(); - let rx_dma = ::rumcake::hw::platform::embassy_stm32::peripherals::#rx_dma::steal(); - let tx_dma = ::rumcake::hw::platform::embassy_stm32::peripherals::#tx_dma::steal(); + let rx_dma = #rx_dma; + let tx_dma = #tx_dma; let time = ::rumcake::hw::platform::embassy_stm32::time::Hertz(100_000); ::rumcake::hw::platform::embassy_stm32::i2c::I2c::new(i2c, scl, sda, Irqs, tx_dma, rx_dma, time, Default::default()) } From d47f0080f9b58c8d6031835a45f70d4d8504fd80 Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:00:28 -0400 Subject: [PATCH 7/8] missing complement --- rumcake/src/pointer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rumcake/src/pointer.rs b/rumcake/src/pointer.rs index 4199ec8..531af41 100644 --- a/rumcake/src/pointer.rs +++ b/rumcake/src/pointer.rs @@ -39,7 +39,7 @@ pub async fn poll_pointing_device( buttons |= bits; } MouseEvent::Release(bits) => { - buttons &= bits; + buttons &= bits.complement(); } MouseEvent::Movement(new_x, new_y) => { x = x.saturating_add(new_x); From b6707dee52579d526b360b2c9f48f043b4c94e59 Mon Sep 17 00:00:00 2001 From: Univa <41708691+Univa@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:56:32 -0400 Subject: [PATCH 8/8] support for mouse events in split keyboard setups also does some slight api changes: - CentralDevice::Layout is now optional - MATRIX_EVENTS get published events from the matrix polling task and central task, instead of peripheral task and layout collect task - split setup function for iqs5xx into two different types: one for default behavior, and another for custom implementor of IQS5xxEventHandler - mouse tasks are now split into two: polling task and mouse event collection task (similar to matrix polling and layout collect) - PeripheralDevice::get_matrix_events_channel changed to return a channel of MessageToCentral instead --- rumcake-macros/src/keyboard.rs | 10 ++- rumcake/src/drivers/iqs5xx.rs | 32 ++++++- rumcake/src/keyboard.rs | 7 +- rumcake/src/lib.rs | 2 +- rumcake/src/pointer.rs | 147 ++++++++++++++++++++++++++------ rumcake/src/split/central.rs | 28 +++++- rumcake/src/split/mod.rs | 36 ++++++++ rumcake/src/split/peripheral.rs | 29 +++---- 8 files changed, 234 insertions(+), 57 deletions(-) diff --git a/rumcake-macros/src/keyboard.rs b/rumcake-macros/src/keyboard.rs index 2a74be1..3a87e09 100644 --- a/rumcake-macros/src/keyboard.rs +++ b/rumcake-macros/src/keyboard.rs @@ -333,6 +333,12 @@ pub(crate) fn keyboard_main( spawning.extend(quote! { spawner.spawn(::rumcake::layout_collect!(#kb_name)).unwrap(); }); + + if keyboard.pointer.is_some() || keyboard.split_central.is_some() { + spawning.extend(quote! { + spawner.spawn(::rumcake::collect_mouse_events!(#kb_name)).unwrap(); + }) + } } spawning.extend(quote! { @@ -380,13 +386,11 @@ pub(crate) fn keyboard_main( } } - if keyboard.usb && keyboard.pointer.is_some() { + if keyboard.usb && (keyboard.pointer.is_some() || keyboard.split_central.is_some()) { initialization.extend(quote! { - // HID consumer let pointer_class = ::rumcake::usb::setup_usb_hid_mouse_writer(&mut builder); }); spawning.extend(quote! { - // HID Consumer Report sending spawner.spawn(::rumcake::usb_hid_mouse_write_task!(#kb_name, pointer_class)).unwrap(); }); } diff --git a/rumcake/src/drivers/iqs5xx.rs b/rumcake/src/drivers/iqs5xx.rs index daa433e..583f3d6 100644 --- a/rumcake/src/drivers/iqs5xx.rs +++ b/rumcake/src/drivers/iqs5xx.rs @@ -23,7 +23,31 @@ pub struct IQS5xx { event_handler: E, } -pub fn setup_driver( +pub struct DefaultBehavior; +impl IQS5xxEventHandler for DefaultBehavior {} + +pub fn setup_driver( + i2c: I2C, + rdy: RDY, + rst: RST, +) -> IQS5xx { + let mut iqs = IQS5xx { + driver: IQS5xxDriver::new(i2c, 0, rdy, rst), + event_handler: DefaultBehavior, + touchpad_state: Touchpad::new(), + }; + iqs.driver.reset(&mut Delay).unwrap(); + iqs.driver.poll_ready(&mut Delay).unwrap(); + iqs.driver.init().unwrap(); + iqs +} + +pub fn setup_driver_with_custom_behavior< + E, + I2C: Write + WriteRead, + RDY: InputPin, + RST: OutputPin, +>( i2c: I2C, rdy: RDY, rst: RST, @@ -40,7 +64,7 @@ pub fn setup_driver( iqs } -pub trait IQS5xxPointingDriver { +pub trait IQS5xxEventHandler { /// This function gets called at a regular interval (usually every millisecond). You can /// re-implement this if you the type you're implementing this trait on needs to update its /// state over time. This can be useful if you want to implement more complicated touchpad @@ -73,7 +97,7 @@ pub trait IQS5xxPointingDriver { } } -impl IQS5xx +impl IQS5xx where RDY: InputPin, RST: OutputPin, @@ -103,7 +127,7 @@ where } } -impl PointingDriver +impl PointingDriver for IQS5xx where RDY: InputPin, diff --git a/rumcake/src/keyboard.rs b/rumcake/src/keyboard.rs index 7cd9648..5103457 100644 --- a/rumcake/src/keyboard.rs +++ b/rumcake/src/keyboard.rs @@ -480,7 +480,7 @@ pub async fn matrix_poll(_k: K) { let layout_channel = ::get_matrix_events_channel(); #[cfg(feature = "split-peripheral")] - let peripheral_channel = ::get_matrix_events_channel(); + let peripheral_channel = ::get_message_to_central_channel(); loop { { @@ -502,13 +502,15 @@ pub async fn matrix_poll(_k: K) { Debug2Format(&remapped_event) ); + MATRIX_EVENTS.publish_immediate(remapped_event); + if let Some(layout_channel) = layout_channel { layout_channel.send(remapped_event).await }; #[cfg(feature = "split-peripheral")] if let Some(peripheral_channel) = peripheral_channel { - peripheral_channel.send(remapped_event).await + peripheral_channel.send(remapped_event.into()).await }; } } @@ -552,7 +554,6 @@ where if let Ok(event) = matrix_channel.try_receive() { layout.event(event); - MATRIX_EVENTS.publish_immediate(event); // Just immediately publish since we don't want to hold up any key events to be converted into keycodes. }; let tick = layout.tick(); diff --git a/rumcake/src/lib.rs b/rumcake/src/lib.rs index 27d4ae0..8f21dc3 100644 --- a/rumcake/src/lib.rs +++ b/rumcake/src/lib.rs @@ -150,7 +150,7 @@ pub mod drivers; pub mod tasks { pub use crate::hw::__output_switcher; pub use crate::keyboard::{__ec11_encoders_poll, __layout_collect, __matrix_poll}; - pub use crate::pointer::__poll_pointing_device; + pub use crate::pointer::{__collect_mouse_events, __poll_pointing_device}; #[cfg(all(feature = "lighting", feature = "storage"))] pub use crate::lighting::__lighting_storage_task; diff --git a/rumcake/src/pointer.rs b/rumcake/src/pointer.rs index 531af41..2444a85 100644 --- a/rumcake/src/pointer.rs +++ b/rumcake/src/pointer.rs @@ -1,15 +1,28 @@ //! Mouse/pointer traits and tasks use defmt::warn; +use embassy_sync::channel::Channel; use embassy_time::{Duration, Ticker}; use num::Saturating; use usbd_human_interface_device::device::mouse::WheelMouseReport; +use crate::hw::platform::RawMutex; use crate::hw::{HIDDevice, CURRENT_OUTPUT_STATE}; use self::mouse::{MouseButtonFlags, MouseEvent}; -pub trait PointingDevice {} +pub trait PointingDevice { + type MouseEventCollector: private::MaybeMouseEventCollector = private::EmptyMouseEventCollector; + + #[cfg(feature = "split-peripheral")] + type PeripheralDeviceType: crate::split::peripheral::private::MaybePeripheralDevice = + crate::split::peripheral::private::EmptyPeripheralDevice; + + const FLIP_X_MOVEMENT: bool = false; + const FLIP_Y_MOVEMENT: bool = false; + const FLIP_X_SCROLL: bool = false; + const FLIP_Y_SCROLL: bool = false; +} pub trait PointingDriver { /// Get events from a pointer device. The implementor is free to wait for an event for an @@ -18,37 +31,115 @@ pub trait PointingDriver { } #[rumcake_macros::task] -pub async fn poll_pointing_device( - _k: K, - mut driver: impl PointingDriver, -) { +pub async fn poll_pointing_device(_k: K, mut driver: impl PointingDriver) { let mut ticker = Ticker::every(Duration::from_millis(1)); + let layout_channel = + ::get_mouse_events_channel(); + + #[cfg(feature = "split-peripheral")] + let peripheral_channel = ::get_message_to_central_channel(); + + loop { + let events = driver.tick().await; + + for mut e in events { + if K::FLIP_X_MOVEMENT { + if let MouseEvent::Movement(x, _) = &mut e { + *x *= -1; + } + } + + if K::FLIP_Y_MOVEMENT { + if let MouseEvent::Movement(_, y) = &mut e { + *y *= -1; + } + } + + if K::FLIP_X_SCROLL { + if let MouseEvent::Scroll(x, _) = &mut e { + *x *= -1; + } + } + + if K::FLIP_Y_SCROLL { + if let MouseEvent::Scroll(_, y) = &mut e { + *y *= -1; + } + } + + if let Some(layout_channel) = layout_channel { + layout_channel.send(e).await + } + + #[cfg(feature = "split-peripheral")] + if let Some(peripheral_channel) = peripheral_channel { + peripheral_channel.send(e.into()).await + } + } + + ticker.next().await; + } +} + +pub trait MouseEventCollector { + fn get_mouse_events_channel() -> &'static Channel { + static POLLED_EVENTS_CHANNEL: Channel = Channel::new(); + + &POLLED_EVENTS_CHANNEL + } +} + +pub(crate) mod private { + use embassy_sync::channel::Channel; + + use crate::hw::platform::RawMutex; + + use super::mouse::MouseEvent; + use super::MouseEventCollector; + + pub struct EmptyMouseEventCollector; + impl MaybeMouseEventCollector for EmptyMouseEventCollector {} + + pub trait MaybeMouseEventCollector { + fn get_mouse_events_channel() -> Option<&'static Channel> { + None + } + } + + impl MaybeMouseEventCollector for T { + fn get_mouse_events_channel() -> Option<&'static Channel> { + Some(T::get_mouse_events_channel()) + } + } +} + +#[rumcake_macros::task] +pub async fn collect_mouse_events(_k: K) { let mouse_report_channel = K::get_mouse_report_send_channel(); let mut buttons = MouseButtonFlags::empty(); + let channel = K::get_mouse_events_channel(); loop { - let events = driver.tick().await; + let event = channel.receive().await; let mut x = 0; let mut y = 0; let mut vertical_wheel = 0; let mut horizontal_wheel = 0; - for e in events { - match e { - MouseEvent::Press(bits) => { - buttons |= bits; - } - MouseEvent::Release(bits) => { - buttons &= bits.complement(); - } - MouseEvent::Movement(new_x, new_y) => { - x = x.saturating_add(new_x); - y = y.saturating_add(new_y); - } - MouseEvent::Scroll(x_amount, y_amount) => { - horizontal_wheel = horizontal_wheel.saturating_add(x_amount); - vertical_wheel = vertical_wheel.saturating_add(y_amount); - } + match event { + MouseEvent::Press(bits) => { + buttons |= bits; + } + MouseEvent::Release(bits) => { + buttons &= bits.complement(); + } + MouseEvent::Movement(new_x, new_y) => { + x = x.saturating_add(new_x); + y = y.saturating_add(new_y); + } + MouseEvent::Scroll(x_amount, y_amount) => { + horizontal_wheel = horizontal_wheel.saturating_add(x_amount); + vertical_wheel = vertical_wheel.saturating_add(y_amount); } } @@ -67,18 +158,22 @@ pub async fn poll_pointing_device( } else { warn!("[POINTER] Discarding report"); } - - ticker.next().await; } } pub mod mouse { // TODO: move this logic into its own crate? use bitflags::bitflags; + use postcard::experimental::max_size::MaxSize; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, MaxSize)] + #[serde(transparent)] + #[repr(transparent)] + pub struct MouseButtonFlags(u8); bitflags! { - #[derive(Clone, Copy, PartialEq, Eq)] - pub struct MouseButtonFlags: u8 { + impl MouseButtonFlags: u8 { const LEFT = 0b00000001; const RIGHT = 0b00000010; const MIDDLE = 0b00000100; diff --git a/rumcake/src/split/central.rs b/rumcake/src/split/central.rs index 07c6c01..fddcccc 100644 --- a/rumcake/src/split/central.rs +++ b/rumcake/src/split/central.rs @@ -12,16 +12,22 @@ use core::fmt::Debug; use defmt::{error, Debug2Format}; use embassy_futures::select::{select, Either}; use embassy_sync::channel::Channel; +use embassy_sync::pubsub::PubSubBehavior; use embedded_io_async::ReadExactError; use postcard::Error; use super::{MessageToCentral, MessageToPeripheral}; use crate::hw::platform::RawMutex; -use crate::keyboard::KeyboardLayout; +use crate::keyboard::MATRIX_EVENTS; pub trait CentralDevice { /// The layout to send matrix events (which were received by peripherals) to. - type Layout: KeyboardLayout; + type Layout: crate::keyboard::private::MaybeKeyboardLayout = + crate::keyboard::private::EmptyKeyboardLayout; + + /// Collector to send mouse events (which were received by peripherals) to. + type MouseEventCollector: crate::pointer::private::MaybeMouseEventCollector = + crate::pointer::private::EmptyMouseEventCollector; /// Get a reference to a channel that can receive messages from other tasks to be sent to /// peripherals. @@ -105,7 +111,10 @@ impl From> for CentralDeviceError { #[rumcake_macros::task] pub async fn central_task(_k: K, mut driver: impl CentralDeviceDriver) { let message_to_peripherals_channel = K::get_message_to_peripheral_channel(); - let matrix_events_channel = K::Layout::get_matrix_events_channel(); + let matrix_events_channel = + ::get_matrix_events_channel(); + let mouse_events_channel = + ::get_mouse_events_channel(); loop { match select( @@ -117,7 +126,18 @@ pub async fn central_task(_k: K, mut driver: impl CentralDevic Either::First(message) => match message { Ok(event) => match event { MessageToCentral::KeyPress(_, _) | MessageToCentral::KeyRelease(_, _) => { - matrix_events_channel.send(event.try_into().unwrap()).await; + MATRIX_EVENTS.publish_immediate(event.try_into().unwrap()); + if let Some(matrix_events_channel) = matrix_events_channel { + matrix_events_channel.send(event.try_into().unwrap()).await; + } + } + MessageToCentral::MouseMovement(_, _) + | MessageToCentral::MousePress(_) + | MessageToCentral::MouseRelease(_) + | MessageToCentral::MouseScroll(_, _) => { + if let Some(mouse_events_channel) = mouse_events_channel { + mouse_events_channel.send(event.try_into().unwrap()).await; + } } }, Err(err) => { diff --git a/rumcake/src/split/mod.rs b/rumcake/src/split/mod.rs index bb2fac5..c4f730b 100644 --- a/rumcake/src/split/mod.rs +++ b/rumcake/src/split/mod.rs @@ -4,6 +4,8 @@ use keyberon::layout::Event; use postcard::experimental::max_size::MaxSize; use serde::{Deserialize, Serialize}; +use crate::pointer::mouse::{MouseButtonFlags, MouseEvent}; + #[cfg(feature = "split-central")] pub mod central; @@ -18,6 +20,14 @@ pub enum MessageToCentral { KeyPress(u8, u8), /// Key release in the form of (row, col). KeyRelease(u8, u8), + /// Mouse movement in the direction of (x, y). + MouseMovement(i8, i8), + /// Mouse buttons that have been pressed in the current tick. + MousePress(MouseButtonFlags), + /// Mouse buttons that have been released in the current tick. + MouseRelease(MouseButtonFlags), + /// Scrolling in the direction of (x, y). + MouseScroll(i8, i8), } /// Size of buffer used when sending messages to a central device @@ -39,6 +49,32 @@ impl TryFrom for Event { match message { MessageToCentral::KeyPress(row, col) => Ok(Event::Press(row, col)), MessageToCentral::KeyRelease(row, col) => Ok(Event::Release(row, col)), + _ => Err(()), + } + } +} + +impl From for MessageToCentral { + fn from(value: MouseEvent) -> Self { + match value { + MouseEvent::Press(buttons) => MessageToCentral::MousePress(buttons), + MouseEvent::Release(buttons) => MessageToCentral::MouseRelease(buttons), + MouseEvent::Movement(x, y) => MessageToCentral::MouseMovement(x, y), + MouseEvent::Scroll(x, y) => MessageToCentral::MouseScroll(x, y), + } + } +} + +impl TryFrom for MouseEvent { + type Error = (); + + fn try_from(value: MessageToCentral) -> Result { + match value { + MessageToCentral::MouseMovement(x, y) => Ok(MouseEvent::Movement(x, y)), + MessageToCentral::MousePress(buttons) => Ok(MouseEvent::Press(buttons)), + MessageToCentral::MouseRelease(buttons) => Ok(MouseEvent::Release(buttons)), + MessageToCentral::MouseScroll(x, y) => Ok(MouseEvent::Scroll(x, y)), + _ => Err(()), } } } diff --git a/rumcake/src/split/peripheral.rs b/rumcake/src/split/peripheral.rs index 21e18ec..2929a0c 100644 --- a/rumcake/src/split/peripheral.rs +++ b/rumcake/src/split/peripheral.rs @@ -11,21 +11,18 @@ use core::fmt::Debug; use defmt::{error, Debug2Format}; use embassy_futures::select::{select, Either}; use embassy_sync::channel::Channel; -use embassy_sync::pubsub::PubSubBehavior; use embedded_io_async::ReadExactError; -use keyberon::layout::Event; use postcard::Error; use super::{MessageToCentral, MessageToPeripheral}; use crate::hw::platform::RawMutex; -use crate::keyboard::MATRIX_EVENTS; // Trait that devices must implement to serve as a peripheral in a split keyboard setup. pub trait PeripheralDevice { - /// Get a reference to a channel that can receive matrix events from other tasks to be - /// processed into keycodes. - fn get_matrix_events_channel() -> &'static Channel { - static POLLED_EVENTS_CHANNEL: Channel = Channel::new(); + /// Get a reference to a channel that can receive message from other tasks to be sent to the + /// central device. + fn get_message_to_central_channel() -> &'static Channel { + static POLLED_EVENTS_CHANNEL: Channel = Channel::new(); &POLLED_EVENTS_CHANNEL } @@ -47,9 +44,9 @@ pub trait PeripheralDevice { pub(crate) mod private { use embassy_sync::channel::Channel; - use keyberon::layout::Event; use crate::hw::platform::RawMutex; + use crate::split::MessageToCentral; use super::PeripheralDevice; @@ -57,14 +54,16 @@ pub(crate) mod private { impl MaybePeripheralDevice for EmptyPeripheralDevice {} pub trait MaybePeripheralDevice { - fn get_matrix_events_channel() -> Option<&'static Channel> { + fn get_message_to_central_channel( + ) -> Option<&'static Channel> { None } } impl MaybePeripheralDevice for T { - fn get_matrix_events_channel() -> Option<&'static Channel> { - Some(T::get_matrix_events_channel()) + fn get_message_to_central_channel( + ) -> Option<&'static Channel> { + Some(T::get_message_to_central_channel()) } } } @@ -114,7 +113,7 @@ impl From> for PeripheralDeviceError { // This task replaces the `layout_collect` task, which is usually used on non-split keyboards for sending events to the keyboard layout #[rumcake_macros::task] pub async fn peripheral_task(_k: K, mut driver: impl PeripheralDeviceDriver) { - let channel = K::get_matrix_events_channel(); + let channel = K::get_message_to_central_channel(); loop { match select( @@ -160,11 +159,9 @@ pub async fn peripheral_task(_k: K, mut driver: impl Periph } }, Either::Second(event) => { - MATRIX_EVENTS.publish_immediate(event); - - if let Err(err) = driver.send_message_to_central(event.into()).await { + if let Err(err) = driver.send_message_to_central(event).await { error!( - "[SPLIT_PERIPHERAL] Error sending matrix events to central: {}", + "[SPLIT_PERIPHERAL] Error sending message to central: {}", Debug2Format(&err) ) };