diff --git a/rmk-macro/src/behavior.rs b/rmk-macro/src/behavior.rs index ec49c629..c4ce7dc2 100644 --- a/rmk-macro/src/behavior.rs +++ b/rmk-macro/src/behavior.rs @@ -3,8 +3,9 @@ use quote::quote; -use crate::config::{OneShotConfig, TriLayerConfig}; +use crate::config::{CombosConfig, OneShotConfig, TriLayerConfig}; use crate::keyboard_config::KeyboardConfig; +use crate::layout::parse_key; fn expand_tri_layer(tri_layer: &Option) -> proc_macro2::TokenStream { match tri_layer { @@ -39,14 +40,50 @@ fn expand_one_shot(one_shot: &Option) -> proc_macro2::TokenStream } } +fn expand_combos(combos: &Option) -> proc_macro2::TokenStream { + let default = quote! { ::core::default::Default::default() }; + match combos { + Some(combos) => { + let combos_def = combos.combos.iter().map(|combo| { + let actions = combo.actions.iter().map(|a| parse_key(a.to_owned())); + let output = parse_key(combo.output.to_owned()); + let layer = match combo.layer { + Some(layer) => quote! { ::core::option::Option::Some(#layer) }, + None => quote! { ::core::option::Option::None }, + }; + quote! { ::rmk::combo::Combo::new([#(#actions),*], #output, #layer) } + }); + + let timeout = match &combos.timeout { + Some(t) => { + let millis = t.0; + quote! { timeout: ::embassy_time::Duration::from_millis(#millis), } + } + None => quote! {}, + }; + + quote! { + ::rmk::config::CombosConfig { + combos: ::rmk::heapless::Vec::from_iter([#(#combos_def),*]), + #timeout + ..Default::default() + } + } + } + None => default, + } +} + pub(crate) fn expand_behavior_config(keyboard_config: &KeyboardConfig) -> proc_macro2::TokenStream { let tri_layer = expand_tri_layer(&keyboard_config.behavior.tri_layer); let one_shot = expand_one_shot(&keyboard_config.behavior.one_shot); + let combos = expand_combos(&keyboard_config.behavior.combo); quote! { let behavior_config = ::rmk::config::BehaviorConfig { tri_layer: #tri_layer, one_shot: #one_shot, + combo: #combos, }; } } diff --git a/rmk-macro/src/config/mod.rs b/rmk-macro/src/config/mod.rs index ffc03c30..3cab2685 100644 --- a/rmk-macro/src/config/mod.rs +++ b/rmk-macro/src/config/mod.rs @@ -134,6 +134,7 @@ pub struct LayoutConfig { pub struct BehaviorConfig { pub tri_layer: Option, pub one_shot: Option, + pub combo: Option, } /// Configurations for tri layer @@ -150,6 +151,21 @@ pub struct OneShotConfig { pub timeout: Option, } +/// Configurations for combos +#[derive(Clone, Debug, Deserialize)] +pub struct CombosConfig { + pub combos: Vec, + pub timeout: Option, +} + +/// Configurations for combo +#[derive(Clone, Debug, Deserialize)] +pub struct ComboConfig { + pub actions: Vec, + pub output: String, + pub layer: Option, +} + /// Configurations for split keyboards #[derive(Clone, Debug, Default, Deserialize)] pub struct SplitConfig { @@ -200,13 +216,15 @@ fn parse_duration_millis<'de, D: de::Deserializer<'de>>(deserializer: D) -> Resu let unit = &input[num.len()..]; let num: u64 = num.parse().map_err(|_| { de::Error::custom(format!( - "Invalid number \"{num}\" in [one_shot.timeout]: number part must be a u64" + "Invalid number \"{num}\" in duration: number part must be a u64" )) })?; match unit { - "s" => Ok(num*1000), + "s" => Ok(num * 1000), "ms" => Ok(num), - other => Err(de::Error::custom(format!("Invalid unit \"{other}\" in [one_shot.timeout]: unit part must be either \"s\" or \"ms\""))), + other => Err(de::Error::custom(format!( + "Invalid unit \"{other}\" for duration: unit part must be either \"s\" or \"ms\"" + ))), } } diff --git a/rmk-macro/src/keyboard_config.rs b/rmk-macro/src/keyboard_config.rs index c3503c34..b1071a31 100644 --- a/rmk-macro/src/keyboard_config.rs +++ b/rmk-macro/src/keyboard_config.rs @@ -22,6 +22,11 @@ macro_rules! rmk_compile_error { }; } +// Max number of combos +pub const COMBO_MAX_NUM: usize = 8; +// Max size of combos +pub const COMBO_MAX_LENGTH: usize = 4; + /// Keyboard's basic info #[allow(unused)] #[derive(Clone, Debug, Deserialize)] @@ -440,6 +445,25 @@ impl KeyboardConfig { behavior.one_shot = behavior.one_shot.or(default.one_shot); + behavior.combo = behavior.combo.or(default.combo); + if let Some(combo) = &behavior.combo { + if combo.combos.len() > COMBO_MAX_NUM { + return rmk_compile_error!(format!("keyboard.toml: number of combos is greater than [behavior.combo.max_num]")); + } + + for (i, c) in combo.combos.iter().enumerate() { + if c.actions.len() > COMBO_MAX_LENGTH { + return rmk_compile_error!(format!("keyboard.toml: number of keys in combo #{i} is greater than [behavior.combo.max_length]")); + } + + if let Some(layer) = c.layer { + if layer >= layout.layers { + return rmk_compile_error!(format!("keyboard.toml: layer in combo #{i} is greater than [layout.layers]")); + } + } + } + } + Ok(behavior) } None => Ok(default), diff --git a/rmk-macro/src/layout.rs b/rmk-macro/src/layout.rs index 70479d29..d2fcd31c 100644 --- a/rmk-macro/src/layout.rs +++ b/rmk-macro/src/layout.rs @@ -38,7 +38,7 @@ fn expand_row(row: Vec) -> TokenStream2 { } /// Parse the key string at a single position -fn parse_key(key: String) -> TokenStream2 { +pub(crate) fn parse_key(key: String) -> TokenStream2 { if key.len() < 5 { return if key.len() > 0 && key.trim_start_matches("_").len() == 0 { quote! { ::rmk::a!(No) } diff --git a/rmk/src/ble/esp/mod.rs b/rmk/src/ble/esp/mod.rs index 5837de4f..6ff3fc13 100644 --- a/rmk/src/ble/esp/mod.rs +++ b/rmk/src/ble/esp/mod.rs @@ -58,16 +58,19 @@ pub(crate) async fn initialize_esp_ble_keyboard_with_config_and_run< ) .await; - let keymap = RefCell::new(KeyMap::new_from_storage(default_keymap, Some(&mut storage)).await); + let keymap = RefCell::new( + KeyMap::new_from_storage( + default_keymap, + Some(&mut storage), + keyboard_config.behavior_config, + ) + .await, + ); let keyboard_report_sender = keyboard_report_channel.sender(); let keyboard_report_receiver = keyboard_report_channel.receiver(); - let mut keyboard = Keyboard::new( - &keymap, - &keyboard_report_sender, - keyboard_config.behavior_config, - ); + let mut keyboard = Keyboard::new(&keymap, &keyboard_report_sender); // esp32c3 doesn't have USB device, so there is no usb here // TODO: add usb service for other chips of esp32 which have USB device diff --git a/rmk/src/ble/nrf/mod.rs b/rmk/src/ble/nrf/mod.rs index 966cd122..45bbb2d7 100644 --- a/rmk/src/ble/nrf/mod.rs +++ b/rmk/src/ble/nrf/mod.rs @@ -243,7 +243,14 @@ pub(crate) async fn initialize_nrf_ble_keyboard_and_run< // Flash and keymap configuration let flash = Flash::take(sd); let mut storage = Storage::new(flash, default_keymap, keyboard_config.storage_config).await; - let keymap = RefCell::new(KeyMap::new_from_storage(default_keymap, Some(&mut storage)).await); + let keymap = RefCell::new( + KeyMap::new_from_storage( + default_keymap, + Some(&mut storage), + keyboard_config.behavior_config, + ) + .await, + ); let mut buf: [u8; 128] = [0; 128]; @@ -292,11 +299,7 @@ pub(crate) async fn initialize_nrf_ble_keyboard_and_run< let keyboard_report_receiver = keyboard_report_channel.receiver(); // Keyboard services - let mut keyboard = Keyboard::new( - &keymap, - &keyboard_report_sender, - keyboard_config.behavior_config, - ); + let mut keyboard = Keyboard::new(&keymap, &keyboard_report_sender); #[cfg(not(feature = "_no_usb"))] let mut usb_device = KeyboardUsbDevice::new(usb_driver, keyboard_config.usb_config); let mut vial_service = VialService::new(&keymap, keyboard_config.vial_config); diff --git a/rmk/src/combo.rs b/rmk/src/combo.rs new file mode 100644 index 00000000..128d2f23 --- /dev/null +++ b/rmk/src/combo.rs @@ -0,0 +1,86 @@ +use heapless::Vec; + +use crate::{action::KeyAction, keyboard::KeyEvent}; + +// Max number of combos +pub(crate) const COMBO_MAX_NUM: usize = 8; +// Max size of combos +pub(crate) const COMBO_MAX_LENGTH: usize = 4; + +#[derive(Clone)] +pub struct Combo { + pub(crate) actions: Vec, + pub(crate) output: KeyAction, + pub(crate) layer: Option, + state: u8, +} + +impl Default for Combo { + fn default() -> Self { + Self::empty() + } +} + +impl Combo { + pub fn new>( + actions: I, + output: KeyAction, + layer: Option, + ) -> Self { + Self { + actions: Vec::from_iter(actions), + output, + layer, + state: 0, + } + } + + pub fn empty() -> Self { + Self::new( + Vec::::new(), + KeyAction::No, + None, + ) + } + + pub(crate) fn update( + &mut self, + key_action: KeyAction, + key_event: KeyEvent, + active_layer: u8, + ) -> bool { + if !key_event.pressed || key_action == KeyAction::No { + return false; + } + + if let Some(layer) = self.layer { + if layer != active_layer { + return false; + } + } + + let action_idx = self.actions.iter().position(|&a| a == key_action); + if let Some(i) = action_idx { + self.state |= 1 << i; + } else if !self.done() { + self.reset(); + } + action_idx.is_some() + } + + pub(crate) fn done(&self) -> bool { + self.started() && self.keys_pressed() == self.actions.len() as u32 + } + + pub(crate) fn started(&self) -> bool { + self.state != 0 + } + + pub(crate) fn keys_pressed(&self) -> u32 { + self.state.count_ones() + } + + pub(crate) fn reset(&mut self) { + self.state = 0; + } +} diff --git a/rmk/src/config/mod.rs b/rmk/src/config/mod.rs index 03147bf3..0fd22807 100644 --- a/rmk/src/config/mod.rs +++ b/rmk/src/config/mod.rs @@ -3,6 +3,7 @@ mod esp_config; #[cfg(feature = "_nrf_ble")] mod nrf_config; +use ::heapless::Vec; #[cfg(feature = "_esp_ble")] pub use esp_config::BleBatteryConfig; #[cfg(feature = "_nrf_ble")] @@ -11,6 +12,8 @@ pub use nrf_config::BleBatteryConfig; use embassy_time::Duration; use embedded_hal::digital::OutputPin; +use crate::combo::{Combo, COMBO_MAX_NUM}; + /// Internal configurations for RMK keyboard. pub struct RmkConfig<'a, O: OutputPin> { pub mouse_config: MouseConfig, @@ -45,6 +48,7 @@ impl<'a, O: OutputPin> Default for RmkConfig<'a, O> { pub struct BehaviorConfig { pub tri_layer: Option<[u8; 3]>, pub one_shot: OneShotConfig, + pub combo: CombosConfig, } /// Config for one shot behavior @@ -60,6 +64,21 @@ impl Default for OneShotConfig { } } +/// Config for combo behavior +pub struct CombosConfig { + pub combos: Vec, + pub timeout: Duration, +} + +impl Default for CombosConfig { + fn default() -> Self { + Self { + timeout: Duration::from_millis(50), + combos: Vec::new(), + } + } +} + /// Config for storage #[derive(Clone, Copy, Debug)] pub struct StorageConfig { diff --git a/rmk/src/keyboard.rs b/rmk/src/keyboard.rs index 0dbc049d..af7f42a8 100644 --- a/rmk/src/keyboard.rs +++ b/rmk/src/keyboard.rs @@ -1,4 +1,4 @@ -use crate::config::BehaviorConfig; +use crate::combo::{Combo, COMBO_MAX_LENGTH}; use crate::{ action::{Action, KeyAction}, hid::{ConnectionType, HidWriterWrapper}, @@ -16,7 +16,7 @@ use embassy_sync::{ channel::{Channel, Receiver, Sender}, }; use embassy_time::{Instant, Timer}; -use heapless::{FnvIndexMap, Vec}; +use heapless::{Deque, FnvIndexMap, Vec}; use postcard::experimental::max_size::MaxSize; use serde::{Deserialize, Serialize}; use usbd_hid::descriptor::KeyboardReport; @@ -132,9 +132,6 @@ pub(crate) struct Keyboard<'a, const ROW: usize, const COL: usize, const NUM_LAY /// Timer which records the timestamp of key changes pub(crate) timer: [[Option; ROW]; COL], - /// Options for configurable action behavior - behavior: BehaviorConfig, - /// One shot modifier state osm_state: OneShotState, @@ -159,6 +156,12 @@ pub(crate) struct Keyboard<'a, const ROW: usize, const COL: usize, const NUM_LAY /// The current distance of mouse key moving mouse_key_move_delta: i8, mouse_wheel_move_delta: i8, + + /// Buffer for pressed `KeyAction` and `KeyEvents` in combos + combo_actions_buffer: Deque<(KeyAction, KeyEvent), COMBO_MAX_LENGTH>, + + /// Used for temporarily disabling combos + combo_on: bool, } impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> @@ -167,13 +170,11 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> pub(crate) fn new( keymap: &'a RefCell>, sender: &'a Sender<'a, CriticalSectionRawMutex, KeyboardReportMessage, REPORT_CHANNEL_SIZE>, - behavior: BehaviorConfig, ) -> Self { Keyboard { keymap, sender, timer: [[None; ROW]; COL], - behavior, osm_state: OneShotState::default(), osl_state: OneShotState::default(), unprocessed_events: Vec::new(), @@ -191,6 +192,8 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> last_mouse_tick: FnvIndexMap::new(), mouse_key_move_delta: 8, mouse_wheel_move_delta: 1, + combo_actions_buffer: Deque::new(), + combo_on: true, } } @@ -270,11 +273,22 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> } // Process key - let action = self + let key_action = self .keymap .borrow_mut() .get_action_with_layer_cache(key_event); - match action { + + if self.combo_on { + if let Some(key_action) = self.process_combo(key_action, key_event).await { + self.process_key_action(key_action, key_event).await; + } + } else { + self.process_key_action(key_action, key_event).await; + } + } + + async fn process_key_action(&mut self, key_action: KeyAction, key_event: KeyEvent) { + match key_action { KeyAction::No | KeyAction::Transparent => (), KeyAction::Single(a) => self.process_key_action_normal(a, key_event).await, KeyAction::WithModifier(a, m) => { @@ -300,11 +314,74 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> .await; } } + } + + async fn process_combo( + &mut self, + key_action: KeyAction, + key_event: KeyEvent, + ) -> Option { + let mut is_combo_action = false; + let current_layer = self.keymap.borrow().get_activated_layer(); + for combo in self.keymap.borrow_mut().combos.iter_mut() { + is_combo_action |= combo.update(key_action, key_event, current_layer); + } + + if key_event.pressed && is_combo_action { + if self + .combo_actions_buffer + .push_back((key_action, key_event)) + .is_err() + { + error!("Combo actions buffer overflowed! This is a bug and should not happen!"); + } + + let next_action = self + .keymap + .borrow_mut() + .combos + .iter() + .find_map(|combo| combo.done().then_some(combo.output)); + + if next_action.is_some() { + self.combo_actions_buffer.clear(); + } else { + let timeout = + embassy_time::Timer::after(self.keymap.borrow().behavior.combo.timeout); + match select(timeout, key_event_channel.receive()).await { + embassy_futures::select::Either::First(_) => self.dispatch_combos().await, + embassy_futures::select::Either::Second(event) => { + self.unprocessed_events.push(event).unwrap() + } + } + } + next_action + } else { + if !key_event.pressed { + for combo in self.keymap.borrow_mut().combos.iter_mut() { + if combo.done() && combo.actions.contains(&key_action) { + combo.reset(); + return Some(combo.output); + } + } + } + + self.dispatch_combos().await; + Some(key_action) + } + } - // Tri Layer - if let Some(ref tri_layer) = self.behavior.tri_layer { - self.keymap.borrow_mut().update_tri_layer(tri_layer); + async fn dispatch_combos(&mut self) { + while let Some((action, event)) = self.combo_actions_buffer.pop_front() { + self.process_key_action(action, event).await; } + + self.keymap + .borrow_mut() + .combos + .iter_mut() + .filter(|combo| !combo.done()) + .for_each(Combo::reset); } async fn update_osm(&mut self, key_event: KeyEvent) { @@ -519,7 +596,8 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> OneShotState::Initial(m) | OneShotState::Single(m) => { self.osm_state = OneShotState::Single(m); - let timeout = embassy_time::Timer::after(self.behavior.one_shot.timeout); + let timeout = + embassy_time::Timer::after(self.keymap.borrow().behavior.one_shot.timeout); match select(timeout, key_event_channel.receive()).await { embassy_futures::select::Either::First(_) => { // Timeout, release modifier @@ -570,7 +648,8 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> OneShotState::Initial(l) | OneShotState::Single(l) => { self.osl_state = OneShotState::Single(l); - let timeout = embassy_time::Timer::after(self.behavior.one_shot.timeout); + let timeout = + embassy_time::Timer::after(self.keymap.borrow().behavior.one_shot.timeout); match select(timeout, key_event_channel.receive()).await { embassy_futures::select::Either::First(_) => { // Timeout, deactivate layer @@ -646,6 +725,8 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> } else if key.is_macro() { // Process macro self.process_action_macro(key, key_event).await; + } else if key.is_combo() { + self.process_action_combo(key, key_event).await; } else { warn!("Unsupported key: {:?}", key); } @@ -661,6 +742,18 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> } } + /// Process combo action. + async fn process_action_combo(&mut self, key: KeyCode, key_event: KeyEvent) { + if key_event.pressed { + match key { + KeyCode::ComboOn => self.combo_on = true, + KeyCode::ComboOff => self.combo_on = false, + KeyCode::ComboToggle => self.combo_on = !self.combo_on, + _ => (), + } + } + } + /// Process consumer control action. Consumer control keys are keys in hid consumer page, such as media keys. async fn process_action_consumer_control(&mut self, key: KeyCode, key_event: KeyEvent) { if key.is_consumer() { diff --git a/rmk/src/keycode.rs b/rmk/src/keycode.rs index e4c2f55b..c4af0d00 100644 --- a/rmk/src/keycode.rs +++ b/rmk/src/keycode.rs @@ -1006,6 +1006,11 @@ impl KeyCode { KeyCode::Bootloader <= self && self <= KeyCode::AltRepeatKey } + /// Returns `true` if the keycode is a combo keycode + pub(crate) fn is_combo(self) -> bool { + KeyCode::ComboOn <= self && self <= KeyCode::ComboToggle + } + /// Returns `true` if the keycode is a kb keycode pub(crate) fn is_kb(self) -> bool { KeyCode::Kb0 <= self && self <= KeyCode::Kb31 diff --git a/rmk/src/keymap.rs b/rmk/src/keymap.rs index b4674eb8..8b8ef921 100644 --- a/rmk/src/keymap.rs +++ b/rmk/src/keymap.rs @@ -1,5 +1,7 @@ use crate::{ action::KeyAction, + combo::{Combo, COMBO_MAX_NUM}, + config::BehaviorConfig, keyboard::KeyEvent, keyboard_macro::{MacroOperation, MACRO_SPACE_SIZE}, keycode::KeyCode, @@ -27,50 +29,63 @@ pub(crate) struct KeyMap<'a, const ROW: usize, const COL: usize, const NUM_LAYER layer_cache: [[u8; COL]; ROW], /// Macro cache pub(crate) macro_cache: [u8; MACRO_SPACE_SIZE], + /// Combos + pub(crate) combos: [Combo; COMBO_MAX_NUM], + /// Options for configurable action behavior + pub(crate) behavior: BehaviorConfig, } impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> KeyMap<'a, ROW, COL, NUM_LAYER> { - pub(crate) async fn new(action_map: &'a mut [[[KeyAction; COL]; ROW]; NUM_LAYER]) -> Self { + pub(crate) async fn new( + action_map: &'a mut [[[KeyAction; COL]; ROW]; NUM_LAYER], + behavior: BehaviorConfig, + ) -> Self { + let mut combos: [Combo; COMBO_MAX_NUM] = Default::default(); + for (i, combo) in behavior.combo.combos.iter().enumerate() { + combos[i] = combo.clone(); + } KeyMap { layers: action_map, layer_state: [false; NUM_LAYER], default_layer: 0, layer_cache: [[0; COL]; ROW], macro_cache: [0; MACRO_SPACE_SIZE], + combos, + behavior, } } pub(crate) async fn new_from_storage( action_map: &'a mut [[[KeyAction; COL]; ROW]; NUM_LAYER], storage: Option<&mut Storage>, + behavior: BehaviorConfig, ) -> Self { // If the storage is initialized, read keymap from storage let mut macro_cache = [0; MACRO_SPACE_SIZE]; + let mut combos: [Combo; COMBO_MAX_NUM] = Default::default(); + for (i, combo) in behavior.combo.combos.iter().enumerate() { + combos[i] = combo.clone(); + } if let Some(storage) = storage { - // Read keymap to `action_map` - if storage.read_keymap(action_map).await.is_err() { - error!("Keymap reading aborted by an error, clearing the storage..."); - // Dont sent flash message here, since the storage task is not running yet + if { + Ok(()) + // Read keymap to `action_map` + .and(storage.read_keymap(action_map).await) + // Read combo cache + .and(storage.read_macro_cache(&mut macro_cache).await) + // Read macro cache + .and(storage.read_combos(&mut combos).await) + } + .is_err() + { + error!("Failed to read from storage, clearing..."); sequential_storage::erase_all(&mut storage.flash, storage.storage_range.clone()) .await .ok(); reboot_keyboard(); - } else { - // Read macro cache - if storage.read_macro_cache(&mut macro_cache).await.is_err() { - error!("Wrong macro cache, clearing the storage..."); - sequential_storage::erase_all( - &mut storage.flash, - storage.storage_range.clone(), - ) - .await - .ok(); - - reboot_keyboard(); - } } } @@ -80,6 +95,8 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> default_layer: 0, layer_cache: [[0; COL]; ROW], macro_cache, + combos, + behavior, } } @@ -228,7 +245,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> KeyAction::No } - fn get_activated_layer(&self) -> u8 { + pub(crate) fn get_activated_layer(&self) -> u8 { for (layer_idx, _) in self.layers.iter().enumerate().rev() { if self.layer_state[layer_idx] || layer_idx as u8 == self.default_layer { return layer_idx as u8; @@ -253,10 +270,12 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> self.layer_cache[row][col] = layer_num; } - /// Update given Tri Layer state - pub(crate) fn update_tri_layer(&mut self, tri_layer: &[u8; 3]) { - self.layer_state[tri_layer[2] as usize] = - self.layer_state[tri_layer[0] as usize] && self.layer_state[tri_layer[1] as usize]; + /// Update Tri Layer state + fn update_tri_layer(&mut self) { + if let Some(ref tri_layer) = self.behavior.tri_layer { + self.layer_state[tri_layer[2] as usize] = + self.layer_state[tri_layer[0] as usize] && self.layer_state[tri_layer[1] as usize]; + } } /// Activate given layer @@ -269,6 +288,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> return; } self.layer_state[layer_num as usize] = true; + self.update_tri_layer(); } /// Deactivate given layer @@ -281,6 +301,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> return; } self.layer_state[layer_num as usize] = false; + self.update_tri_layer(); } /// Toggle given layer diff --git a/rmk/src/lib.rs b/rmk/src/lib.rs index 69218137..36ab1512 100644 --- a/rmk/src/lib.rs +++ b/rmk/src/lib.rs @@ -45,6 +45,7 @@ use embedded_hal_async::digital::Wait; use embedded_storage::nor_flash::NorFlash; pub use flash::EmptyFlashWrapper; use futures::pin_mut; +pub use heapless; use keyboard::{ communication_task, keyboard_report_channel, Keyboard, KeyboardReportMessage, REPORT_CHANNEL_SIZE, @@ -60,6 +61,7 @@ use {embedded_storage_async::nor_flash::NorFlash as AsyncNorFlash, storage::Stor pub mod action; #[cfg(feature = "_ble")] pub mod ble; +pub mod combo; pub mod config; mod debounce; pub mod direct_pin; @@ -240,22 +242,27 @@ pub(crate) async fn initialize_usb_keyboard_and_run< #[cfg(any(feature = "_nrf_ble", not(feature = "_no_external_storage")))] let (mut storage, keymap) = { let mut s = Storage::new(flash, default_keymap, keyboard_config.storage_config).await; - let keymap = RefCell::new(KeyMap::new_from_storage(default_keymap, Some(&mut s)).await); + let keymap = RefCell::new( + KeyMap::new_from_storage( + default_keymap, + Some(&mut s), + keyboard_config.behavior_config, + ) + .await, + ); (s, keymap) }; #[cfg(all(not(feature = "_nrf_ble"), feature = "_no_external_storage"))] - let keymap = RefCell::new(KeyMap::::new(default_keymap).await); + let keymap = RefCell::new( + KeyMap::::new(default_keymap, keyboard_config.behavior_config).await, + ); let keyboard_report_sender = keyboard_report_channel.sender(); let keyboard_report_receiver = keyboard_report_channel.receiver(); // Create keyboard services and devices let (mut keyboard, mut usb_device, mut vial_service, mut light_service) = ( - Keyboard::new( - &keymap, - &keyboard_report_sender, - keyboard_config.behavior_config, - ), + Keyboard::new(&keymap, &keyboard_report_sender), KeyboardUsbDevice::new(usb_driver, keyboard_config.usb_config), VialService::new(&keymap, keyboard_config.vial_config), LightService::from_config(keyboard_config.light_config), diff --git a/rmk/src/split/central.rs b/rmk/src/split/central.rs index 412fc0b9..2100936e 100644 --- a/rmk/src/split/central.rs +++ b/rmk/src/split/central.rs @@ -277,6 +277,7 @@ pub(crate) async fn initialize_usb_split_central_and_run< KeyMap::::new_from_storage( default_keymap, Some(&mut s), + keyboard_config.behavior_config, ) .await, ); @@ -284,18 +285,20 @@ pub(crate) async fn initialize_usb_split_central_and_run< }; #[cfg(all(not(feature = "_nrf_ble"), feature = "_no_external_storage"))] - let keymap = RefCell::new(KeyMap::::new(default_keymap).await); + let keymap = RefCell::new( + KeyMap::::new( + default_keymap, + keyboard_config.behavior_config, + ) + .await, + ); let keyboard_report_sender = keyboard_report_channel.sender(); let keyboard_report_receiver = keyboard_report_channel.receiver(); // Create keyboard services and devices let (mut keyboard, mut usb_device, mut vial_service, mut light_service) = ( - Keyboard::new( - &keymap, - &keyboard_report_sender, - keyboard_config.behavior_config, - ), + Keyboard::new(&keymap, &keyboard_report_sender), KeyboardUsbDevice::new(usb_driver, keyboard_config.usb_config), VialService::new(&keymap, keyboard_config.vial_config), LightService::from_config(keyboard_config.light_config), diff --git a/rmk/src/storage/mod.rs b/rmk/src/storage/mod.rs index f2c2b9ef..4feccc4f 100644 --- a/rmk/src/storage/mod.rs +++ b/rmk/src/storage/mod.rs @@ -1,7 +1,10 @@ mod eeconfig; pub mod nor_flash; -use crate::config::StorageConfig; +use crate::{ + combo::{Combo, COMBO_MAX_LENGTH}, + config::StorageConfig, +}; use byteorder::{BigEndian, ByteOrder}; use core::fmt::Debug; use core::ops::Range; @@ -10,6 +13,7 @@ use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::channel::Channel; use embedded_storage::nor_flash::NorFlash; use embedded_storage_async::nor_flash::NorFlash as AsyncNorFlash; +use heapless::Vec; use sequential_storage::{ cache::NoCache, map::{fetch_item, store_item, SerializationError, Value}, @@ -57,6 +61,8 @@ pub(crate) enum FlashOperationMessage { }, // Current saved connection type ConnectionType(u8), + // Write combo + WriteCombo(ComboData), } #[repr(u32)] @@ -68,6 +74,7 @@ pub(crate) enum StorageKeys { LayoutConfig, KeymapKeys, MacroData, + ComboData, ConnectionType, #[cfg(feature = "_nrf_ble")] ActiveBleProfile = 0xEE, @@ -85,6 +92,7 @@ impl StorageKeys { 4 => Some(StorageKeys::LayoutConfig), 5 => Some(StorageKeys::KeymapKeys), 6 => Some(StorageKeys::MacroData), + 7 => Some(StorageKeys::ComboData), #[cfg(feature = "_nrf_ble")] 0xEF => Some(StorageKeys::BleBondInfo), _ => None, @@ -99,6 +107,7 @@ pub(crate) enum StorageData { KeymapConfig(EeKeymapConfig), KeymapKey(KeymapKey), MacroData([u8; MACRO_SPACE_SIZE]), + ComboData(ComboData), ConnectionType(u8), #[cfg(feature = "_nrf_ble")] BondInfo(BondInfo), @@ -118,6 +127,10 @@ pub(crate) fn get_keymap_key u32 { + (0x3000 + idx) as u32 +} + impl Value<'_> for StorageData { fn serialize_into(&self, buffer: &mut [u8]) -> Result { if buffer.len() < 6 { @@ -162,6 +175,20 @@ impl Value<'_> for StorageData { buffer[1..MACRO_SPACE_SIZE + 1].copy_from_slice(d); Ok(MACRO_SPACE_SIZE + 1) } + StorageData::ComboData(combo) => { + if buffer.len() < 11 { + return Err(SerializationError::BufferTooSmall); + } + buffer[0] = StorageKeys::ComboData as u8; + for i in 0..4 { + BigEndian::write_u16( + &mut buffer[1 + i * 2..3 + i * 2], + to_via_keycode(combo.actions[i]), + ); + } + BigEndian::write_u16(&mut buffer[9..11], to_via_keycode(combo.output)); + Ok(11) + } StorageData::ConnectionType(ty) => { buffer[0] = StorageKeys::ConnectionType as u8; buffer[1] = *ty; @@ -245,6 +272,22 @@ impl Value<'_> for StorageData { buf.copy_from_slice(&buffer[1..MACRO_SPACE_SIZE + 1]); Ok(StorageData::MacroData(buf)) } + StorageKeys::ComboData => { + if buffer.len() < 11 { + return Err(SerializationError::InvalidData); + } + let mut actions = [KeyAction::No; 4]; + for i in 0..4 { + actions[i] = + from_via_keycode(BigEndian::read_u16(&buffer[1 + i * 2..3 + i * 2])); + } + let output = from_via_keycode(BigEndian::read_u16(&buffer[9..11])); + Ok(StorageData::ComboData(ComboData { + idx: 0, + actions, + output, + })) + } StorageKeys::ConnectionType => Ok(StorageData::ConnectionType(buffer[1])), #[cfg(feature = "_nrf_ble")] StorageKeys::BleBondInfo => { @@ -274,6 +317,9 @@ impl StorageData { panic!("To get storage key for KeymapKey, use `get_keymap_key` instead"); } StorageData::MacroData(_) => StorageKeys::MacroData as u32, + StorageData::ComboData(_) => { + panic!("To get combo key for ComboData, use `get_combo_key` instead"); + } StorageData::ConnectionType(_) => StorageKeys::ConnectionType as u32, #[cfg(feature = "_nrf_ble")] StorageData::BondInfo(b) => get_bond_info_key(b.slot_num), @@ -282,6 +328,7 @@ impl StorageData { } } } + #[derive(Clone, Copy, Debug, Format)] pub(crate) struct LocalStorageConfig { enable: bool, @@ -301,6 +348,13 @@ pub(crate) struct KeymapKey { action: KeyAction, } +#[derive(Clone, Copy, Debug, Format)] +pub(crate) struct ComboData { + pub(crate) idx: usize, + pub(crate) actions: [KeyAction; COMBO_MAX_LENGTH], + pub(crate) output: KeyAction, +} + pub(crate) struct Storage< F: AsyncNorFlash, const ROW: usize, @@ -495,6 +549,18 @@ impl { + let key = get_combo_key(combo.idx); + store_item( + &mut self.flash, + self.storage_range.clone(), + &mut storage_cache, + &mut self.buffer, + &key, + &StorageData::ComboData(combo), + ) + .await + } FlashOperationMessage::ConnectionType(ty) => { store_item( &mut self.flash, @@ -623,6 +689,31 @@ impl Result<(), ()> { + for i in 0..combos.len() { + let key = get_combo_key(i); + let read_data = fetch_item::( + &mut self.flash, + self.storage_range.clone(), + &mut NoCache::new(), + &mut self.buffer, + &key, + ) + .await + .map_err(|e| print_storage_error::(e))?; + + if let Some(StorageData::ComboData(combo)) = read_data { + let mut actions = Vec::<_, 4>::new(); + for &action in combo.actions.iter().filter(|&&a| a != KeyAction::No) { + let _ = actions.push(action); + } + combos[i] = Combo::new(actions, combo.output, combos[i].layer); + } + } + + Ok(()) + } + async fn initialize_storage_with_config( &mut self, keymap: &[[[KeyAction; COL]; ROW]; NUM_LAYER], diff --git a/rmk/src/via/keycode_convert.rs b/rmk/src/via/keycode_convert.rs index 132e5e88..307f0e84 100644 --- a/rmk/src/via/keycode_convert.rs +++ b/rmk/src/via/keycode_convert.rs @@ -16,6 +16,8 @@ pub(crate) fn to_via_keycode(key_action: KeyAction) -> u16 { k as u16 & 0xFF | 0x7700 } else if k.is_user() { k as u16 & 0xF | 0x7E00 + } else if k.is_combo() { + k as u16 & 0xF | 0x7C50 } else { k as u16 } @@ -154,6 +156,7 @@ pub(crate) fn from_via_keycode(via_keycode: u16) -> KeyAction { KeyAction::No } 0x7700..=0x770F => { + // Macro let keycode = via_keycode & 0xFF | 0x500; KeyAction::Single(Action::Key(KeyCode::from_primitive(keycode))) } @@ -162,6 +165,11 @@ pub(crate) fn from_via_keycode(via_keycode: u16) -> KeyAction { warn!("Backlight and RGB configuration key not supported"); KeyAction::No } + 0x7C50..=0x7C52 => { + // Combo + let keycode = via_keycode & 0xFF | 0x750; + KeyAction::Single(Action::Key(KeyCode::from_primitive(keycode))) + } 0x7C00..=0x7C5F => { // TODO: Reset/GESC/Space Cadet/Haptic/Auto shift(AS)/Dynamic macro // - [GESC](https://docs.qmk.fm/#/feature_grave_esc) @@ -320,6 +328,13 @@ mod test { ), from_via_keycode(via_keycode) ); + + // ComboOff + let via_keycode = 0x7C51; + assert_eq!( + KeyAction::Single(Action::Key(KeyCode::ComboOff)), + from_via_keycode(via_keycode) + ); } #[test] @@ -409,5 +424,9 @@ mod test { ModifierCombination::new_from(false, false, true, true, true), ); assert_eq!(0x2704, to_via_keycode(a)); + + // ComboOff + let a = KeyAction::Single(Action::Key(KeyCode::ComboOff)); + assert_eq!(0x7C51, to_via_keycode(a)); } } diff --git a/rmk/src/via/process.rs b/rmk/src/via/process.rs index e6018421..1c73f7ba 100644 --- a/rmk/src/via/process.rs +++ b/rmk/src/via/process.rs @@ -1,17 +1,26 @@ -use super::{protocol::*, vial::process_vial}; -use crate::config::VialConfig; +use super::{ + protocol::*, + vial::{VialCommand, VialDynamic, VIAL_COMBO_MAX_LENGTH}, +}; use crate::{ + action::KeyAction, + combo::{Combo, COMBO_MAX_NUM}, + config::VialConfig, hid::{HidError, HidReaderWriterWrapper}, keyboard_macro::{MACRO_SPACE_SIZE, NUM_MACRO}, keymap::KeyMap, - storage::{FlashOperationMessage, FLASH_CHANNEL}, + storage::{ComboData, FlashOperationMessage, FLASH_CHANNEL}, usb::descriptor::ViaReport, - via::keycode_convert::{from_via_keycode, to_via_keycode}, + via::{ + keycode_convert::{from_via_keycode, to_via_keycode}, + vial::{VIAL_EP_SIZE, VIAL_PROTOCOL_VERSION}, + }, }; use byteorder::{BigEndian, ByteOrder, LittleEndian}; use core::cell::RefCell; use defmt::{debug, error, info, warn}; use embassy_time::Instant; +use heapless::Vec; use num_enum::{FromPrimitive, TryFromPrimitive}; pub(crate) struct VialService<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> { @@ -313,14 +322,175 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> ViaCommand::DynamicKeymapSetEncoder => { warn!("Keymap get encoder -- not supported"); } - ViaCommand::Vial => process_vial( - report, - self.vial_config.vial_keyboard_id, - self.vial_config.vial_keyboard_def, - ), + ViaCommand::Vial => self.process_vial(report).await, ViaCommand::Unhandled => report.input_data[0] = ViaCommand::Unhandled as u8, } } + + // Note: vial uses litte endian, while via uses big endian + async fn process_vial(&self, report: &mut ViaReport) { + // report.output_data[0] == 0xFE -> vial commands + let vial_command = VialCommand::from_primitive(report.output_data[1]); + match vial_command { + VialCommand::GetKeyboardId => { + debug!("Received Vial - GetKeyboardId"); + // Returns vial protocol version + vial keyboard id + LittleEndian::write_u32(&mut report.input_data[0..4], VIAL_PROTOCOL_VERSION); + report.input_data[4..12].clone_from_slice(self.vial_config.vial_keyboard_id); + } + VialCommand::GetSize => { + debug!("Received Vial - GetSize"); + LittleEndian::write_u32( + &mut report.input_data[0..4], + self.vial_config.vial_keyboard_def.len() as u32, + ); + } + VialCommand::GetKeyboardDef => { + debug!("Received Vial - GetKeyboardDefinition"); + let page = LittleEndian::read_u16(&report.output_data[2..4]) as usize; + let start = page * VIAL_EP_SIZE; + let mut end = start + VIAL_EP_SIZE; + if end < start || start >= self.vial_config.vial_keyboard_def.len() { + return; + } + if end > self.vial_config.vial_keyboard_def.len() { + end = self.vial_config.vial_keyboard_def.len(); + } + self.vial_config.vial_keyboard_def[start..end] + .iter() + .enumerate() + .for_each(|(i, v)| { + report.input_data[i] = *v; + }); + debug!( + "Vial return: page:{} start:{} end: {}, data: {:?}", + page, start, end, report.input_data + ); + } + VialCommand::GetUnlockStatus => { + debug!("Received Vial - GetUnlockStatus"); + // Reset all data to 0xFF(it's required!) + report.input_data.fill(0xFF); + // Unlocked + report.input_data[0] = 1; + // Unlock in progress + report.input_data[1] = 0; + } + VialCommand::QmkSettingsQuery => { + report.input_data.fill(0xFF); + } + VialCommand::DynamicEntryOp => { + let vial_dynamic = VialDynamic::from_primitive(report.output_data[2]); + match vial_dynamic { + VialDynamic::DynamicVialGetNumberOfEntries => { + debug!("DynamicEntryOp - DynamicVialGetNumberOfEntries"); + // TODO: Support dynamic tap dance + report.input_data[0] = 0; // Tap dance entries + report.input_data[1] = 8; // Combo entries + // TODO: Support dynamic key override + report.input_data[2] = 0; // Key override entries + } + VialDynamic::DynamicVialTapDanceGet => { + warn!("DynamicEntryOp - DynamicVialTapDanceGet -- to be implemented"); + report.input_data.fill(0x00); + } + VialDynamic::DynamicVialTapDanceSet => { + warn!("DynamicEntryOp - DynamicVialTapDanceSet -- to be implemented"); + report.input_data.fill(0x00); + } + VialDynamic::DynamicVialComboGet => { + debug!("DynamicEntryOp - DynamicVialComboGet"); + report.input_data[0] = 0; // Index 0 is the return code, 0 means success + + let combo_idx = report.output_data[3] as usize; + let combos = &self.keymap.borrow().combos; + if let Some((_, combo)) = vial_combo(combos, combo_idx) { + for i in 0..4 { + LittleEndian::write_u16( + &mut report.input_data[1 + i * 2..3 + i * 2], + to_via_keycode(*combo.actions.get(i).unwrap_or(&KeyAction::No)), + ); + } + LittleEndian::write_u16( + &mut report.input_data[9..11], + to_via_keycode(combo.output), + ); + } else { + report.input_data[1..11].fill(0); + } + } + VialDynamic::DynamicVialComboSet => { + debug!("DynamicEntryOp - DynamicVialComboSet"); + report.input_data[0] = 0; // Index 0 is the return code, 0 means success + + let combo_idx = report.output_data[3] as usize; + let combos = &mut self.keymap.borrow_mut().combos; + let Some((real_idx, combo)) = vial_combo_mut(combos, combo_idx) else { + return; + }; + + let mut actions = Vec::new(); + for i in 0..4 { + let action = from_via_keycode(LittleEndian::read_u16( + &report.output_data[4 + i * 2..6 + i * 2], + )); + if action != KeyAction::No { + let _ = actions.push(action); + } + } + let output = + from_via_keycode(LittleEndian::read_u16(&report.output_data[12..14])); + + combo.actions = actions; + combo.output = output; + + let mut actions = [KeyAction::No; 4]; + for (i, &action) in combo.actions.iter().enumerate() { + actions[i] = action; + } + FLASH_CHANNEL + .send(FlashOperationMessage::WriteCombo(ComboData { + idx: real_idx, + actions, + output, + })) + .await; + } + VialDynamic::DynamicVialKeyOverrideGet => { + warn!("DynamicEntryOp - DynamicVialKeyOverrideGet -- to be implemented"); + report.input_data.fill(0x00); + } + VialDynamic::DynamicVialKeyOverrideSet => { + warn!("DynamicEntryOp - DynamicVialKeyOverrideSet -- to be implemented"); + report.input_data.fill(0x00); + } + VialDynamic::Unhandled => { + warn!("DynamicEntryOp - Unhandled -- subcommand not recognized"); + report.input_data.fill(0x00); + } + } + } + _ => (), + } + } +} + +fn vial_combo(combos: &[Combo; COMBO_MAX_NUM], idx: usize) -> Option<(usize, &Combo)> { + combos + .iter() + .enumerate() + .filter(|(_, combo)| combo.actions.len() <= VIAL_COMBO_MAX_LENGTH) + .enumerate() + .find_map(|(i, combo)| (i == idx).then_some(combo)) +} + +fn vial_combo_mut(combos: &mut [Combo; COMBO_MAX_NUM], idx: usize) -> Option<(usize, &mut Combo)> { + combos + .iter_mut() + .enumerate() + .filter(|(_, combo)| combo.actions.len() <= VIAL_COMBO_MAX_LENGTH) + .enumerate() + .find_map(|(i, combo)| (i == idx).then_some(combo)) } fn get_position_from_offset( diff --git a/rmk/src/via/vial.rs b/rmk/src/via/vial.rs index 42d07128..63837dac 100644 --- a/rmk/src/via/vial.rs +++ b/rmk/src/via/vial.rs @@ -1,12 +1,9 @@ -use byteorder::{ByteOrder, LittleEndian}; -use defmt::debug; use num_enum::FromPrimitive; -use crate::usb::descriptor::ViaReport; - +/// Vial communication commands. Check [vial-qmk/quantum/vial.h`](https://github.com/vial-kb/vial-qmk/blob/20d61fcb373354dc17d6ecad8f8176be469743da/quantum/vial.h#L36) #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, FromPrimitive)] #[repr(u8)] -enum VialCommand { +pub(crate) enum VialCommand { GetKeyboardId = 0x00, GetSize = 0x01, GetKeyboardDef = 0x02, @@ -25,65 +22,22 @@ enum VialCommand { Unhandled = 0xFF, } -const VIAL_PROTOCOL_VERSION: u32 = 6; -const VIAL_EP_SIZE: usize = 32; -/// -/// Note: vial uses litte endian, while via uses big endian -pub(crate) fn process_vial( - report: &mut ViaReport, - vial_keyboard_Id: &[u8], - vial_keyboard_def: &[u8], -) { - // report.output_data[0] == 0xFE -> vial commands - let vial_command = VialCommand::from_primitive(report.output_data[1]); - match vial_command { - VialCommand::GetKeyboardId => { - debug!("Received Vial - GetKeyboardId"); - // Returns vial protocol version + vial keyboard id - LittleEndian::write_u32(&mut report.input_data[0..4], VIAL_PROTOCOL_VERSION); - report.input_data[4..12].clone_from_slice(vial_keyboard_Id); - } - VialCommand::GetSize => { - debug!("Received Vial - GetSize"); - LittleEndian::write_u32(&mut report.input_data[0..4], vial_keyboard_def.len() as u32); - } - VialCommand::GetKeyboardDef => { - debug!("Received Vial - GetKeyboardDefinition"); - let page = LittleEndian::read_u16(&report.output_data[2..4]) as usize; - let start = page * VIAL_EP_SIZE; - let mut end = start + VIAL_EP_SIZE; - if end < start || start >= vial_keyboard_def.len() { - return; - } - if end > vial_keyboard_def.len() { - end = vial_keyboard_def.len(); - } - vial_keyboard_def[start..end] - .iter() - .enumerate() - .for_each(|(i, v)| { - report.input_data[i] = *v; - }); - debug!( - "Vial return: page:{} start:{} end: {}, data: {:?}", - page, start, end, report.input_data - ); - } - VialCommand::GetUnlockStatus => { - debug!("Received Vial - GetUnlockStatus"); - // Reset all data to 0xFF(it's required!) - report.input_data.fill(0xFF); - // Unlocked - report.input_data[0] = 1; - // Unlock in progress - report.input_data[1] = 0; - } - VialCommand::QmkSettingsQuery => { - report.input_data.fill(0xFF); - } - VialCommand::DynamicEntryOp => { - report.input_data.fill(0x00); - } - _ => (), - } +/// Vial dynamic commands. Check [vial-qmk/quantum/vial.h`](https://github.com/vial-kb/vial-qmk/blob/20d61fcb373354dc17d6ecad8f8176be469743da/quantum/vial.h#L53) +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, FromPrimitive)] +#[repr(u8)] +pub(crate) enum VialDynamic { + DynamicVialGetNumberOfEntries = 0x00, + DynamicVialTapDanceGet = 0x01, + DynamicVialTapDanceSet = 0x02, + DynamicVialComboGet = 0x03, + DynamicVialComboSet = 0x04, + DynamicVialKeyOverrideGet = 0x05, + DynamicVialKeyOverrideSet = 0x06, + #[num_enum(default)] + Unhandled = 0xFF, } + +pub(crate) const VIAL_PROTOCOL_VERSION: u32 = 6; +pub(crate) const VIAL_EP_SIZE: usize = 32; + +pub(crate) const VIAL_COMBO_MAX_LENGTH: usize = 4;