diff --git a/docs/src/content/docs/features/feature-encoders.md b/docs/src/content/docs/features/feature-encoders.md new file mode 100644 index 0000000..62034f1 --- /dev/null +++ b/docs/src/content/docs/features/feature-encoders.md @@ -0,0 +1,95 @@ +--- +title: Encoders +description: How to add EC11-compatible encoders to your device. +--- + +:::caution +This feature is still a work in progress. For a list of features that still need +to be implemented, check the [to-do list](#to-do-list). +::: + +This document contains information about how to add EC11-compatible encoders to your device. + +# Setup + +## Required code + +To set up your keyboard for use with encoders, you must add `encoders` to your `#[keyboard]` macro invocation, +and your keyboard must implement the `DeviceWithEncoders` trait. + +This can be done easily by using the `setup_encoders!` macro: + +```rust ins={5,9-31} +use rumcake::keyboard; + +#[keyboard( + // somewhere in your keyboard macro invocation ... + encoders +)] +struct MyKeyboard; + +use rumcake::keyboard::DeviceWithEncoders; +impl DeviceWithEncoders for MyKeyboard { + type Layout = Self; + + setup_encoders! { + Encoder { + sw_pin: input_pin!(PB12, EXTI12), + sw_pos: (0, 0), + output_a_pin: input_pin!(PB2, EXTI2), + output_b_pin: input_pin!(PB1), + cw_pos: (0, 1), + ccw_pos: (0, 2), + }, + Encoder { + sw_pin: input_pin!(PA11, EXTI11), + sw_pos: (1, 0), + output_a_pin: input_pin!(PA3, EXTI3), + output_b_pin: input_pin!(PA1), + cw_pos: (1, 1), + ccw_pos: (1, 2), + }, + }; +} + +use rumcake::keyboard::{build_layout, KeyboardLayout}; +impl KeyboardLayout for MyKeyboard { + build_layout! { + { + [ A B C ] + [ D E F ] + } + { + [ G H I ] + [ J K L ] + } + } +} +``` + +The `sw_pin` corresponds to the pin connected to the encoder's push button. `output_a_pin` and `output_b_pin` +correspond to the pins that pulse as the encoder rotates. + +:::note +The current implementation of encoders relies on interrupts to avoid polling the encoders constantly. + +For STM32, this means you need to specify the EXTI channels for `sw_pin` and `output_a_pin`. This can +be done by adding an extra argument to the `input_pin!` macro, as shown in the example above. This can +be omitted for other platforms. +::: + +Encoders work by mapping their outputs to a position on your layout. +`type Layout = Self` tells rumcake to redirect encoder events to the implemented `KeyboardLayout` for `MyKeyboard`. + +In the example above, here are the following mappings: + +- Encoder 1 Button: `A` key (or `G` on the second layer) +- Encoder 1 Clockwise rotation: `B` key (or `H` on the second layer) +- Encoder 1 Counter-clockwise rotation: `C` key (or `I` on the second layer) +- Encoder 2 Button: `D` key (or `J` on the second layer) +- Encoder 2 Clockwise rotation: `H` key (or `K` on the second layer) +- Encoder 2 Counter-clockwise rotation: `I` key (or `L` on the second layer) + +# To-do List + +- [ ] Via(l) support diff --git a/rumcake-macros/src/hw/stm32.rs b/rumcake-macros/src/hw/stm32.rs index 835b8d2..48a0b31 100644 --- a/rumcake-macros/src/hw/stm32.rs +++ b/rumcake-macros/src/hw/stm32.rs @@ -12,15 +12,35 @@ use crate::common::{ pub const HAL_CRATE: &str = "embassy_stm32"; -pub fn input_pin(ident: Ident) -> TokenStream { - quote! { - unsafe { - ::rumcake::hw::platform::embassy_stm32::gpio::Input::new( - ::rumcake::hw::platform::embassy_stm32::gpio::Pin::degrade( +pub fn input_pin(args: Punctuated) -> TokenStream { + let mut args = args.iter(); + let ident = args.next().expect_or_abort("missing pin identifier"); + let exti_channel = args.next(); + + if let Some(arg) = args.next() { + abort!(arg, "unexpected extra args provided"); + } + + if let Some(exti) = exti_channel { + quote! { + unsafe { + ::rumcake::hw::platform::embassy_stm32::exti::ExtiInput::new( ::rumcake::hw::platform::embassy_stm32::peripherals::#ident::steal(), - ), - ::rumcake::hw::platform::embassy_stm32::gpio::Pull::Up, - ) + ::rumcake::hw::platform::embassy_stm32::peripherals::#exti::steal(), + ::rumcake::hw::platform::embassy_stm32::gpio::Pull::Up, + ) + } + } + } else { + quote! { + unsafe { + ::rumcake::hw::platform::embassy_stm32::gpio::Input::new( + ::rumcake::hw::platform::embassy_stm32::gpio::Pin::degrade( + ::rumcake::hw::platform::embassy_stm32::peripherals::#ident::steal(), + ), + ::rumcake::hw::platform::embassy_stm32::gpio::Pull::Up, + ) + } } } } diff --git a/rumcake-macros/src/keyboard.rs b/rumcake-macros/src/keyboard.rs index 2b93e4d..4a87c06 100644 --- a/rumcake-macros/src/keyboard.rs +++ b/rumcake-macros/src/keyboard.rs @@ -3,7 +3,11 @@ use darling::FromMeta; use proc_macro2::{Ident, TokenStream, TokenTree}; use proc_macro_error::{abort, emit_error, OptionExt}; use quote::quote; -use syn::{ExprRange, ItemStruct, LitInt, LitStr, PathSegment}; +use syn::parse::Parse; +use syn::punctuated::Punctuated; +use syn::{ + braced, custom_keyword, Expr, ExprRange, ItemStruct, LitInt, LitStr, PathSegment, Token, +}; use crate::common::{Layer, LayoutLike, MatrixLike, OptionalItem, Row}; use crate::TuplePair; @@ -14,6 +18,7 @@ pub(crate) struct KeyboardSettings { no_matrix: bool, bluetooth: bool, usb: bool, + encoders: bool, storage: Option, simple_backlight: Option, simple_backlight_matrix: Option, @@ -291,6 +296,14 @@ pub(crate) fn keyboard_main( }); } + if keyboard.encoders { + spawning.extend(quote! { + spawner + .spawn(::rumcake::ec11_encoders_poll!(#kb_name)) + .unwrap(); + }) + } + // Flash setup if let Some(ref driver) = keyboard.storage { if !cfg!(feature = "storage") { @@ -876,6 +889,75 @@ pub fn build_layout(raw: TokenStream, layers: LayoutLike) -> TokenStr } } +crate::parse_as_custom_fields! { + pub struct SetupEncoderArgsBuilder for SetupEncoderArgs { + sw_pin: Expr, + output_a_pin: Expr, + output_b_pin: Expr, + sw_pos: TuplePair, + cw_pos: TuplePair, + ccw_pos: TuplePair, + } +} + +custom_keyword!(Encoder); + +pub struct EncoderDefinition { + encoder_keyword: Encoder, + brace_token: syn::token::Brace, + encoder_args: SetupEncoderArgs, +} + +impl Parse for EncoderDefinition { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + Ok(Self { + encoder_keyword: input.parse()?, + brace_token: braced!(content in input), + encoder_args: content.parse()?, + }) + } +} + +pub fn setup_encoders(encoders: Punctuated) -> TokenStream { + let count = encoders.len(); + + let (positions, definitions): (Vec, Vec) = encoders + .iter() + .map(|EncoderDefinition { encoder_args, .. }| { + let SetupEncoderArgs { + sw_pin, + output_a_pin, + output_b_pin, + sw_pos, + cw_pos, + ccw_pos, + } = encoder_args; + + ( + quote! { + [#sw_pos, #cw_pos, #ccw_pos] + }, + quote! { + ::rumcake::keyboard::EC11Encoder::new(#sw_pin, #output_a_pin, #output_b_pin) + }, + ) + }) + .unzip(); + + quote! { + const ENCODER_COUNT: usize = #count; + + fn get_encoders() -> [impl ::rumcake::keyboard::Encoder; Self::ENCODER_COUNT] { + [#(#definitions),*] + } + + fn get_layout_mappings() -> [[(u8, u8); 3]; Self::ENCODER_COUNT] { + [#(#positions),*] + } + } +} + crate::parse_as_custom_fields! { pub struct RemapMacroInputBuilder for RemapMacroInput { pub original: Layer>, diff --git a/rumcake-macros/src/lib.rs b/rumcake-macros/src/lib.rs index 1677ec4..c964efd 100644 --- a/rumcake-macros/src/lib.rs +++ b/rumcake-macros/src/lib.rs @@ -162,6 +162,7 @@ pub fn keyboard_main( } #[proc_macro] +#[proc_macro_error] pub fn build_standard_matrix(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let matrix = parse_macro_input!(input as keyboard::StandardMatrixDefinition); keyboard::build_standard_matrix(matrix).into() @@ -190,6 +191,14 @@ pub fn build_layout(input: proc_macro::TokenStream) -> proc_macro::TokenStream { } #[proc_macro] +#[proc_macro_error] +pub fn setup_encoders(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let args = parse_macro_input!(input with Punctuated::parse_terminated); + keyboard::setup_encoders(args).into() +} + +#[proc_macro] +#[proc_macro_error] pub fn remap_matrix(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let remap = parse_macro_input!(input as keyboard::RemapMacroInput); keyboard::remap_matrix(remap).into() @@ -286,9 +295,10 @@ mod hw; #[cfg(feature = "stm32")] #[proc_macro] +#[proc_macro_error] pub fn stm32_input_pin(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let ident = parse_macro_input!(input as Ident); - hw::input_pin(ident).into() + let args = parse_macro_input!(input with Punctuated::parse_terminated); + hw::input_pin(args).into() } #[cfg(feature = "stm32")] diff --git a/rumcake/Cargo.toml b/rumcake/Cargo.toml index 0796885..788cfae 100644 --- a/rumcake/Cargo.toml +++ b/rumcake/Cargo.toml @@ -47,7 +47,7 @@ embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "b8be1 embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "b8be126", features = ["defmt", "defmt-timestamp-uptime"] } embassy-usb = { git = "https://github.com/embassy-rs/embassy", rev = "b8be126", features = ["defmt"] } embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "b8be126", features = ["defmt", "unstable-pac"], optional = true } -embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "b8be126", features = ["defmt", "unstable-pac"], optional = true } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "b8be126", features = ["defmt", "unstable-pac", "exti"], optional = true } embassy-nrf = { git = "https://github.com/embassy-rs/embassy", rev = "b8be126", features = ["defmt", "nfc-pins-as-gpio", "time-driver-rtc1"], optional = true } nrf-softdevice = { git = "https://github.com/embassy-rs/nrf-softdevice", rev = "487f98e", optional = true } tickv = { git = "https://github.com/tock/tock", rev = "18cf287" } diff --git a/rumcake/src/keyboard.rs b/rumcake/src/keyboard.rs index 8751d6b..7cd9648 100644 --- a/rumcake/src/keyboard.rs +++ b/rumcake/src/keyboard.rs @@ -4,14 +4,17 @@ //! Keyboard layouts and matrices are implemented with the help of [TeXitoi's `keyberon` crate](`keyberon`). use core::convert::Infallible; +use core::fmt::Debug; use core::ops::Range; use defmt::{debug, info, warn, Debug2Format}; +use embassy_futures::select::{select, select_slice, Either}; use embassy_sync::channel::Channel; use embassy_sync::mutex::Mutex; use embassy_sync::pubsub::{PubSubBehavior, PubSubChannel}; use embassy_time::{Duration, Ticker, Timer}; use embedded_hal::digital::v2::{InputPin, OutputPin}; +use embedded_hal_async::digital::Wait; use heapless::Vec; use keyberon::analog::{AnalogActuator, AnalogAcutationMode}; use keyberon::debounce::Debouncer; @@ -30,7 +33,8 @@ use crate::hw::platform::RawMutex; use crate::hw::{HIDDevice, CURRENT_OUTPUT_STATE}; pub use rumcake_macros::{ - build_analog_matrix, build_direct_pin_matrix, build_layout, build_standard_matrix, remap_matrix, + build_analog_matrix, build_direct_pin_matrix, build_layout, build_standard_matrix, + remap_matrix, setup_encoders, }; /// Basic keyboard trait that must be implemented to use rumcake. Defines basic keyboard information. @@ -61,7 +65,7 @@ pub trait KeyboardLayout { &POLLED_EVENTS_CHANNEL } - const NUM_ENCODERS: usize = 0; // Only for VIA compatibility, no proper encoder support. This is the default if not set in QMK + const NUM_ENCODERS: usize = 0; // No encoder via support yet. This is the default if not set in QMK /// Number of columns in the layout. /// @@ -127,6 +131,66 @@ impl Layout { } } +pub trait DeviceWithEncoders { + type Layout: private::MaybeKeyboardLayout = private::EmptyKeyboardLayout; + + const ENCODER_COUNT: usize; + + fn get_encoders() -> [impl Encoder; Self::ENCODER_COUNT]; + + fn get_layout_mappings() -> [[(u8, u8); 3]; Self::ENCODER_COUNT]; +} + +pub trait Encoder { + async fn wait_for_event(&mut self) -> EncoderEvent; +} + +pub enum EncoderEvent { + ClockwiseRotation, + CounterClockwiseRotation, + Press, + Release, +} + +pub struct EC11Encoder { + sw: SW, + a: A, + b: B, +} + +impl EC11Encoder { + pub fn new(sw: SW, a: A, b: B) -> Self { + Self { sw, a, b } + } + + pub async fn wait_for_event(&mut self) -> EncoderEvent { + let Self { sw, a, b } = self; + + match select(a.wait_for_falling_edge(), sw.wait_for_any_edge()).await { + Either::First(_) => { + if b.is_high().unwrap_or_default() { + EncoderEvent::CounterClockwiseRotation + } else { + EncoderEvent::ClockwiseRotation + } + } + Either::Second(_) => { + if sw.is_low().unwrap_or_default() { + EncoderEvent::Press + } else { + EncoderEvent::Release + } + } + } + } +} + +impl Encoder for EC11Encoder { + async fn wait_for_event(&mut self) -> EncoderEvent { + self.wait_for_event().await + } +} + /// A trait that must be implemented for any device that needs to poll a switch matrix. pub trait KeyboardMatrix { /// The layout to send matrix events to. @@ -360,6 +424,56 @@ where } } +#[rumcake_macros::task] +pub async fn ec11_encoders_poll(_k: K) +where + [(); K::ENCODER_COUNT]:, +{ + let mappings = K::get_layout_mappings(); + let mut encoders = K::get_encoders(); + + let layout_channel = ::get_matrix_events_channel(); + let mut events: Vec = Vec::new(); + + loop { + events.clear(); + let (event, idx) = select_slice( + &mut encoders + .iter_mut() + .map(|e| e.wait_for_event()) + .collect::>(), + ) + .await; + + let [sw_pos, cw_pos, ccw_pos] = mappings[idx]; + + match event { + EncoderEvent::ClockwiseRotation => { + events.push(Event::Press(cw_pos.0, cw_pos.1)); + events.push(Event::Release(cw_pos.0, cw_pos.1)); + } + EncoderEvent::CounterClockwiseRotation => { + events.push(Event::Press(ccw_pos.0, ccw_pos.1)); + events.push(Event::Release(ccw_pos.0, ccw_pos.1)); + } + EncoderEvent::Press => { + events.push(Event::Press(sw_pos.0, sw_pos.1)); + } + EncoderEvent::Release => { + events.push(Event::Release(sw_pos.0, sw_pos.1)); + } + }; + + for e in &events { + if let Some(layout_channel) = layout_channel { + layout_channel.send(*e).await; + } + } + + Timer::after(Duration::from_millis(1)).await; + } +} + #[rumcake_macros::task] pub async fn matrix_poll(_k: K) { let matrix = K::get_matrix(); diff --git a/rumcake/src/lib.rs b/rumcake/src/lib.rs index 86225f0..769cd44 100644 --- a/rumcake/src/lib.rs +++ b/rumcake/src/lib.rs @@ -147,7 +147,7 @@ pub mod drivers; pub mod tasks { pub use crate::hw::__output_switcher; - pub use crate::keyboard::{__layout_collect, __matrix_poll}; + pub use crate::keyboard::{__ec11_encoders_poll, __layout_collect, __matrix_poll}; #[cfg(all(feature = "lighting", feature = "storage"))] pub use crate::lighting::__lighting_storage_task;