From 33c8284237f3912734fb6626247226dff34aafbf Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Fri, 7 Aug 2026 23:52:34 -0700 Subject: [PATCH] assembly: enforce positional HRM triggers --- manifest.lock.json | 37 +- manifest.toml | 7 +- .../postimage | 1896 ++++++++++++++++ .../preimage | 1904 ++++++++++++++++ .../postimage.1 | 1903 ++++++++++++++++ .../preimage.1 | 1907 +++++++++++++++++ resolutions/rerere/INDEX.toml | 8 +- 7 files changed, 7633 insertions(+), 29 deletions(-) create mode 100644 resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/postimage create mode 100644 resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/preimage create mode 100644 resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/postimage.1 create mode 100644 resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/preimage.1 diff --git a/manifest.lock.json b/manifest.lock.json index dfad616..f36d771 100644 --- a/manifest.lock.json +++ b/manifest.lock.json @@ -7,12 +7,11 @@ "feat/combo-hold-output": "cce7d99c94d918e02442a15ead3a359f587fbaa1", "feat/consumer-keycodes": "1373e2533443e044305328df85336ab766ab03a9", "feat/maintenance-mode": "711fa96104fc9d96502ff54cf5dd4f144e743cb5", - "feat/morse-hold-trigger-on-release": "bed6738f6110df6732256109cd46712b533ce228", - "feat/morse-hold-trigger-positions": "a6f6a5734fffd5c1c802e88eeae561013991a628", + "feat/morse-hold-trigger-on-release": "e8d97930624e48a6a5119783ac6e1e9521987386", + "feat/morse-hold-trigger-positions": "a95895d9785e0cf3301afb168f68fcbd955fc979", "feat/morse-per-profile-prior-idle": "6b3ef8a8f36af168cf9cf9a890cf18f7b4618e21", "feat/morse-retro-tap": "4ada295c929a64ba62e9bffc8e97a552f82796ce", "feat/morse-tap-unless-interrupted": "e954947b9e6bb85c1abfdccdc7aa5be2427364eb", - "feat/morse-unilateral-mod-chording": "a2df0ec69daededb21a8ecf1540fa4d015797269", "feat/pointer-layer-scaling": "2adf2ab4492a7aa9fe8c74f5b9d87563ccc28bcb", "feat/pointing-drag-mode": "211af5703d7e398951159cd5598dd691d2e7035f", "feat/single-pass-storage-read": "fe61e8a28f79b4229fd1da17a8b7416450d0973b", @@ -30,13 +29,13 @@ } }, "build": { - "generated": "2026-08-08T02:51:55.873942646+00:00", + "generated": "2026-08-08T06:54:15.607054313+00:00", "base": "65df15775026bad1189139613ee3d338139bec3d", - "commit": "10abef3b40498bbc102363cb04566ac9260a75cc", - "pre_provenance_commit": "10abef3b40498bbc102363cb04566ac9260a75cc", - "tree": "51b1298cde1e1b5465bcaaf6696c31825853f5c3", - "built_tree": "51b1298cde1e1b5465bcaaf6696c31825853f5c3", - "previous_tree": "b7926f37647d9f6c56a264f0636a7a140f669fe2", + "commit": "ab8bbb537894aa047d39079de785335b8b5f8d35", + "pre_provenance_commit": "ab8bbb537894aa047d39079de785335b8b5f8d35", + "tree": "a140ed730dc0d993dcb2311c786c54cf954fec1a", + "built_tree": "a140ed730dc0d993dcb2311c786c54cf954fec1a", + "previous_tree": "51b1298cde1e1b5465bcaaf6696c31825853f5c3", "tree_changed": true, "conflicts": 14, "manifest_entries": [ @@ -111,19 +110,13 @@ "name": "feat/morse-hold-trigger-positions", "kind": "branch", "source": "fork:feat/morse-hold-trigger-positions", - "pin": "a6f6a5734fffd5c1c802e88eeae561013991a628" + "pin": "a95895d9785e0cf3301afb168f68fcbd955fc979" }, { "name": "feat/morse-hold-trigger-on-release", "kind": "branch", "source": "fork:feat/morse-hold-trigger-on-release", - "pin": "bed6738f6110df6732256109cd46712b533ce228" - }, - { - "name": "feat/morse-unilateral-mod-chording", - "kind": "branch", - "source": "fork:feat/morse-unilateral-mod-chording", - "pin": "a2df0ec69daededb21a8ecf1540fa4d015797269" + "pin": "e8d97930624e48a6a5119783ac6e1e9521987386" }, { "name": "feat/consumer-keycodes", @@ -265,24 +258,18 @@ }, { "name": "feat/morse-hold-trigger-positions", - "oid": "a6f6a5734fffd5c1c802e88eeae561013991a628", + "oid": "a95895d9785e0cf3301afb168f68fcbd955fc979", "status": "merged", "conflicted": true, "resolution": "rerere" }, { "name": "feat/morse-hold-trigger-on-release", - "oid": "bed6738f6110df6732256109cd46712b533ce228", + "oid": "e8d97930624e48a6a5119783ac6e1e9521987386", "status": "merged", "conflicted": true, "resolution": "rerere" }, - { - "name": "feat/morse-unilateral-mod-chording", - "oid": "a2df0ec69daededb21a8ecf1540fa4d015797269", - "status": "merged", - "conflicted": false - }, { "name": "feat/consumer-keycodes", "oid": "1373e2533443e044305328df85336ab766ab03a9", diff --git a/manifest.toml b/manifest.toml index 58c5e0f..aaa1f97 100644 --- a/manifest.toml +++ b/manifest.toml @@ -63,9 +63,6 @@ branch = "fork:feat/morse-hold-trigger-positions" [[entry]] branch = "fork:feat/morse-hold-trigger-on-release" -[[entry]] -branch = "fork:feat/morse-unilateral-mod-chording" - [[entry]] branch = "fork:feat/consumer-keycodes" @@ -109,3 +106,7 @@ reason = "held modifier combinations landed upstream in f91b22a0" [[exclude]] branch = "fork:feat/rynk" reason = "stale copy of the rynk stack; upstream rebased and merged its own feat/rynk into main at 9448f7a1, so the base carries it now" + +[[exclude]] +branch = "fork:feat/morse-unilateral-mod-chording" +reason = "superseded by strict positional hold triggers and opt-in hold-trigger-on-release; unconditional modifier exemption violates bilateral HRM safety" diff --git a/resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/postimage b/resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/postimage new file mode 100644 index 0000000..cd83d9b --- /dev/null +++ b/resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/postimage @@ -0,0 +1,1896 @@ +use std::collections::HashMap; +use std::path::Path; + +use config::{Config, File, FileFormat}; +use serde::{Deserialize, de}; +use serde_inline_default::serde_inline_default; + +/// Event channel default configuration +const EVENT_DEFAULT_CONFIG: &str = include_str!("default_config/event_default.toml"); + +pub(crate) mod chip; +pub(crate) mod communication; +pub mod resolved; +#[rustfmt::skip] +pub mod usb_interrupt_map; +pub(crate) mod behavior; +pub(crate) mod board; +pub(crate) mod dfu; +pub(crate) mod display; +pub(crate) mod host; +pub(crate) mod keycode_alias; +pub(crate) mod keymap; +pub mod layout; +pub use layout::{STOCK_WIDTHS, layout_blob_from_toml, layout_info_from_toml}; +pub(crate) mod light; +pub(crate) mod storage; + +/// Protocol-level capacity ceilings for wire-format Vec sizes. +/// +/// These define the maximum values any firmware may use for protocol +/// Vec capacities (`COMBO_SIZE`, `MORSE_SIZE`, etc.). The host tool compiles +/// against these as upper bounds. Any firmware with `rynk` enabled +/// must satisfy `value <= ceiling` at compile time. +/// +/// Constant names mirror the generated constants with a `MAX_` prefix: +/// `COMBO_SIZE` is bounded by `MAX_COMBO_SIZE`, etc. +pub mod protocol_limits { + /// Max keys in a combo trigger — ceiling for `COMBO_SIZE` + pub const MAX_COMBO_SIZE: usize = 16; + /// Max pattern entries per morse key — ceiling for `MORSE_SIZE` + pub const MAX_MORSE_SIZE: usize = 32; + /// Max bytes per macro data chunk — ceiling for `MACRO_DATA_SIZE` + pub const MAX_MACRO_DATA_SIZE: usize = 256; + /// Max key positions in an unlock challenge. + pub const MAX_UNLOCK_KEYS_SIZE: usize = 4; +} + +pub(crate) fn validate_unlock_keys( + section: &str, + unlock_keys: &[[u8; 2]], + layout: Option<&LayoutTomlConfig>, +) -> Result<(), String> { + if unlock_keys.len() > protocol_limits::MAX_UNLOCK_KEYS_SIZE { + return Err(format!( + "{section}.unlock_keys has {} entries, the max is {}", + unlock_keys.len(), + protocol_limits::MAX_UNLOCK_KEYS_SIZE + )); + } + + if let Some(layout) = layout { + for key in unlock_keys { + let (row, col) = (key[0], key[1]); + if row >= layout.rows || col >= layout.cols { + return Err(format!( + "{section}.unlock_keys position ({row}, {col}) is outside the {}x{} matrix", + layout.rows, layout.cols + )); + } + } + } + + Ok(()) +} + +/// Configurations for RMK keyboard. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub struct KeyboardTomlConfig { + /// Basic keyboard info + keyboard: Option, + /// Matrix of the keyboard, only for non-split keyboards + matrix: Option, + // Aliases for key maps + aliases: Option>, + /// Keymap config: layer count and the per-layer key actions (`[[keymap.layer]]`). + keymap: Option, + /// Layout config: the physical key arrangement (`map`) plus the rendered layout. + /// For split keyboards, the total row/col is defined in this section. + layout: Option, + /// Topology-aware lighting. Key geometry is always derived from + /// `[layout].map`; emitters add semantic identity and electrical routing. + lighting: Option, + /// Behavior config + behavior: Option, + /// Light config + light: Option, + /// Storage config + storage: Option, + /// DFU partition config (embassy-boot) + dfu: Option, + /// Ble config + pub(crate) ble: Option, + /// Chip-specific configs (e.g., [chip.nrf52840]) + chip: Option>, + /// Dependency config + dependency: Option, + /// Split config + split: Option, + /// Input device config + input_device: Option, + /// Display config + display: Option, + /// Output Pin config + output: Option>, + /// Set host configurations + pub(crate) host: Option, + /// RMK config constants + #[serde(default)] + pub(crate) rmk: RmkConstantsConfig, + /// Event channel configuration + /// Default values are loaded from event_default.toml in new_from_toml_path() + /// build.rs also loads event defaults via new_from_toml_path_with_event_defaults() + #[serde(default)] + pub(crate) event: EventConfig, + /// Whether the user explicitly set a [storage] section in keyboard.toml. + #[serde(skip)] + pub(crate) storage_user_set: bool, +} + +impl KeyboardTomlConfig { + fn parse_from_toml_path>(config_toml_path: P, chip_default_config: Option<&str>) -> Self { + let path = config_toml_path.as_ref(); + let path_str = path + .to_str() + .unwrap_or_else(|| panic!("Config path is not valid UTF-8: {:?}", path)); + + let mut builder = Config::builder().add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)); + if let Some(default_config) = chip_default_config { + builder = builder.add_source(File::from_str(default_config, FileFormat::Toml)); + } + builder + .add_source(File::with_name(path_str)) + .build() + .unwrap_or_else(|e| panic!("Parse {:?} error: {}", path, e)) + .try_deserialize() + .unwrap_or_else(|e| panic!("Deserialize {:?} error: {}", path, e)) + } + + /// Load keyboard.toml with event defaults only. + /// + /// This is used in build.rs where we only need [rmk] and [event] constants, + /// and should not require `[keyboard.board]`/`[keyboard.chip]`. + pub fn new_from_toml_path_with_event_defaults>(config_toml_path: P) -> Self { + let mut config = Self::parse_from_toml_path(config_toml_path, None); + config.storage_user_set = config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + config.auto_calculate_parameters(); + config + } + + pub fn new_from_toml_path>(config_toml_path: P) -> Self { + let path = config_toml_path.as_ref(); + + // First pass: load user config with event defaults to get chip model. + // This allows user's keyboard.toml to omit [event] section. + let user_config = Self::parse_from_toml_path(path, None); + + let default_config_str = user_config + .get_chip_model() + .and_then(|chip| chip.get_default_config_str()) + .unwrap_or_else(|e| panic!("❌ keyboard.toml error: {e}")); + + // Second pass: load with all three config sources + // Config priority (later sources override earlier ones): + // 1. Event default config (lowest priority) + // 2. Chip-specific default config + // 3. User config (highest priority) + let mut config = Self::parse_from_toml_path(path, Some(default_config_str)); + config.storage_user_set = user_config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + + config.auto_calculate_parameters(); + + config + } + + /// Auto calculate some parameters in toml: + /// - Update morse_max_num to fit all configured morses + /// - Update max_patterns_per_key to fit the max number of configured (pattern, action) pairs per morse key + /// - Update peripheral number based on the number of split boards + /// - TODO: Update controller number based on the number of split boards + pub(crate) fn auto_calculate_parameters(&mut self) { + // Update the number of peripherals + if let Some(split) = &self.split + && split.peripheral.len() > self.rmk.split_peripherals_num + { + // eprintln!( + // "The number of split peripherals is updated to {} from {}", + // split.peripheral.len(), + // self.rmk.split_peripherals_num + // ); + self.rmk.split_peripherals_num = split.peripheral.len(); + } + + if let Some(behavior) = &self.behavior { + // Update the max_patterns_per_key + if let Some(morse) = &behavior.morse + && let Some(morses) = &morse.morses + { + let mut max_required_patterns = self.rmk.max_patterns_per_key; + + for morse in morses { + let tap_actions_len = morse.tap_actions.as_ref().map(|v| v.len()).unwrap_or(0); + let hold_actions_len = morse.hold_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + let n = tap_actions_len.max(hold_actions_len); + if n > 15 { + panic!("The number of taps per morse is too large, the max number of taps is 15, got {n}"); + } + + let morse_actions_len = morse.morse_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + max_required_patterns = + max_required_patterns.max(tap_actions_len + hold_actions_len + morse_actions_len); + } + self.rmk.max_patterns_per_key = max_required_patterns; + + // Update the morse_max_num + self.rmk.morse_max_num = self.rmk.morse_max_num.max(morses.len()); + } + + // Update hold_trigger_key_position_max_num to fit every configured position + if let Some(morse) = &behavior.morse { + let regions = self.layout.as_ref().and_then(|layout| layout.regions.as_ref()); + let configured_len = |positions: Option<&Vec<[u8; 2]>>, names: Option<&Vec>| { + positions.map(Vec::len).unwrap_or(0) + + names + .into_iter() + .flatten() + .filter_map(|name| regions.and_then(|r| r.get(name))) + .map(Vec::len) + .sum::() + }; + let mut total = configured_len( + morse.hold_trigger_key_positions.as_ref(), + morse.hold_trigger_regions.as_ref(), + ); + if let Some(profiles) = &morse.profiles { + for profile in profiles.values() { + total += configured_len( + profile.hold_trigger_key_positions.as_ref(), + profile.hold_trigger_regions.as_ref(), + ); + } + } + self.rmk.hold_trigger_key_position_max_num = self.rmk.hold_trigger_key_position_max_num.max(total); + } + + let auto_mouse_layers = behavior.auto_mouse_layer.as_deref().unwrap_or_default(); + self.rmk.auto_mouse_layer_max_num.get_or_insert(auto_mouse_layers.len()); + + let mouse_layer_scales = behavior.mouse_layer_scale.as_deref().unwrap_or_default(); + self.rmk + .mouse_layer_scale_max_num + .get_or_insert(mouse_layer_scales.len()); + } else { + self.rmk.auto_mouse_layer_max_num.get_or_insert(0); + self.rmk.mouse_layer_scale_max_num.get_or_insert(0); + } + } +} + +/// Keyboard constants configuration for performance and hardware limits +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct RmkConstantsConfig { + /// Mouse key interval (ms) - controls mouse movement speed + #[serde_inline_default(20)] + pub mouse_key_interval: u16, + /// Mouse wheel interval (ms) - controls scrolling speed + #[serde_inline_default(80)] + pub mouse_wheel_interval: u16, + /// Maximum number of combos keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_combo_max_num")] + pub combo_max_num: usize, + /// Maximum number of keys pressed simultaneously in a combo + #[serde_inline_default(4)] + pub combo_max_length: usize, + /// Maximum number of forks for conditional key actions + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_fork_max_num")] + pub fork_max_num: usize, + /// Maximum number of morses keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_morse_max_num")] + pub morse_max_num: usize, + /// Capacity of the morse profile table (named profiles in `[behavior.morse.profiles]`) + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_morse_profile_max_num")] + pub morse_profile_max_num: usize, + /// Capacity of the hold trigger position table, counting every `hold_trigger_key_positions` + /// entry across all profiles. Raised automatically to fit `keyboard.toml`; the default + /// covers a typical home row mod setup at 3 bytes per entry. + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_hold_trigger_key_position_max_num")] + pub hold_trigger_key_position_max_num: usize, + /// Maximum number of patterns a morse key can handle + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_max_patterns_per_key")] + pub max_patterns_per_key: usize, + /// Macro space size in bytes for storing sequences + #[serde_inline_default(256)] + pub macro_space_size: usize, + /// Default debounce time in ms + #[serde_inline_default(20)] + pub debounce_time: u16, + /// Report channel size + #[serde_inline_default(16)] + pub report_channel_size: usize, + /// Vial channel size + #[serde_inline_default(4)] + pub vial_channel_size: usize, + /// Flash channel size + #[serde_inline_default(4)] + pub flash_channel_size: usize, + /// The number of the split peripherals + #[serde_inline_default(0)] + pub split_peripherals_num: usize, + /// The number of available BLE profiles + #[serde_inline_default(3)] + pub ble_profiles_num: usize, + /// BLE Split Central sleep timeout in seconds (0 = disabled) + #[serde_inline_default(0)] + pub split_central_sleep_timeout_seconds: u32, + /// Maximum BLE peripheral latency on external power, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_powered: u16, + /// Maximum BLE peripheral latency on battery, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_battery: u16, + /// Maximum macro data chunk size for protocol transfers (bytes). + /// Smaller values reduce firmware RAM usage but require more round-trips. + #[serde_inline_default(64)] + pub protocol_macro_chunk_size: usize, + /// Maximum number of auto mouse layer entries; auto-derived from `[[behavior.auto_mouse_layer]]` if unset. + #[serde(default)] + pub auto_mouse_layer_max_num: Option, + /// Maximum number of per-layer mouse scaling entries; auto-derived from `[[behavior.mouse_layer_scale]]` if unset. + #[serde(default)] + pub mouse_layer_scale_max_num: Option, + /// Exact RAM of each Rynk RX/TX frame buffer (bytes), payload capacity and bulk counts derive from it. + /// Default 488 fills exactly two BLE notifications. + #[serde_inline_default(488)] + pub rynk_buffer_size: usize, +} + +fn check_combo_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "combo_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_morse_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "morse_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +/// The profile index is a `u8` in `KeyAction::TapHold` and an index with no +/// table entry means "use the default profile", so the table may never cover +/// the full `u8` range: capacity ≤ 255 keeps at least one index always vacant. +fn check_hold_trigger_key_position_max_num<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = usize::deserialize(deserializer)?; + if value > 255 { + panic!( + "❌ Parse `keyboard.toml` error: hold_trigger_key_position_max_num must be between 0 and 255, got {value}" + ); + } + Ok(value) +} + +fn check_morse_profile_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > 255 { + panic!("❌ Parse `keyboard.toml` error: morse_profile_max_num must be between 0 and 255, got {value}"); + } + Ok(value) +} + +fn check_max_patterns_per_key<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if !(4..=65536).contains(&value) { + return Err(de::Error::custom(format!( + "max_patterns_per_key must be between 4 and 65536, got {value}" + ))); + } + Ok(value) +} + +fn check_fork_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "fork_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_split_central_max_latency<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value >= 500 { + return Err(de::Error::custom(format!( + "split_central_max_latency must be between 0 and 499, got {value}" + ))); + } + Ok(value) +} + +/// This separate Default impl is needed when `[rmk]` section is not set in keyboard.toml +impl Default for RmkConstantsConfig { + fn default() -> Self { + Self { + mouse_key_interval: 20, + mouse_wheel_interval: 80, + combo_max_num: 8, + combo_max_length: 4, + fork_max_num: 8, + morse_max_num: 8, + morse_profile_max_num: 16, + hold_trigger_key_position_max_num: 16, + max_patterns_per_key: 8, + macro_space_size: 256, + debounce_time: 20, + report_channel_size: 16, + vial_channel_size: 4, + flash_channel_size: 4, + split_peripherals_num: 0, + ble_profiles_num: 3, + split_central_sleep_timeout_seconds: 0, + split_central_max_latency_powered: 30, + split_central_max_latency_battery: 30, + protocol_macro_chunk_size: 64, + auto_mouse_layer_max_num: None, + mouse_layer_scale_max_num: None, + rynk_buffer_size: 488, + } + } +} + +/// Event channel configuration for a single event type +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct EventChannelConfig { + /// Channel buffer size + pub channel_size: usize, + /// Number of publishers + pub pubs: usize, + /// Number of subscribers + pub subs: usize, +} + +impl Default for EventChannelConfig { + fn default() -> Self { + Self { + channel_size: 1, + pubs: 1, + subs: 1, + } + } +} + +/// Macro to define EventConfig and related code without repetition +macro_rules! define_event_config { + ($($field:ident),* $(,)?) => { + /// Event configuration for all controller events + /// Default values are loaded from event_default.toml + #[derive(Clone, Debug, Deserialize)] + #[serde(deny_unknown_fields, default)] + pub(crate) struct EventConfig { + $(pub $field: EventChannelConfig,)* + } + + /// Cached default EventConfig parsed from event_default.toml + static EVENT_CONFIG_DEFAULTS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + #[derive(Deserialize)] + struct Inner { $($field: EventChannelConfig,)* } + #[derive(Deserialize)] + struct Wrapper { event: Inner } + let w: Wrapper = toml::from_str(EVENT_DEFAULT_CONFIG).expect("Failed to parse event_default.toml"); + EventConfig { $($field: w.event.$field,)* } + }); + + impl Default for EventConfig { + fn default() -> Self { + EVENT_CONFIG_DEFAULTS.clone() + } + } + }; +} + +define_event_config!( + // Connection events + connection_status_change, + // Input events + modifier, + keyboard, + // Keyboard state events + layer_change, + wpm_update, + led_indicator, + sleep_state, + lighting_changed, + // Power events + battery_status, + battery_adc, + charging_state, + // Pointing device events + pointing, + // Split events + peripheral_connected, + central_connected, + peripheral_battery, + clear_peer, + // DFU events + dfu_status, + // Action events + action, +); + +/// The `[layout]` section: the physical key arrangement plus the rendered layout. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayoutTomlConfig { + pub rows: u8, + pub cols: u8, + /// The physical arrangement: an ordered map of `(row,col)` positions with + /// optional hand, shape (`@2u`), gaps (`[1.5]`), row-steps (`[y=]`), and + /// encoders (`(e,0)`). Its order also defines the order of `[[keymap.layer]]`. + pub map: Option, + /// Reusable named sets of matrix coordinates for positional behaviors. + pub regions: Option>>, + // Rendered-layout fields. + pub default_variant: Option, + pub shapes: Option>, + pub variant: Option>, +} + +/// A named shape from `[layout.shapes]`. Every field optional; widths/ +/// heights default to 1u, nudges/rotation to 0, and `w2/h2/x2/y2` are an +/// optional second rectangle for L-shaped caps. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ShapeToml { + pub w: Option, + pub h: Option, + pub x: Option, + pub y: Option, + pub r: Option, + pub w2: Option, + pub h2: Option, + pub x2: Option, + pub y2: Option, +} + +/// One `[[layout.variant]]` render overlay: reshape some keys, hide others. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct VariantToml { + pub name: String, + pub shapes: Option>, + pub hidden: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingTomlConfig { + #[serde(default = "default_topology_revision")] + pub topology_revision: u32, + #[serde(default, rename = "zone")] + pub zones: Vec, + #[serde(default, rename = "output")] + pub outputs: Vec, + #[serde(default, rename = "emitter")] + pub emitters: Vec, + #[serde(default, rename = "layer_scene")] + pub layer_scenes: Vec, + #[serde(default, rename = "conditional_scene")] + pub conditional_scenes: Vec, + pub controls: Option, + pub background: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingControlsTomlConfig { + pub output_toggle_user_action: Option, + pub output_mode_cycle_user_action: Option, + /// Layers that wake lighting while held. A list, since any set of layers + /// may wake it; the host can replace the resolved mask at runtime. + pub wake_layers: Option>, + #[serde(default)] + pub initial_output_mode: LightingOutputModeToml, + #[serde(default)] + pub powered_only_scope: LightingPoweredOnlyScopeToml, + pub output_mode_indicator: Option, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingPoweredOnlyScopeToml { + #[default] + Authority, + Local, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingOutputModeToml { + #[default] + AlwaysOn, + AlwaysOff, + PoweredOnly, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputModeIndicatorTomlConfig { + pub target: LightingTargetTomlConfig, + pub always_on: LightingEffectTomlConfig, + pub always_off: LightingEffectTomlConfig, + pub powered_only: LightingEffectTomlConfig, +} + +fn default_topology_revision() -> u32 { + 1 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingZoneTomlConfig { + pub id: u8, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputTomlConfig { + pub node: u8, + pub id: u8, + pub pixel_count: u16, + pub capabilities: Vec, + #[serde(default)] + pub sparse: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingEmitterTomlConfig { + pub id: u16, + pub key: Option<[u8; 2]>, + pub position: Option<[f32; 3]>, + #[serde(default)] + pub zones: Vec, + pub node: u8, + pub output: u8, + pub physical_index: u16, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerSceneTomlConfig { + pub layer: u8, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingConditionalSceneTomlConfig { + pub layer: Option, + pub battery: Option, + pub output_mode: Option, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerConditionTomlConfig { + pub layer: u8, + #[serde(default = "default_true")] + pub active: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBatteryConditionTomlConfig { + pub node: u8, + pub min_level: Option, + pub max_level: Option, + #[serde(default)] + pub charge: LightingChargeConditionToml, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingChargeConditionToml { + #[default] + Any, + Charging, + Discharging, + Unknown, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingSceneCellTomlConfig { + pub target: LightingTargetTomlConfig, + pub effect: LightingEffectTomlConfig, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub(crate) enum LightingTargetTomlConfig { + Led { led: u16 }, + Key { key: [u8; 2] }, + Zone { zone: u8 }, + All { all: bool }, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub(crate) enum LightingEffectTomlConfig { + Solid { + color: [u8; 3], + }, + Blink { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + duty_percent: u8, + }, + Breathe { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + #[serde(default = "default_breathe_step_ms")] + step_ms: u16, + }, +} + +fn default_breathe_step_ms() -> u16 { + 16 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBackgroundTomlConfig { + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default)] + pub hue: u8, + #[serde(default)] + pub saturation: u8, + #[serde(default = "default_background_value")] + pub value: u8, + #[serde(default = "default_background_speed")] + pub speed: u8, + #[serde(default)] + pub mode: LightingBackgroundModeToml, +} + +fn default_background_value() -> u8 { + 32 +} + +fn default_background_speed() -> u8 { + 128 +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingBackgroundModeToml { + #[default] + Solid, + Breathe, +} + +/// The `[keymap]` section: layer count plus the per-layer key actions. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct KeymapTomlConfig { + /// Total layer count. Optional — defaults to the number of `[[keymap.layer]]` + /// blocks; set it larger to reserve extra empty layers (e.g. for Vial/Rynk). + pub layers: Option, + /// Per-layer key actions: `[[keymap.layer]]`. + #[serde(default)] + pub layer: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayerTomlConfig { + pub name: Option, + pub keys: String, + pub encoders: Option>, +} + +/// Configurations for keyboard info +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct KeyboardInfo { + /// Keyboard name + pub name: String, + /// Vender id + pub vendor_id: u16, + /// Product id + pub product_id: u16, + /// Manufacturer + pub manufacturer: Option, + /// Product name, if not set, it will use `name` as default + pub product_name: Option, + /// Serial number + pub serial_number: Option, + /// Board name(if a supported board is used) + pub board: Option, + /// Chip model + pub chip: Option, + /// enable usb + pub usb_enable: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub enum MatrixType { + #[default] + #[serde(rename = "normal")] + Normal, + #[serde(rename = "direct_pin")] + DirectPin, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DebouncerType { + #[default] + Default, + Fast, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MatrixConfig { + #[serde(default)] + pub matrix_type: MatrixType, + pub row_pins: Option>, + pub col_pins: Option>, + pub direct_pins: Option>>, + #[serde(default = "default_true")] + pub direct_pin_low_active: bool, + #[serde(default = "default_false")] + pub row2col: bool, + #[serde(default)] + pub debouncer: DebouncerType, + pub bootmagic: Option<(u8, u8)>, +} + +/// Config for storage +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct StorageConfig { + /// Start address of local storage, MUST BE start of a sector. + /// If start_addr is set to 0(this is the default value), the last `num_sectors` sectors will be used. + pub start_addr: Option, + // Number of sectors used for storage, >= 2. + pub num_sectors: Option, + #[serde(default = "default_true")] + pub enabled: bool, + // Clear on the storage at reboot, set this to true if you want to reset the keymap + pub clear_storage: Option, + // Clear on the layout at reboot, set this to true if you want to reset the layout + pub clear_layout: Option, +} + +/// Config for DFU partition layout (embassy-boot). +/// +/// These values must match the bootloader's `memory.x` / linker script. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct DfuTomlConfig { + /// Offset of the boot state partition + pub state_offset: Option, + /// Size of the boot state partition + pub state_size: Option, + /// Offset of the DFU download partition + pub dfu_offset: Option, + /// Size of the DFU download partition + pub dfu_size: Option, + /// Flash page size in bytes (e.g. 4096 for RP2040). + /// Used with `flash_size` to auto-calculate partition addresses. + pub page_size: Option, + /// Total flash size in bytes. When set, DFU partition addresses are + /// calculated automatically using the rmk-boot formula. + /// Defaults to 2 MB (2097152) when omitted. + pub flash_size: Option, + /// Optional DFU activity LED pin, e.g. `"PIN_16"`. When set, the LED + /// is lit while a DFU download is in progress. + pub led: Option, + /// Unlock keys for DFU lock (optional) + pub unlock_keys: Option>, +} + +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BleConfig { + pub enabled: bool, + pub battery_adc_pin: Option, + pub charge_state: Option, + pub charge_led: Option, + pub adc_divider_measured: Option, + pub adc_divider_total: Option, + pub default_tx_power: Option, + pub use_2m_phy: Option, + pub passkey_entry: Option, + pub passkey_entry_timeout: Option, +} + +/// Default passkey entry timeout in seconds. +pub const DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 120; + +/// Minimum passkey entry timeout in seconds. +pub const MIN_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 30; + +/// nRF52840 DCDC REG0 output voltage +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum DcdcReg0Voltage { + #[serde(rename = "3V3")] + V3_3, + #[serde(rename = "1V8")] + V1_8, +} + +/// Config for chip-specific settings +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ChipConfig { + /// DCDC regulator 0 enabled (for nrf52840) + pub dcdc_reg0: Option, + /// DCDC regulator 1 enabled (for nrf52840, nrf52833) + pub dcdc_reg1: Option, + /// DCDC regulator 0 voltage (for nrf52840) + pub dcdc_reg0_voltage: Option, +} + +/// Config for lights +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LightConfig { + pub capslock: Option, + pub scrolllock: Option, + pub numslock: Option, +} + +/// Config for a single pin +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PinConfig { + pub pin: String, + pub low_active: bool, +} + +/// Configurations for dependencies +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DependencyConfig { + /// Enable defmt log or not + #[serde(default = "default_true")] + pub defmt_log: bool, +} + +impl Default for DependencyConfig { + fn default() -> Self { + Self { defmt_log: true } + } +} + +/// Intermediate resolved keymap grid (rows/cols/layers + per-layer actions). +/// Built once by `get_keymap_config` and unpacked into `Keymap`; never (de)serialized. +pub(crate) struct KeymapConfig { + pub rows: u8, + pub cols: u8, + pub layers: u8, + pub keymap: Vec>>, + pub encoder_map: Vec>, // Empty if there are no encoders or not configured +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KeyInfo { + pub hand: char, // 'L' or 'R' or other chars +} + +/// Configurations for actions behavior +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct BehaviorConfig { + pub tri_layer: Option, + pub one_shot: Option, + pub one_shot_modifiers: Option, + pub combo: Option, + #[serde(alias = "macro")] + pub macros: Option, + pub fork: Option, + pub morse: Option, + pub auto_mouse_layer: Option>, + pub mouse_layer_scale: Option>, +} + +/// Per-layer mouse movement and scroll scaling. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MouseLayerScaleConfig { + pub layer: u8, + pub r#move: Option<[u16; 2]>, + pub scroll: Option<[u16; 2]>, +} + +/// Configurations for auto mouse layer +/// +/// When motion is detected from a pointing device (e.g. PMW3610), the +/// specified `target_layer` is activated. The layer stays active until +/// `timeout` has elapsed without further motion, then it is deactivated. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct AutoMouseLayerConfig { + /// Pointing device id this entry applies to. When omitted, the entry acts as + /// a fallback for events whose `device_id` matches no other entry. + pub device_id: Option, + /// Layer index to activate on cursor motion + pub target_layer: u8, + /// Idle time after the last cursor motion before the layer is deactivated + /// (e.g. `"500ms"` or `"2s"`). + pub timeout: Option, + /// Minimum absolute axis delta required to be considered as motion. + /// Defaults to `1` (any motion). Helpful to filter out sensor noise. + pub threshold: Option, + /// When `true`, non-mouse key presses deactivate `target_layer` immediately (mouse HID keys and `extra_mouse_keys` excepted). + /// Macro-emitted keycodes, `Again`/`Repeat`, and `GraveEscape` cannot be classified and never deactivate the layer. + pub deactivate_on_key: Option, + /// Extra keycodes (e.g. modifiers) that do not trigger deactivation when `deactivate_on_key` is set. + /// Modifier keycodes listed here also exempt modifier-only actions containing them. + pub extra_mouse_keys: Option>, + /// When `true`, key presses that do NOT deactivate `target_layer` extend the timeout deadline + /// (i.e. reset it to now + `timeout`) at the moment the key's action resolves. + pub reset_timeout_on_key: Option, +} + +/// Per Key configurations profiles for morse, tap-hold, etc. +/// overrides the defaults given in TapHoldConfig +#[derive(Clone, Debug, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseProfile { + pub enable_flow_tap: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + /// overrides the global `[behavior.morse] prior_idle_time` for keys using this profile + pub prior_idle_time: Option, + + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves this tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, +} + +/// Configurations for tri layer +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct TriLayerConfig { + pub upper: u8, + pub lower: u8, + pub adjust: u8, +} + +/// Configurations for oneshot modifiers/layers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OneShotConfig { + pub timeout: Option, +} + +/// Configurations for oneshot modifiers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OneShotModifiersConfig { + pub activate_on_keypress: Option, + pub quick_release: Option, +} + +/// Configurations for combos +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CombosConfig { + #[serde(default)] + pub combos: Vec, + pub timeout: Option, + pub prior_idle_time: Option, +} + +/// Configurations for combo +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ComboConfig { + pub actions: Vec, + pub output: String, + pub layer: Option, +} + +/// Configurations for macros +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacrosConfig { + pub macros: Vec, +} + +/// Configurations for macro +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacroConfig { + pub operations: Vec, +} + +/// Macro operations (TOML deserialization type — resolved equivalent is in `resolved::behavior`) +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "operation", rename_all = "lowercase")] +pub(crate) enum MacroOperation { + Tap { keycode: String }, + Down { keycode: String }, + Up { keycode: String }, + Delay { duration: DurationMillis }, + Text { text: String }, +} + +/// Configurations for forks +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForksConfig { + pub forks: Vec, +} + +/// Configurations for fork +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForkConfig { + pub trigger: String, + pub negative_output: String, + pub positive_output: String, + pub match_any: Option, + pub match_none: Option, + pub kept_modifiers: Option, + pub bindable: Option, +} + +/// Configurations for morse keys +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorsesConfig { + pub enable_flow_tap: Option, //default: false + /// used in permissive_hold mode + pub prior_idle_time: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, + + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves the tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, + /// these can be used to overrides the defaults given above + pub profiles: Option>, + + /// the definition of morse / tap dance keys + pub morses: Option>, +} + +/// Configurations for morse +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseConfig { + // name of morse profile (to address BehaviorConfig::morse.profiles[self.profile]) + pub profile: Option, + + pub tap: Option, + pub hold: Option, + pub hold_after_tap: Option, + pub double_tap: Option, + /// Array of tap actions for each tap count (0-indexed) + pub tap_actions: Option>, + /// Array of hold actions for each tap count (0-indexed) + pub hold_actions: Option>, + /// Array of morse patter->action pairs count (0-indexed) + pub morse_actions: Option>, +} + +/// Configurations for morse action pairs +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseActionPair { + pub pattern: String, // for example morse code of "B": "-..." or "_..." or "1000" + pub action: String, // "B" +} + +/// Split connection transport +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SplitConnection { + #[default] + Ble, + Serial, +} + +/// Configurations for split keyboards +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitConfig { + pub connection: SplitConnection, + pub central: SplitBoardConfig, + pub peripheral: Vec, +} + +/// Configurations for each split board +/// +/// The transport field must match `split.connection`: `serial` is required for +/// serial splits and forbidden for BLE splits; `ble_addr` is optional for BLE +/// splits (dongle setups omit it) and forbidden for serial splits. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitBoardConfig { + /// Row number of the split board + pub rows: usize, + /// Col number of the split board + pub cols: usize, + /// Row offset of the split board + pub row_offset: usize, + /// Col offset of the split board + pub col_offset: usize, + /// Ble address + pub ble_addr: Option<[u8; 6]>, + /// Serial config, the vector length should be 1 for peripheral + pub serial: Option>, + /// Matrix config for the split + pub matrix: MatrixConfig, + /// Input device config for the split + pub input_device: Option, + /// Display config for the split board + pub display: Option, + /// Battery ADC pin for this split board + pub battery_adc_pin: Option, + /// ADC divider measured value for battery + pub adc_divider_measured: Option, + /// ADC divider total value for battery + pub adc_divider_total: Option, + /// Output Pin config for the split + pub output: Option>, + /// Path to the peripheral firmware binary for automatic dfu_split update. + /// Relative to the project's `Cargo.toml`. When set, the generated code + /// includes the binary with `include_bytes!` and registers it via + /// [`set_firmware_update_data`](crate::set_firmware_update_data). + pub firmware: Option, + /// DFU update policy for this peripheral. "MatchHash" (default) only + /// flashes when the firmware differs; "force" always flashes. + pub update_policy: Option, +} + +/// Serial port config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SerialConfig { + pub instance: String, + pub tx_pin: String, + pub rx_pin: String, +} + +/// Duration in milliseconds +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct DurationMillis(#[serde(deserialize_with = "parse_duration_millis")] pub u64); + +const fn default_true() -> bool { + true +} + +const fn default_false() -> bool { + false +} + +const fn default_pointing_report_hz() -> u16 { + 125 +} + +fn parse_duration_millis<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { + let input: String = de::Deserialize::deserialize(deserializer)?; + let num = input.trim_end_matches(|c: char| !c.is_numeric()); + let unit = &input[num.len()..]; + let num: u64 = num.parse().map_err(|_| { + de::Error::custom(format!( + "Invalid number \"{num}\" in duration: number part must be a u64" + )) + })?; + + match unit { + "s" => Ok(num * 1000), + "ms" => Ok(num), + other => Err(de::Error::custom(format!( + "Invalid duration unit \"{other}\": unit part must be either \"s\" or \"ms\"" + ))), + } +} + +/// Configuration for host tools +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct HostConfig { + /// Whether Vial is enabled + #[serde_inline_default(true)] + pub vial_enabled: bool, + /// Whether the RMK-native Rynk protocol is enabled. Mutually exclusive + /// with `vial_enabled` (the underlying Cargo features conflict). + #[serde_inline_default(false)] + pub rynk_enabled: bool, + /// Physical keys (row, col) held simultaneously to unlock (optional). + /// Shared by the Vial lock and the Rynk lock gate. + pub unlock_keys: Option>, + /// Start (and stay) unlocked, bypassing the unlock-key combo (default: + /// false). Renamed from `vial_insecure`; the old name still parses. + #[serde(alias = "vial_insecure")] + #[serde_inline_default(false)] + pub insecure: bool, + /// Move the Rynk config-write tier (`SetKeyAction`, `SetMacro`, …) into the + /// locked set, so writes also require unlock (default: false). + #[serde_inline_default(false)] + pub write_requires_unlock: bool, + /// Require the Rynk physical-presence unlock before entering either the + /// central or a split peripheral bootloader (default: true). + #[serde_inline_default(true)] + pub bootloader_requires_unlock: bool, +} + +impl Default for HostConfig { + fn default() -> Self { + Self { + vial_enabled: true, + rynk_enabled: false, + unlock_keys: None, + insecure: false, + write_requires_unlock: false, + bootloader_requires_unlock: true, + } + } +} + +/// Configurations for input devices +/// +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InputDeviceConfig { + pub encoder: Option>, + pub pointing: Option>, + pub joystick: Option>, + pub pmw3610: Option>, + pub pmw33xx: Option>, + pub iqs5xx: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct JoystickConfig { + // Name of the joystick + pub name: String, + /// Device id used to match this joystick with its JoystickProcessor. + /// If omitted, ids are assigned sequentially starting from 0. + pub id: Option, + // Pin a of the joystick + pub pin_x: String, + // Pin b of the joystick + pub pin_y: String, + // Pin z of the joystick + pub pin_z: String, + pub transform: Vec>, + pub bias: Vec, + pub resolution: u16, +} + +/// PMW3610 optical mouse sensor configuration +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw3610Config { + /// Name of the sensor (used for variable naming) + pub name: String, + /// id of the device + pub id: Option, + /// SPI pins + pub spi: SpiConfig, + /// Optional motion interrupt pin + pub motion: Option, + /// CPI resolution (200-3200, step 200). Optional, uses sensor default if not set. + pub cpi: Option, + /// Invert X axis + #[serde(default)] + pub invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub swap_xy: bool, + /// Force awake mode (disable power saving) + #[serde(default)] + pub force_awake: bool, + /// Enable smart mode for better tracking on shiny surfaces + #[serde(default)] + pub smart_mode: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Pmw33xxType { + #[default] + PMW3360, + PMW3389, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw33xxConfig { + // Name of the sensor (used for variable naming) + pub name: String, + // id of the device + pub id: Option, + // Sensor Type (3360 or 3389) + pub sensor_type: Pmw33xxType, + // SPI pins + pub spi: SpiConfig, + // Optional motion interrupt pin + pub motion: Option, + // CPI resolution (100-12000, step 100).Optional, uses sensor default 1600 if not set. + pub cpi: Option, + // Rotational transform angle (-127 to 127) Optional, uses sensor default 0 if not set. + pub rot_trans_angle: Option, + // liftoff distance. Optional, uses sensor default 0 if not set. + pub liftoff_dist: Option, + // Invert X axis + #[serde(default)] + pub proc_invert_x: bool, + // Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + // Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, +} + +/// Azoteq IQS5xx trackpad configuration. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxConfig { + /// Name of the trackpad (used for variable naming). + pub name: String, + /// RMK pointing-device id (0-255). Defaults to 0. + pub id: Option, + /// I²C bus the trackpad is connected to. The bus is dedicated to this + /// device — sharing with other I²C peripherals (e.g. an OLED) is not yet + /// supported via TOML. + pub i2c: Iqs5xxI2cConfig, + /// Optional `RDY` pin. Strongly recommended; without it the driver falls + /// back to timed polling and may stall the bus through clock-stretching. + pub rdy: Option, + /// Invert X in the PointingProcessor. + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y in the PointingProcessor. + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y in the PointingProcessor. + #[serde(default)] + pub proc_swap_xy: bool, +} + +/// I²C bus configuration for the IQS5xx. Distinct from the generic `I2cConfig` +/// because the IQS5xx address is fixed (`0x74` by default; can be reprogrammed +/// at the IC, but not at runtime — exposing it would be misleading). +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxI2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EncoderConfig { + // Pin a of the encoder + pub pin_a: String, + // Pin b of the encoder + pub pin_b: String, + // Phase is the working mode of the rotary encoders. + // Available mode: + // - default: resolution = 1 + // - e8h7: phase table tuned for E8H7 encoders + // - resolution: customized resolution, the resolution value and reverse should be specified + // A typical [EC11 encoder](https://tech.alpsalpine.com/cms.media/product_catalog_ec_01_ec11e_en_611f078659.pdf)'s resolution is 2 + // In resolution mode, you can also specify the number of detent and pulses, the resolution will be calculated by `pulse * 4 / detent` + #[serde(default)] + pub phase: EncoderPhase, + // Resolution + pub resolution: Option, + // The number of detent + pub detent: Option, + // The number of pulse + pub pulse: Option, + // Whether the direction of the rotary encoder is reversed. + pub reverse: Option, + // Use MCU's internal pull-up resistor or not, defaults to false, the external pull-up resistor is needed + #[serde(default = "default_false")] + pub internal_pullup: bool, + // Debounce interval in milliseconds. Suppresses spurious events from mechanical contact bounce. + // Defaults to 0 (disabled) if not specified. + pub debounce_ms: Option, +} + +/// Rotary encoder phase (decoding) mode +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum EncoderPhase { + #[default] + Default, + E8h7, + Resolution, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields, untagged)] +pub enum EncoderResolution { + Value(u8), + Derived { detent: u8, pulse: u8 }, +} + +impl Default for EncoderResolution { + fn default() -> Self { + Self::Value(4) + } +} + +/// Pointing device config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PointingDeviceConfig { + pub interface: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CommunicationProtocol { + I2c(I2cConfig), + Spi(SpiConfig), +} + +/// SPI config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SpiConfig { + pub instance: String, + pub sck: String, + pub mosi: String, + pub miso: String, + pub cs: Option, + pub cpi: Option, + pub tx_dma: Option, + pub rx_dma: Option, +} + +/// I2C config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct I2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, + /// 7-bit I2C address. Defaults to 0x3C when omitted. + #[serde(default = "default_i2c_address")] + pub address: u8, +} + +const fn default_i2c_address() -> u8 { + 0x3C +} + +/// Display driver type +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DisplayDriver { + Ssd1306, + Sh1106, + Sh1107, + Sh1108, + Ssd1309, +} + +/// Display configuration +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DisplayConfig { + pub driver: DisplayDriver, + pub protocol: CommunicationProtocol, + pub size: String, + #[serde(default)] + pub rotation: u16, + pub renderer: Option, + /// Poll interval in milliseconds for periodic redraws (animations). + /// When absent, polling is disabled — the display only redraws on events. + pub render_interval: Option, + /// Minimum time in milliseconds between event-driven renders. + /// Prevents the display from being hammered by rapid events. Default: 10 ms. + pub min_render_interval: Option, +} + +/// Configuration for an output pin +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OutputConfig { + pub pin: String, + #[serde(default)] + pub low_active: bool, + #[serde(default)] + pub initial_state_active: bool, +} + +impl KeyboardTomlConfig { + pub(crate) fn get_output_config(&self) -> Result, String> { + let output_config = self.output.clone(); + let split = self.split.clone(); + match (output_config, split) { + (None, Some(s)) => Ok(s.central.output.unwrap_or_default()), + (Some(c), None) => Ok(c), + (None, None) => Ok(Default::default()), + _ => Err("Use [[split.output]] to define outputs for split in your keyboard.toml!".to_string()), + } + } + + pub(crate) fn get_dependency_config(&self) -> DependencyConfig { + self.dependency.clone().unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_event_config_default_values() { + let config = EventConfig::default(); + + // Check some key default values from event_default.toml + assert_eq!(config.keyboard.channel_size, 16); + assert_eq!(config.keyboard.pubs, 2); + assert_eq!(config.keyboard.subs, 3); + + assert_eq!(config.modifier.channel_size, 8); + assert_eq!(config.modifier.pubs, 1); + assert_eq!(config.modifier.subs, 2); + + assert_eq!(config.layer_change.channel_size, 1); + assert_eq!(config.layer_change.subs, 1); + + assert_eq!(config.led_indicator.channel_size, 2); + assert_eq!(config.led_indicator.pubs, 2); + assert_eq!(config.led_indicator.subs, 3); + + assert_eq!(config.lighting_changed.channel_size, 1); + assert_eq!(config.lighting_changed.pubs, 1); + assert_eq!(config.lighting_changed.subs, 1); + + assert_eq!(config.pointing.channel_size, 8); + assert_eq!(config.pointing.subs, 2); + + assert_eq!(config.action.channel_size, 16); + assert_eq!(config.action.pubs, 1); + assert_eq!(config.action.subs, 0); + } + + #[test] + fn test_event_config_user_override() { + // Simulate user config that overrides some event settings + let user_toml = r#" +[event.keyboard] +channel_size = 32 +"#; + // Parse with event defaults first, then user config + let config: KeyboardTomlConfig = Config::builder() + .add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)) + .add_source(File::from_str(user_toml, FileFormat::Toml)) + .build() + .unwrap() + .try_deserialize() + .unwrap(); + + // User-overridden values + assert_eq!(config.event.keyboard.channel_size, 32); + assert_eq!(config.event.keyboard.pubs, 2); + assert_eq!(config.event.keyboard.subs, 3); + + // Non-overridden values should use defaults + assert_eq!(config.event.modifier.channel_size, 8); + assert_eq!(config.event.modifier.subs, 2); + assert_eq!(config.event.layer_change.subs, 1); + } + + #[test] + fn rmk_count_limits_fit_u8_capability_fields() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +combo_max_num = 255 +morse_max_num = 255 +fork_max_num = 255 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.combo_max_num, 255); + assert_eq!(ok.rmk.morse_max_num, 255); + assert_eq!(ok.rmk.fork_max_num, 255); + + for (field, message) in [ + ("combo_max_num", "combo_max_num must be between 0 and 255"), + ("morse_max_num", "morse_max_num must be between 0 and 255"), + ("fork_max_num", "fork_max_num must be between 0 and 255"), + ] { + let toml = format!("[rmk]\n{field} = 256\n"); + let err = toml::from_str::(&toml).unwrap_err(); + assert!(err.to_string().contains(message), "{err}"); + } + } + + #[test] + fn split_central_max_latency_matches_ble_limit() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +split_central_max_latency_powered = 499 +split_central_max_latency_battery = 498 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.split_central_max_latency_powered, 499); + assert_eq!(ok.rmk.split_central_max_latency_battery, 498); + + let err = toml::from_str::( + r#" +[rmk] +split_central_max_latency_battery = 500 +"#, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("split_central_max_latency must be between 0 and 499"), + "{err}" + ); + } + + #[test] + fn test_event_config_partial_override_with_event_defaults_loader() { + let user_toml = r#" +[event.layer_change] +subs = 2 +"#; + let path = std::env::temp_dir().join(format!( + "rmk-event-defaults-loader-{}-{}.toml", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::write(&path, user_toml).unwrap(); + + let config = KeyboardTomlConfig::new_from_toml_path_with_event_defaults(&path); + std::fs::remove_file(path).unwrap(); + + assert_eq!(config.event.layer_change.channel_size, 1); + assert_eq!(config.event.layer_change.pubs, 2); + assert_eq!(config.event.layer_change.subs, 2); + } +} diff --git a/resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/preimage b/resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/preimage new file mode 100644 index 0000000..829ec85 --- /dev/null +++ b/resolutions/rerere/3f52228629b6938a12071de8cecb48baef7811f5/preimage @@ -0,0 +1,1904 @@ +use std::collections::HashMap; +use std::path::Path; + +use config::{Config, File, FileFormat}; +use serde::{Deserialize, de}; +use serde_inline_default::serde_inline_default; + +/// Event channel default configuration +const EVENT_DEFAULT_CONFIG: &str = include_str!("default_config/event_default.toml"); + +pub(crate) mod chip; +pub(crate) mod communication; +pub mod resolved; +#[rustfmt::skip] +pub mod usb_interrupt_map; +pub(crate) mod behavior; +pub(crate) mod board; +pub(crate) mod dfu; +pub(crate) mod display; +pub(crate) mod host; +pub(crate) mod keycode_alias; +pub(crate) mod keymap; +pub mod layout; +pub use layout::{STOCK_WIDTHS, layout_blob_from_toml, layout_info_from_toml}; +pub(crate) mod light; +pub(crate) mod storage; + +/// Protocol-level capacity ceilings for wire-format Vec sizes. +/// +/// These define the maximum values any firmware may use for protocol +/// Vec capacities (`COMBO_SIZE`, `MORSE_SIZE`, etc.). The host tool compiles +/// against these as upper bounds. Any firmware with `rynk` enabled +/// must satisfy `value <= ceiling` at compile time. +/// +/// Constant names mirror the generated constants with a `MAX_` prefix: +/// `COMBO_SIZE` is bounded by `MAX_COMBO_SIZE`, etc. +pub mod protocol_limits { + /// Max keys in a combo trigger — ceiling for `COMBO_SIZE` + pub const MAX_COMBO_SIZE: usize = 16; + /// Max pattern entries per morse key — ceiling for `MORSE_SIZE` + pub const MAX_MORSE_SIZE: usize = 32; + /// Max bytes per macro data chunk — ceiling for `MACRO_DATA_SIZE` + pub const MAX_MACRO_DATA_SIZE: usize = 256; + /// Max key positions in an unlock challenge. + pub const MAX_UNLOCK_KEYS_SIZE: usize = 4; +} + +pub(crate) fn validate_unlock_keys( + section: &str, + unlock_keys: &[[u8; 2]], + layout: Option<&LayoutTomlConfig>, +) -> Result<(), String> { + if unlock_keys.len() > protocol_limits::MAX_UNLOCK_KEYS_SIZE { + return Err(format!( + "{section}.unlock_keys has {} entries, the max is {}", + unlock_keys.len(), + protocol_limits::MAX_UNLOCK_KEYS_SIZE + )); + } + + if let Some(layout) = layout { + for key in unlock_keys { + let (row, col) = (key[0], key[1]); + if row >= layout.rows || col >= layout.cols { + return Err(format!( + "{section}.unlock_keys position ({row}, {col}) is outside the {}x{} matrix", + layout.rows, layout.cols + )); + } + } + } + + Ok(()) +} + +/// Configurations for RMK keyboard. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub struct KeyboardTomlConfig { + /// Basic keyboard info + keyboard: Option, + /// Matrix of the keyboard, only for non-split keyboards + matrix: Option, + // Aliases for key maps + aliases: Option>, + /// Keymap config: layer count and the per-layer key actions (`[[keymap.layer]]`). + keymap: Option, + /// Layout config: the physical key arrangement (`map`) plus the rendered layout. + /// For split keyboards, the total row/col is defined in this section. + layout: Option, + /// Topology-aware lighting. Key geometry is always derived from + /// `[layout].map`; emitters add semantic identity and electrical routing. + lighting: Option, + /// Behavior config + behavior: Option, + /// Light config + light: Option, + /// Storage config + storage: Option, + /// DFU partition config (embassy-boot) + dfu: Option, + /// Ble config + pub(crate) ble: Option, + /// Chip-specific configs (e.g., [chip.nrf52840]) + chip: Option>, + /// Dependency config + dependency: Option, + /// Split config + split: Option, + /// Input device config + input_device: Option, + /// Display config + display: Option, + /// Output Pin config + output: Option>, + /// Set host configurations + pub(crate) host: Option, + /// RMK config constants + #[serde(default)] + pub(crate) rmk: RmkConstantsConfig, + /// Event channel configuration + /// Default values are loaded from event_default.toml in new_from_toml_path() + /// build.rs also loads event defaults via new_from_toml_path_with_event_defaults() + #[serde(default)] + pub(crate) event: EventConfig, + /// Whether the user explicitly set a [storage] section in keyboard.toml. + #[serde(skip)] + pub(crate) storage_user_set: bool, +} + +impl KeyboardTomlConfig { + fn parse_from_toml_path>(config_toml_path: P, chip_default_config: Option<&str>) -> Self { + let path = config_toml_path.as_ref(); + let path_str = path + .to_str() + .unwrap_or_else(|| panic!("Config path is not valid UTF-8: {:?}", path)); + + let mut builder = Config::builder().add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)); + if let Some(default_config) = chip_default_config { + builder = builder.add_source(File::from_str(default_config, FileFormat::Toml)); + } + builder + .add_source(File::with_name(path_str)) + .build() + .unwrap_or_else(|e| panic!("Parse {:?} error: {}", path, e)) + .try_deserialize() + .unwrap_or_else(|e| panic!("Deserialize {:?} error: {}", path, e)) + } + + /// Load keyboard.toml with event defaults only. + /// + /// This is used in build.rs where we only need [rmk] and [event] constants, + /// and should not require `[keyboard.board]`/`[keyboard.chip]`. + pub fn new_from_toml_path_with_event_defaults>(config_toml_path: P) -> Self { + let mut config = Self::parse_from_toml_path(config_toml_path, None); + config.storage_user_set = config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + config.auto_calculate_parameters(); + config + } + + pub fn new_from_toml_path>(config_toml_path: P) -> Self { + let path = config_toml_path.as_ref(); + + // First pass: load user config with event defaults to get chip model. + // This allows user's keyboard.toml to omit [event] section. + let user_config = Self::parse_from_toml_path(path, None); + + let default_config_str = user_config + .get_chip_model() + .and_then(|chip| chip.get_default_config_str()) + .unwrap_or_else(|e| panic!("❌ keyboard.toml error: {e}")); + + // Second pass: load with all three config sources + // Config priority (later sources override earlier ones): + // 1. Event default config (lowest priority) + // 2. Chip-specific default config + // 3. User config (highest priority) + let mut config = Self::parse_from_toml_path(path, Some(default_config_str)); + config.storage_user_set = user_config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + + config.auto_calculate_parameters(); + + config + } + + /// Auto calculate some parameters in toml: + /// - Update morse_max_num to fit all configured morses + /// - Update max_patterns_per_key to fit the max number of configured (pattern, action) pairs per morse key + /// - Update peripheral number based on the number of split boards + /// - TODO: Update controller number based on the number of split boards + pub(crate) fn auto_calculate_parameters(&mut self) { + // Update the number of peripherals + if let Some(split) = &self.split + && split.peripheral.len() > self.rmk.split_peripherals_num + { + // eprintln!( + // "The number of split peripherals is updated to {} from {}", + // split.peripheral.len(), + // self.rmk.split_peripherals_num + // ); + self.rmk.split_peripherals_num = split.peripheral.len(); + } + + if let Some(behavior) = &self.behavior { + // Update the max_patterns_per_key + if let Some(morse) = &behavior.morse + && let Some(morses) = &morse.morses + { + let mut max_required_patterns = self.rmk.max_patterns_per_key; + + for morse in morses { + let tap_actions_len = morse.tap_actions.as_ref().map(|v| v.len()).unwrap_or(0); + let hold_actions_len = morse.hold_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + let n = tap_actions_len.max(hold_actions_len); + if n > 15 { + panic!("The number of taps per morse is too large, the max number of taps is 15, got {n}"); + } + + let morse_actions_len = morse.morse_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + max_required_patterns = + max_required_patterns.max(tap_actions_len + hold_actions_len + morse_actions_len); + } + self.rmk.max_patterns_per_key = max_required_patterns; + + // Update the morse_max_num + self.rmk.morse_max_num = self.rmk.morse_max_num.max(morses.len()); + } + + // Update hold_trigger_key_position_max_num to fit every configured position + if let Some(morse) = &behavior.morse { + let regions = self.layout.as_ref().and_then(|layout| layout.regions.as_ref()); + let configured_len = |positions: Option<&Vec<[u8; 2]>>, names: Option<&Vec>| { + positions.map(Vec::len).unwrap_or(0) + + names + .into_iter() + .flatten() + .filter_map(|name| regions.and_then(|r| r.get(name))) + .map(Vec::len) + .sum::() + }; + let mut total = configured_len( + morse.hold_trigger_key_positions.as_ref(), + morse.hold_trigger_regions.as_ref(), + ); + if let Some(profiles) = &morse.profiles { + for profile in profiles.values() { + total += configured_len( + profile.hold_trigger_key_positions.as_ref(), + profile.hold_trigger_regions.as_ref(), + ); + } + } + self.rmk.hold_trigger_key_position_max_num = self.rmk.hold_trigger_key_position_max_num.max(total); + } + + let auto_mouse_layers = behavior.auto_mouse_layer.as_deref().unwrap_or_default(); + self.rmk.auto_mouse_layer_max_num.get_or_insert(auto_mouse_layers.len()); + + let mouse_layer_scales = behavior.mouse_layer_scale.as_deref().unwrap_or_default(); + self.rmk + .mouse_layer_scale_max_num + .get_or_insert(mouse_layer_scales.len()); + } else { + self.rmk.auto_mouse_layer_max_num.get_or_insert(0); + self.rmk.mouse_layer_scale_max_num.get_or_insert(0); + } + } +} + +/// Keyboard constants configuration for performance and hardware limits +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct RmkConstantsConfig { + /// Mouse key interval (ms) - controls mouse movement speed + #[serde_inline_default(20)] + pub mouse_key_interval: u16, + /// Mouse wheel interval (ms) - controls scrolling speed + #[serde_inline_default(80)] + pub mouse_wheel_interval: u16, + /// Maximum number of combos keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_combo_max_num")] + pub combo_max_num: usize, + /// Maximum number of keys pressed simultaneously in a combo + #[serde_inline_default(4)] + pub combo_max_length: usize, + /// Maximum number of forks for conditional key actions + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_fork_max_num")] + pub fork_max_num: usize, + /// Maximum number of morses keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_morse_max_num")] + pub morse_max_num: usize, + /// Capacity of the morse profile table (named profiles in `[behavior.morse.profiles]`) + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_morse_profile_max_num")] + pub morse_profile_max_num: usize, + /// Capacity of the hold trigger position table, counting every `hold_trigger_key_positions` + /// entry across all profiles. Raised automatically to fit `keyboard.toml`; the default + /// covers a typical home row mod setup at 3 bytes per entry. + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_hold_trigger_key_position_max_num")] + pub hold_trigger_key_position_max_num: usize, + /// Maximum number of patterns a morse key can handle + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_max_patterns_per_key")] + pub max_patterns_per_key: usize, + /// Macro space size in bytes for storing sequences + #[serde_inline_default(256)] + pub macro_space_size: usize, + /// Default debounce time in ms + #[serde_inline_default(20)] + pub debounce_time: u16, + /// Report channel size + #[serde_inline_default(16)] + pub report_channel_size: usize, + /// Vial channel size + #[serde_inline_default(4)] + pub vial_channel_size: usize, + /// Flash channel size + #[serde_inline_default(4)] + pub flash_channel_size: usize, + /// The number of the split peripherals + #[serde_inline_default(0)] + pub split_peripherals_num: usize, + /// The number of available BLE profiles + #[serde_inline_default(3)] + pub ble_profiles_num: usize, + /// BLE Split Central sleep timeout in seconds (0 = disabled) + #[serde_inline_default(0)] + pub split_central_sleep_timeout_seconds: u32, + /// Maximum BLE peripheral latency on external power, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_powered: u16, + /// Maximum BLE peripheral latency on battery, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_battery: u16, + /// Maximum macro data chunk size for protocol transfers (bytes). + /// Smaller values reduce firmware RAM usage but require more round-trips. + #[serde_inline_default(64)] + pub protocol_macro_chunk_size: usize, + /// Maximum number of auto mouse layer entries; auto-derived from `[[behavior.auto_mouse_layer]]` if unset. + #[serde(default)] + pub auto_mouse_layer_max_num: Option, + /// Maximum number of per-layer mouse scaling entries; auto-derived from `[[behavior.mouse_layer_scale]]` if unset. + #[serde(default)] + pub mouse_layer_scale_max_num: Option, + /// Exact RAM of each Rynk RX/TX frame buffer (bytes), payload capacity and bulk counts derive from it. + /// Default 488 fills exactly two BLE notifications. + #[serde_inline_default(488)] + pub rynk_buffer_size: usize, +} + +fn check_combo_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "combo_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_morse_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "morse_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +/// The profile index is a `u8` in `KeyAction::TapHold` and an index with no +/// table entry means "use the default profile", so the table may never cover +/// the full `u8` range: capacity ≤ 255 keeps at least one index always vacant. +fn check_hold_trigger_key_position_max_num<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = usize::deserialize(deserializer)?; + if value > 255 { + panic!( + "❌ Parse `keyboard.toml` error: hold_trigger_key_position_max_num must be between 0 and 255, got {value}" + ); + } + Ok(value) +} + +fn check_morse_profile_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > 255 { + panic!("❌ Parse `keyboard.toml` error: morse_profile_max_num must be between 0 and 255, got {value}"); + } + Ok(value) +} + +fn check_max_patterns_per_key<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if !(4..=65536).contains(&value) { + return Err(de::Error::custom(format!( + "max_patterns_per_key must be between 4 and 65536, got {value}" + ))); + } + Ok(value) +} + +fn check_fork_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "fork_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_split_central_max_latency<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value >= 500 { + return Err(de::Error::custom(format!( + "split_central_max_latency must be between 0 and 499, got {value}" + ))); + } + Ok(value) +} + +/// This separate Default impl is needed when `[rmk]` section is not set in keyboard.toml +impl Default for RmkConstantsConfig { + fn default() -> Self { + Self { + mouse_key_interval: 20, + mouse_wheel_interval: 80, + combo_max_num: 8, + combo_max_length: 4, + fork_max_num: 8, + morse_max_num: 8, + morse_profile_max_num: 16, + hold_trigger_key_position_max_num: 16, + max_patterns_per_key: 8, + macro_space_size: 256, + debounce_time: 20, + report_channel_size: 16, + vial_channel_size: 4, + flash_channel_size: 4, + split_peripherals_num: 0, + ble_profiles_num: 3, + split_central_sleep_timeout_seconds: 0, + split_central_max_latency_powered: 30, + split_central_max_latency_battery: 30, + protocol_macro_chunk_size: 64, + auto_mouse_layer_max_num: None, + mouse_layer_scale_max_num: None, + rynk_buffer_size: 488, + } + } +} + +/// Event channel configuration for a single event type +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct EventChannelConfig { + /// Channel buffer size + pub channel_size: usize, + /// Number of publishers + pub pubs: usize, + /// Number of subscribers + pub subs: usize, +} + +impl Default for EventChannelConfig { + fn default() -> Self { + Self { + channel_size: 1, + pubs: 1, + subs: 1, + } + } +} + +/// Macro to define EventConfig and related code without repetition +macro_rules! define_event_config { + ($($field:ident),* $(,)?) => { + /// Event configuration for all controller events + /// Default values are loaded from event_default.toml + #[derive(Clone, Debug, Deserialize)] + #[serde(deny_unknown_fields, default)] + pub(crate) struct EventConfig { + $(pub $field: EventChannelConfig,)* + } + + /// Cached default EventConfig parsed from event_default.toml + static EVENT_CONFIG_DEFAULTS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + #[derive(Deserialize)] + struct Inner { $($field: EventChannelConfig,)* } + #[derive(Deserialize)] + struct Wrapper { event: Inner } + let w: Wrapper = toml::from_str(EVENT_DEFAULT_CONFIG).expect("Failed to parse event_default.toml"); + EventConfig { $($field: w.event.$field,)* } + }); + + impl Default for EventConfig { + fn default() -> Self { + EVENT_CONFIG_DEFAULTS.clone() + } + } + }; +} + +define_event_config!( + // Connection events + connection_status_change, + // Input events + modifier, + keyboard, + // Keyboard state events + layer_change, + wpm_update, + led_indicator, + sleep_state, + lighting_changed, + // Power events + battery_status, + battery_adc, + charging_state, + // Pointing device events + pointing, + // Split events + peripheral_connected, + central_connected, + peripheral_battery, + clear_peer, + // DFU events + dfu_status, + // Action events + action, +); + +/// The `[layout]` section: the physical key arrangement plus the rendered layout. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayoutTomlConfig { + pub rows: u8, + pub cols: u8, + /// The physical arrangement: an ordered map of `(row,col)` positions with + /// optional hand, shape (`@2u`), gaps (`[1.5]`), row-steps (`[y=]`), and + /// encoders (`(e,0)`). Its order also defines the order of `[[keymap.layer]]`. + pub map: Option, + /// Reusable named sets of matrix coordinates for positional behaviors. + pub regions: Option>>, + // Rendered-layout fields. + pub default_variant: Option, + pub shapes: Option>, + pub variant: Option>, +} + +/// A named shape from `[layout.shapes]`. Every field optional; widths/ +/// heights default to 1u, nudges/rotation to 0, and `w2/h2/x2/y2` are an +/// optional second rectangle for L-shaped caps. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ShapeToml { + pub w: Option, + pub h: Option, + pub x: Option, + pub y: Option, + pub r: Option, + pub w2: Option, + pub h2: Option, + pub x2: Option, + pub y2: Option, +} + +/// One `[[layout.variant]]` render overlay: reshape some keys, hide others. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct VariantToml { + pub name: String, + pub shapes: Option>, + pub hidden: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingTomlConfig { + #[serde(default = "default_topology_revision")] + pub topology_revision: u32, + #[serde(default, rename = "zone")] + pub zones: Vec, + #[serde(default, rename = "output")] + pub outputs: Vec, + #[serde(default, rename = "emitter")] + pub emitters: Vec, + #[serde(default, rename = "layer_scene")] + pub layer_scenes: Vec, + #[serde(default, rename = "conditional_scene")] + pub conditional_scenes: Vec, + pub controls: Option, + pub background: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingControlsTomlConfig { + pub output_toggle_user_action: Option, + pub output_mode_cycle_user_action: Option, + /// Layers that wake lighting while held. A list, since any set of layers + /// may wake it; the host can replace the resolved mask at runtime. + pub wake_layers: Option>, + #[serde(default)] + pub initial_output_mode: LightingOutputModeToml, + #[serde(default)] + pub powered_only_scope: LightingPoweredOnlyScopeToml, + pub output_mode_indicator: Option, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingPoweredOnlyScopeToml { + #[default] + Authority, + Local, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingOutputModeToml { + #[default] + AlwaysOn, + AlwaysOff, + PoweredOnly, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputModeIndicatorTomlConfig { + pub target: LightingTargetTomlConfig, + pub always_on: LightingEffectTomlConfig, + pub always_off: LightingEffectTomlConfig, + pub powered_only: LightingEffectTomlConfig, +} + +fn default_topology_revision() -> u32 { + 1 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingZoneTomlConfig { + pub id: u8, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputTomlConfig { + pub node: u8, + pub id: u8, + pub pixel_count: u16, + pub capabilities: Vec, + #[serde(default)] + pub sparse: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingEmitterTomlConfig { + pub id: u16, + pub key: Option<[u8; 2]>, + pub position: Option<[f32; 3]>, + #[serde(default)] + pub zones: Vec, + pub node: u8, + pub output: u8, + pub physical_index: u16, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerSceneTomlConfig { + pub layer: u8, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingConditionalSceneTomlConfig { + pub layer: Option, + pub battery: Option, + pub output_mode: Option, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerConditionTomlConfig { + pub layer: u8, + #[serde(default = "default_true")] + pub active: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBatteryConditionTomlConfig { + pub node: u8, + pub min_level: Option, + pub max_level: Option, + #[serde(default)] + pub charge: LightingChargeConditionToml, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingChargeConditionToml { + #[default] + Any, + Charging, + Discharging, + Unknown, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingSceneCellTomlConfig { + pub target: LightingTargetTomlConfig, + pub effect: LightingEffectTomlConfig, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub(crate) enum LightingTargetTomlConfig { + Led { led: u16 }, + Key { key: [u8; 2] }, + Zone { zone: u8 }, + All { all: bool }, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub(crate) enum LightingEffectTomlConfig { + Solid { + color: [u8; 3], + }, + Blink { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + duty_percent: u8, + }, + Breathe { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + #[serde(default = "default_breathe_step_ms")] + step_ms: u16, + }, +} + +fn default_breathe_step_ms() -> u16 { + 16 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBackgroundTomlConfig { + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default)] + pub hue: u8, + #[serde(default)] + pub saturation: u8, + #[serde(default = "default_background_value")] + pub value: u8, + #[serde(default = "default_background_speed")] + pub speed: u8, + #[serde(default)] + pub mode: LightingBackgroundModeToml, +} + +fn default_background_value() -> u8 { + 32 +} + +fn default_background_speed() -> u8 { + 128 +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingBackgroundModeToml { + #[default] + Solid, + Breathe, +} + +/// The `[keymap]` section: layer count plus the per-layer key actions. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct KeymapTomlConfig { + /// Total layer count. Optional — defaults to the number of `[[keymap.layer]]` + /// blocks; set it larger to reserve extra empty layers (e.g. for Vial/Rynk). + pub layers: Option, + /// Per-layer key actions: `[[keymap.layer]]`. + #[serde(default)] + pub layer: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayerTomlConfig { + pub name: Option, + pub keys: String, + pub encoders: Option>, +} + +/// Configurations for keyboard info +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct KeyboardInfo { + /// Keyboard name + pub name: String, + /// Vender id + pub vendor_id: u16, + /// Product id + pub product_id: u16, + /// Manufacturer + pub manufacturer: Option, + /// Product name, if not set, it will use `name` as default + pub product_name: Option, + /// Serial number + pub serial_number: Option, + /// Board name(if a supported board is used) + pub board: Option, + /// Chip model + pub chip: Option, + /// enable usb + pub usb_enable: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub enum MatrixType { + #[default] + #[serde(rename = "normal")] + Normal, + #[serde(rename = "direct_pin")] + DirectPin, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DebouncerType { + #[default] + Default, + Fast, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MatrixConfig { + #[serde(default)] + pub matrix_type: MatrixType, + pub row_pins: Option>, + pub col_pins: Option>, + pub direct_pins: Option>>, + #[serde(default = "default_true")] + pub direct_pin_low_active: bool, + #[serde(default = "default_false")] + pub row2col: bool, + #[serde(default)] + pub debouncer: DebouncerType, + pub bootmagic: Option<(u8, u8)>, +} + +/// Config for storage +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct StorageConfig { + /// Start address of local storage, MUST BE start of a sector. + /// If start_addr is set to 0(this is the default value), the last `num_sectors` sectors will be used. + pub start_addr: Option, + // Number of sectors used for storage, >= 2. + pub num_sectors: Option, + #[serde(default = "default_true")] + pub enabled: bool, + // Clear on the storage at reboot, set this to true if you want to reset the keymap + pub clear_storage: Option, + // Clear on the layout at reboot, set this to true if you want to reset the layout + pub clear_layout: Option, +} + +/// Config for DFU partition layout (embassy-boot). +/// +/// These values must match the bootloader's `memory.x` / linker script. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct DfuTomlConfig { + /// Offset of the boot state partition + pub state_offset: Option, + /// Size of the boot state partition + pub state_size: Option, + /// Offset of the DFU download partition + pub dfu_offset: Option, + /// Size of the DFU download partition + pub dfu_size: Option, + /// Flash page size in bytes (e.g. 4096 for RP2040). + /// Used with `flash_size` to auto-calculate partition addresses. + pub page_size: Option, + /// Total flash size in bytes. When set, DFU partition addresses are + /// calculated automatically using the rmk-boot formula. + /// Defaults to 2 MB (2097152) when omitted. + pub flash_size: Option, + /// Optional DFU activity LED pin, e.g. `"PIN_16"`. When set, the LED + /// is lit while a DFU download is in progress. + pub led: Option, + /// Unlock keys for DFU lock (optional) + pub unlock_keys: Option>, +} + +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BleConfig { + pub enabled: bool, + pub battery_adc_pin: Option, + pub charge_state: Option, + pub charge_led: Option, + pub adc_divider_measured: Option, + pub adc_divider_total: Option, + pub default_tx_power: Option, + pub use_2m_phy: Option, + pub passkey_entry: Option, + pub passkey_entry_timeout: Option, +} + +/// Default passkey entry timeout in seconds. +pub const DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 120; + +/// Minimum passkey entry timeout in seconds. +pub const MIN_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 30; + +/// nRF52840 DCDC REG0 output voltage +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum DcdcReg0Voltage { + #[serde(rename = "3V3")] + V3_3, + #[serde(rename = "1V8")] + V1_8, +} + +/// Config for chip-specific settings +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ChipConfig { + /// DCDC regulator 0 enabled (for nrf52840) + pub dcdc_reg0: Option, + /// DCDC regulator 1 enabled (for nrf52840, nrf52833) + pub dcdc_reg1: Option, + /// DCDC regulator 0 voltage (for nrf52840) + pub dcdc_reg0_voltage: Option, +} + +/// Config for lights +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LightConfig { + pub capslock: Option, + pub scrolllock: Option, + pub numslock: Option, +} + +/// Config for a single pin +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PinConfig { + pub pin: String, + pub low_active: bool, +} + +/// Configurations for dependencies +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DependencyConfig { + /// Enable defmt log or not + #[serde(default = "default_true")] + pub defmt_log: bool, +} + +impl Default for DependencyConfig { + fn default() -> Self { + Self { defmt_log: true } + } +} + +/// Intermediate resolved keymap grid (rows/cols/layers + per-layer actions). +/// Built once by `get_keymap_config` and unpacked into `Keymap`; never (de)serialized. +pub(crate) struct KeymapConfig { + pub rows: u8, + pub cols: u8, + pub layers: u8, + pub keymap: Vec>>, + pub encoder_map: Vec>, // Empty if there are no encoders or not configured +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KeyInfo { + pub hand: char, // 'L' or 'R' or other chars +} + +/// Configurations for actions behavior +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct BehaviorConfig { + pub tri_layer: Option, + pub one_shot: Option, + pub one_shot_modifiers: Option, + pub combo: Option, + #[serde(alias = "macro")] + pub macros: Option, + pub fork: Option, + pub morse: Option, + pub auto_mouse_layer: Option>, + pub mouse_layer_scale: Option>, +} + +/// Per-layer mouse movement and scroll scaling. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MouseLayerScaleConfig { + pub layer: u8, + pub r#move: Option<[u16; 2]>, + pub scroll: Option<[u16; 2]>, +} + +/// Configurations for auto mouse layer +/// +/// When motion is detected from a pointing device (e.g. PMW3610), the +/// specified `target_layer` is activated. The layer stays active until +/// `timeout` has elapsed without further motion, then it is deactivated. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct AutoMouseLayerConfig { + /// Pointing device id this entry applies to. When omitted, the entry acts as + /// a fallback for events whose `device_id` matches no other entry. + pub device_id: Option, + /// Layer index to activate on cursor motion + pub target_layer: u8, + /// Idle time after the last cursor motion before the layer is deactivated + /// (e.g. `"500ms"` or `"2s"`). + pub timeout: Option, + /// Minimum absolute axis delta required to be considered as motion. + /// Defaults to `1` (any motion). Helpful to filter out sensor noise. + pub threshold: Option, + /// When `true`, non-mouse key presses deactivate `target_layer` immediately (mouse HID keys and `extra_mouse_keys` excepted). + /// Macro-emitted keycodes, `Again`/`Repeat`, and `GraveEscape` cannot be classified and never deactivate the layer. + pub deactivate_on_key: Option, + /// Extra keycodes (e.g. modifiers) that do not trigger deactivation when `deactivate_on_key` is set. + /// Modifier keycodes listed here also exempt modifier-only actions containing them. + pub extra_mouse_keys: Option>, + /// When `true`, key presses that do NOT deactivate `target_layer` extend the timeout deadline + /// (i.e. reset it to now + `timeout`) at the moment the key's action resolves. + pub reset_timeout_on_key: Option, +} + +/// Per Key configurations profiles for morse, tap-hold, etc. +/// overrides the defaults given in TapHoldConfig +#[derive(Clone, Debug, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseProfile { + pub enable_flow_tap: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, +<<<<<<< + + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves this tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, +======= + + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + /// overrides the global `[behavior.morse] prior_idle_time` for keys using this profile + pub prior_idle_time: Option, +>>>>>>> +} + +/// Configurations for tri layer +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct TriLayerConfig { + pub upper: u8, + pub lower: u8, + pub adjust: u8, +} + +/// Configurations for oneshot modifiers/layers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OneShotConfig { + pub timeout: Option, +} + +/// Configurations for oneshot modifiers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OneShotModifiersConfig { + pub activate_on_keypress: Option, + pub quick_release: Option, +} + +/// Configurations for combos +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CombosConfig { + #[serde(default)] + pub combos: Vec, + pub timeout: Option, + pub prior_idle_time: Option, +} + +/// Configurations for combo +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ComboConfig { + pub actions: Vec, + pub output: String, + pub layer: Option, +} + +/// Configurations for macros +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacrosConfig { + pub macros: Vec, +} + +/// Configurations for macro +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacroConfig { + pub operations: Vec, +} + +/// Macro operations (TOML deserialization type — resolved equivalent is in `resolved::behavior`) +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "operation", rename_all = "lowercase")] +pub(crate) enum MacroOperation { + Tap { keycode: String }, + Down { keycode: String }, + Up { keycode: String }, + Delay { duration: DurationMillis }, + Text { text: String }, +} + +/// Configurations for forks +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForksConfig { + pub forks: Vec, +} + +/// Configurations for fork +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForkConfig { + pub trigger: String, + pub negative_output: String, + pub positive_output: String, + pub match_any: Option, + pub match_none: Option, + pub kept_modifiers: Option, + pub bindable: Option, +} + +/// Configurations for morse keys +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorsesConfig { + pub enable_flow_tap: Option, //default: false + /// used in permissive_hold mode + pub prior_idle_time: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, + +<<<<<<< + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves the tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, + +======= + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + +>>>>>>> + /// these can be used to overrides the defaults given above + pub profiles: Option>, + + /// the definition of morse / tap dance keys + pub morses: Option>, +} + +/// Configurations for morse +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseConfig { + // name of morse profile (to address BehaviorConfig::morse.profiles[self.profile]) + pub profile: Option, + + pub tap: Option, + pub hold: Option, + pub hold_after_tap: Option, + pub double_tap: Option, + /// Array of tap actions for each tap count (0-indexed) + pub tap_actions: Option>, + /// Array of hold actions for each tap count (0-indexed) + pub hold_actions: Option>, + /// Array of morse patter->action pairs count (0-indexed) + pub morse_actions: Option>, +} + +/// Configurations for morse action pairs +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseActionPair { + pub pattern: String, // for example morse code of "B": "-..." or "_..." or "1000" + pub action: String, // "B" +} + +/// Split connection transport +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SplitConnection { + #[default] + Ble, + Serial, +} + +/// Configurations for split keyboards +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitConfig { + pub connection: SplitConnection, + pub central: SplitBoardConfig, + pub peripheral: Vec, +} + +/// Configurations for each split board +/// +/// The transport field must match `split.connection`: `serial` is required for +/// serial splits and forbidden for BLE splits; `ble_addr` is optional for BLE +/// splits (dongle setups omit it) and forbidden for serial splits. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitBoardConfig { + /// Row number of the split board + pub rows: usize, + /// Col number of the split board + pub cols: usize, + /// Row offset of the split board + pub row_offset: usize, + /// Col offset of the split board + pub col_offset: usize, + /// Ble address + pub ble_addr: Option<[u8; 6]>, + /// Serial config, the vector length should be 1 for peripheral + pub serial: Option>, + /// Matrix config for the split + pub matrix: MatrixConfig, + /// Input device config for the split + pub input_device: Option, + /// Display config for the split board + pub display: Option, + /// Battery ADC pin for this split board + pub battery_adc_pin: Option, + /// ADC divider measured value for battery + pub adc_divider_measured: Option, + /// ADC divider total value for battery + pub adc_divider_total: Option, + /// Output Pin config for the split + pub output: Option>, + /// Path to the peripheral firmware binary for automatic dfu_split update. + /// Relative to the project's `Cargo.toml`. When set, the generated code + /// includes the binary with `include_bytes!` and registers it via + /// [`set_firmware_update_data`](crate::set_firmware_update_data). + pub firmware: Option, + /// DFU update policy for this peripheral. "MatchHash" (default) only + /// flashes when the firmware differs; "force" always flashes. + pub update_policy: Option, +} + +/// Serial port config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SerialConfig { + pub instance: String, + pub tx_pin: String, + pub rx_pin: String, +} + +/// Duration in milliseconds +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct DurationMillis(#[serde(deserialize_with = "parse_duration_millis")] pub u64); + +const fn default_true() -> bool { + true +} + +const fn default_false() -> bool { + false +} + +const fn default_pointing_report_hz() -> u16 { + 125 +} + +fn parse_duration_millis<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { + let input: String = de::Deserialize::deserialize(deserializer)?; + let num = input.trim_end_matches(|c: char| !c.is_numeric()); + let unit = &input[num.len()..]; + let num: u64 = num.parse().map_err(|_| { + de::Error::custom(format!( + "Invalid number \"{num}\" in duration: number part must be a u64" + )) + })?; + + match unit { + "s" => Ok(num * 1000), + "ms" => Ok(num), + other => Err(de::Error::custom(format!( + "Invalid duration unit \"{other}\": unit part must be either \"s\" or \"ms\"" + ))), + } +} + +/// Configuration for host tools +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct HostConfig { + /// Whether Vial is enabled + #[serde_inline_default(true)] + pub vial_enabled: bool, + /// Whether the RMK-native Rynk protocol is enabled. Mutually exclusive + /// with `vial_enabled` (the underlying Cargo features conflict). + #[serde_inline_default(false)] + pub rynk_enabled: bool, + /// Physical keys (row, col) held simultaneously to unlock (optional). + /// Shared by the Vial lock and the Rynk lock gate. + pub unlock_keys: Option>, + /// Start (and stay) unlocked, bypassing the unlock-key combo (default: + /// false). Renamed from `vial_insecure`; the old name still parses. + #[serde(alias = "vial_insecure")] + #[serde_inline_default(false)] + pub insecure: bool, + /// Move the Rynk config-write tier (`SetKeyAction`, `SetMacro`, …) into the + /// locked set, so writes also require unlock (default: false). + #[serde_inline_default(false)] + pub write_requires_unlock: bool, + /// Require the Rynk physical-presence unlock before entering either the + /// central or a split peripheral bootloader (default: true). + #[serde_inline_default(true)] + pub bootloader_requires_unlock: bool, +} + +impl Default for HostConfig { + fn default() -> Self { + Self { + vial_enabled: true, + rynk_enabled: false, + unlock_keys: None, + insecure: false, + write_requires_unlock: false, + bootloader_requires_unlock: true, + } + } +} + +/// Configurations for input devices +/// +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InputDeviceConfig { + pub encoder: Option>, + pub pointing: Option>, + pub joystick: Option>, + pub pmw3610: Option>, + pub pmw33xx: Option>, + pub iqs5xx: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct JoystickConfig { + // Name of the joystick + pub name: String, + /// Device id used to match this joystick with its JoystickProcessor. + /// If omitted, ids are assigned sequentially starting from 0. + pub id: Option, + // Pin a of the joystick + pub pin_x: String, + // Pin b of the joystick + pub pin_y: String, + // Pin z of the joystick + pub pin_z: String, + pub transform: Vec>, + pub bias: Vec, + pub resolution: u16, +} + +/// PMW3610 optical mouse sensor configuration +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw3610Config { + /// Name of the sensor (used for variable naming) + pub name: String, + /// id of the device + pub id: Option, + /// SPI pins + pub spi: SpiConfig, + /// Optional motion interrupt pin + pub motion: Option, + /// CPI resolution (200-3200, step 200). Optional, uses sensor default if not set. + pub cpi: Option, + /// Invert X axis + #[serde(default)] + pub invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub swap_xy: bool, + /// Force awake mode (disable power saving) + #[serde(default)] + pub force_awake: bool, + /// Enable smart mode for better tracking on shiny surfaces + #[serde(default)] + pub smart_mode: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Pmw33xxType { + #[default] + PMW3360, + PMW3389, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw33xxConfig { + // Name of the sensor (used for variable naming) + pub name: String, + // id of the device + pub id: Option, + // Sensor Type (3360 or 3389) + pub sensor_type: Pmw33xxType, + // SPI pins + pub spi: SpiConfig, + // Optional motion interrupt pin + pub motion: Option, + // CPI resolution (100-12000, step 100).Optional, uses sensor default 1600 if not set. + pub cpi: Option, + // Rotational transform angle (-127 to 127) Optional, uses sensor default 0 if not set. + pub rot_trans_angle: Option, + // liftoff distance. Optional, uses sensor default 0 if not set. + pub liftoff_dist: Option, + // Invert X axis + #[serde(default)] + pub proc_invert_x: bool, + // Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + // Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, +} + +/// Azoteq IQS5xx trackpad configuration. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxConfig { + /// Name of the trackpad (used for variable naming). + pub name: String, + /// RMK pointing-device id (0-255). Defaults to 0. + pub id: Option, + /// I²C bus the trackpad is connected to. The bus is dedicated to this + /// device — sharing with other I²C peripherals (e.g. an OLED) is not yet + /// supported via TOML. + pub i2c: Iqs5xxI2cConfig, + /// Optional `RDY` pin. Strongly recommended; without it the driver falls + /// back to timed polling and may stall the bus through clock-stretching. + pub rdy: Option, + /// Invert X in the PointingProcessor. + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y in the PointingProcessor. + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y in the PointingProcessor. + #[serde(default)] + pub proc_swap_xy: bool, +} + +/// I²C bus configuration for the IQS5xx. Distinct from the generic `I2cConfig` +/// because the IQS5xx address is fixed (`0x74` by default; can be reprogrammed +/// at the IC, but not at runtime — exposing it would be misleading). +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxI2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EncoderConfig { + // Pin a of the encoder + pub pin_a: String, + // Pin b of the encoder + pub pin_b: String, + // Phase is the working mode of the rotary encoders. + // Available mode: + // - default: resolution = 1 + // - e8h7: phase table tuned for E8H7 encoders + // - resolution: customized resolution, the resolution value and reverse should be specified + // A typical [EC11 encoder](https://tech.alpsalpine.com/cms.media/product_catalog_ec_01_ec11e_en_611f078659.pdf)'s resolution is 2 + // In resolution mode, you can also specify the number of detent and pulses, the resolution will be calculated by `pulse * 4 / detent` + #[serde(default)] + pub phase: EncoderPhase, + // Resolution + pub resolution: Option, + // The number of detent + pub detent: Option, + // The number of pulse + pub pulse: Option, + // Whether the direction of the rotary encoder is reversed. + pub reverse: Option, + // Use MCU's internal pull-up resistor or not, defaults to false, the external pull-up resistor is needed + #[serde(default = "default_false")] + pub internal_pullup: bool, + // Debounce interval in milliseconds. Suppresses spurious events from mechanical contact bounce. + // Defaults to 0 (disabled) if not specified. + pub debounce_ms: Option, +} + +/// Rotary encoder phase (decoding) mode +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum EncoderPhase { + #[default] + Default, + E8h7, + Resolution, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields, untagged)] +pub enum EncoderResolution { + Value(u8), + Derived { detent: u8, pulse: u8 }, +} + +impl Default for EncoderResolution { + fn default() -> Self { + Self::Value(4) + } +} + +/// Pointing device config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PointingDeviceConfig { + pub interface: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CommunicationProtocol { + I2c(I2cConfig), + Spi(SpiConfig), +} + +/// SPI config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SpiConfig { + pub instance: String, + pub sck: String, + pub mosi: String, + pub miso: String, + pub cs: Option, + pub cpi: Option, + pub tx_dma: Option, + pub rx_dma: Option, +} + +/// I2C config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct I2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, + /// 7-bit I2C address. Defaults to 0x3C when omitted. + #[serde(default = "default_i2c_address")] + pub address: u8, +} + +const fn default_i2c_address() -> u8 { + 0x3C +} + +/// Display driver type +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DisplayDriver { + Ssd1306, + Sh1106, + Sh1107, + Sh1108, + Ssd1309, +} + +/// Display configuration +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DisplayConfig { + pub driver: DisplayDriver, + pub protocol: CommunicationProtocol, + pub size: String, + #[serde(default)] + pub rotation: u16, + pub renderer: Option, + /// Poll interval in milliseconds for periodic redraws (animations). + /// When absent, polling is disabled — the display only redraws on events. + pub render_interval: Option, + /// Minimum time in milliseconds between event-driven renders. + /// Prevents the display from being hammered by rapid events. Default: 10 ms. + pub min_render_interval: Option, +} + +/// Configuration for an output pin +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OutputConfig { + pub pin: String, + #[serde(default)] + pub low_active: bool, + #[serde(default)] + pub initial_state_active: bool, +} + +impl KeyboardTomlConfig { + pub(crate) fn get_output_config(&self) -> Result, String> { + let output_config = self.output.clone(); + let split = self.split.clone(); + match (output_config, split) { + (None, Some(s)) => Ok(s.central.output.unwrap_or_default()), + (Some(c), None) => Ok(c), + (None, None) => Ok(Default::default()), + _ => Err("Use [[split.output]] to define outputs for split in your keyboard.toml!".to_string()), + } + } + + pub(crate) fn get_dependency_config(&self) -> DependencyConfig { + self.dependency.clone().unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_event_config_default_values() { + let config = EventConfig::default(); + + // Check some key default values from event_default.toml + assert_eq!(config.keyboard.channel_size, 16); + assert_eq!(config.keyboard.pubs, 2); + assert_eq!(config.keyboard.subs, 3); + + assert_eq!(config.modifier.channel_size, 8); + assert_eq!(config.modifier.pubs, 1); + assert_eq!(config.modifier.subs, 2); + + assert_eq!(config.layer_change.channel_size, 1); + assert_eq!(config.layer_change.subs, 1); + + assert_eq!(config.led_indicator.channel_size, 2); + assert_eq!(config.led_indicator.pubs, 2); + assert_eq!(config.led_indicator.subs, 3); + + assert_eq!(config.lighting_changed.channel_size, 1); + assert_eq!(config.lighting_changed.pubs, 1); + assert_eq!(config.lighting_changed.subs, 1); + + assert_eq!(config.pointing.channel_size, 8); + assert_eq!(config.pointing.subs, 2); + + assert_eq!(config.action.channel_size, 16); + assert_eq!(config.action.pubs, 1); + assert_eq!(config.action.subs, 0); + } + + #[test] + fn test_event_config_user_override() { + // Simulate user config that overrides some event settings + let user_toml = r#" +[event.keyboard] +channel_size = 32 +"#; + // Parse with event defaults first, then user config + let config: KeyboardTomlConfig = Config::builder() + .add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)) + .add_source(File::from_str(user_toml, FileFormat::Toml)) + .build() + .unwrap() + .try_deserialize() + .unwrap(); + + // User-overridden values + assert_eq!(config.event.keyboard.channel_size, 32); + assert_eq!(config.event.keyboard.pubs, 2); + assert_eq!(config.event.keyboard.subs, 3); + + // Non-overridden values should use defaults + assert_eq!(config.event.modifier.channel_size, 8); + assert_eq!(config.event.modifier.subs, 2); + assert_eq!(config.event.layer_change.subs, 1); + } + + #[test] + fn rmk_count_limits_fit_u8_capability_fields() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +combo_max_num = 255 +morse_max_num = 255 +fork_max_num = 255 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.combo_max_num, 255); + assert_eq!(ok.rmk.morse_max_num, 255); + assert_eq!(ok.rmk.fork_max_num, 255); + + for (field, message) in [ + ("combo_max_num", "combo_max_num must be between 0 and 255"), + ("morse_max_num", "morse_max_num must be between 0 and 255"), + ("fork_max_num", "fork_max_num must be between 0 and 255"), + ] { + let toml = format!("[rmk]\n{field} = 256\n"); + let err = toml::from_str::(&toml).unwrap_err(); + assert!(err.to_string().contains(message), "{err}"); + } + } + + #[test] + fn split_central_max_latency_matches_ble_limit() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +split_central_max_latency_powered = 499 +split_central_max_latency_battery = 498 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.split_central_max_latency_powered, 499); + assert_eq!(ok.rmk.split_central_max_latency_battery, 498); + + let err = toml::from_str::( + r#" +[rmk] +split_central_max_latency_battery = 500 +"#, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("split_central_max_latency must be between 0 and 499"), + "{err}" + ); + } + + #[test] + fn test_event_config_partial_override_with_event_defaults_loader() { + let user_toml = r#" +[event.layer_change] +subs = 2 +"#; + let path = std::env::temp_dir().join(format!( + "rmk-event-defaults-loader-{}-{}.toml", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::write(&path, user_toml).unwrap(); + + let config = KeyboardTomlConfig::new_from_toml_path_with_event_defaults(&path); + std::fs::remove_file(path).unwrap(); + + assert_eq!(config.event.layer_change.channel_size, 1); + assert_eq!(config.event.layer_change.pubs, 2); + assert_eq!(config.event.layer_change.subs, 2); + } +} diff --git a/resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/postimage.1 b/resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/postimage.1 new file mode 100644 index 0000000..cd19f44 --- /dev/null +++ b/resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/postimage.1 @@ -0,0 +1,1903 @@ +use std::collections::HashMap; +use std::path::Path; + +use config::{Config, File, FileFormat}; +use serde::{Deserialize, de}; +use serde_inline_default::serde_inline_default; + +/// Event channel default configuration +const EVENT_DEFAULT_CONFIG: &str = include_str!("default_config/event_default.toml"); + +pub(crate) mod chip; +pub(crate) mod communication; +pub mod resolved; +#[rustfmt::skip] +pub mod usb_interrupt_map; +pub(crate) mod behavior; +pub(crate) mod board; +pub(crate) mod dfu; +pub(crate) mod display; +pub(crate) mod host; +pub(crate) mod keycode_alias; +pub(crate) mod keymap; +pub mod layout; +pub use layout::{STOCK_WIDTHS, layout_blob_from_toml, layout_info_from_toml}; +pub(crate) mod light; +pub(crate) mod storage; + +/// Protocol-level capacity ceilings for wire-format Vec sizes. +/// +/// These define the maximum values any firmware may use for protocol +/// Vec capacities (`COMBO_SIZE`, `MORSE_SIZE`, etc.). The host tool compiles +/// against these as upper bounds. Any firmware with `rynk` enabled +/// must satisfy `value <= ceiling` at compile time. +/// +/// Constant names mirror the generated constants with a `MAX_` prefix: +/// `COMBO_SIZE` is bounded by `MAX_COMBO_SIZE`, etc. +pub mod protocol_limits { + /// Max keys in a combo trigger — ceiling for `COMBO_SIZE` + pub const MAX_COMBO_SIZE: usize = 16; + /// Max pattern entries per morse key — ceiling for `MORSE_SIZE` + pub const MAX_MORSE_SIZE: usize = 32; + /// Max bytes per macro data chunk — ceiling for `MACRO_DATA_SIZE` + pub const MAX_MACRO_DATA_SIZE: usize = 256; + /// Max key positions in an unlock challenge. + pub const MAX_UNLOCK_KEYS_SIZE: usize = 4; +} + +pub(crate) fn validate_unlock_keys( + section: &str, + unlock_keys: &[[u8; 2]], + layout: Option<&LayoutTomlConfig>, +) -> Result<(), String> { + if unlock_keys.len() > protocol_limits::MAX_UNLOCK_KEYS_SIZE { + return Err(format!( + "{section}.unlock_keys has {} entries, the max is {}", + unlock_keys.len(), + protocol_limits::MAX_UNLOCK_KEYS_SIZE + )); + } + + if let Some(layout) = layout { + for key in unlock_keys { + let (row, col) = (key[0], key[1]); + if row >= layout.rows || col >= layout.cols { + return Err(format!( + "{section}.unlock_keys position ({row}, {col}) is outside the {}x{} matrix", + layout.rows, layout.cols + )); + } + } + } + + Ok(()) +} + +/// Configurations for RMK keyboard. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub struct KeyboardTomlConfig { + /// Basic keyboard info + keyboard: Option, + /// Matrix of the keyboard, only for non-split keyboards + matrix: Option, + // Aliases for key maps + aliases: Option>, + /// Keymap config: layer count and the per-layer key actions (`[[keymap.layer]]`). + keymap: Option, + /// Layout config: the physical key arrangement (`map`) plus the rendered layout. + /// For split keyboards, the total row/col is defined in this section. + layout: Option, + /// Topology-aware lighting. Key geometry is always derived from + /// `[layout].map`; emitters add semantic identity and electrical routing. + lighting: Option, + /// Behavior config + behavior: Option, + /// Light config + light: Option, + /// Storage config + storage: Option, + /// DFU partition config (embassy-boot) + dfu: Option, + /// Ble config + pub(crate) ble: Option, + /// Chip-specific configs (e.g., [chip.nrf52840]) + chip: Option>, + /// Dependency config + dependency: Option, + /// Split config + split: Option, + /// Input device config + input_device: Option, + /// Display config + display: Option, + /// Output Pin config + output: Option>, + /// Set host configurations + pub(crate) host: Option, + /// RMK config constants + #[serde(default)] + pub(crate) rmk: RmkConstantsConfig, + /// Event channel configuration + /// Default values are loaded from event_default.toml in new_from_toml_path() + /// build.rs also loads event defaults via new_from_toml_path_with_event_defaults() + #[serde(default)] + pub(crate) event: EventConfig, + /// Whether the user explicitly set a [storage] section in keyboard.toml. + #[serde(skip)] + pub(crate) storage_user_set: bool, +} + +impl KeyboardTomlConfig { + fn parse_from_toml_path>(config_toml_path: P, chip_default_config: Option<&str>) -> Self { + let path = config_toml_path.as_ref(); + let path_str = path + .to_str() + .unwrap_or_else(|| panic!("Config path is not valid UTF-8: {:?}", path)); + + let mut builder = Config::builder().add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)); + if let Some(default_config) = chip_default_config { + builder = builder.add_source(File::from_str(default_config, FileFormat::Toml)); + } + builder + .add_source(File::with_name(path_str)) + .build() + .unwrap_or_else(|e| panic!("Parse {:?} error: {}", path, e)) + .try_deserialize() + .unwrap_or_else(|e| panic!("Deserialize {:?} error: {}", path, e)) + } + + /// Load keyboard.toml with event defaults only. + /// + /// This is used in build.rs where we only need [rmk] and [event] constants, + /// and should not require `[keyboard.board]`/`[keyboard.chip]`. + pub fn new_from_toml_path_with_event_defaults>(config_toml_path: P) -> Self { + let mut config = Self::parse_from_toml_path(config_toml_path, None); + config.storage_user_set = config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + config.auto_calculate_parameters(); + config + } + + pub fn new_from_toml_path>(config_toml_path: P) -> Self { + let path = config_toml_path.as_ref(); + + // First pass: load user config with event defaults to get chip model. + // This allows user's keyboard.toml to omit [event] section. + let user_config = Self::parse_from_toml_path(path, None); + + let default_config_str = user_config + .get_chip_model() + .and_then(|chip| chip.get_default_config_str()) + .unwrap_or_else(|e| panic!("❌ keyboard.toml error: {e}")); + + // Second pass: load with all three config sources + // Config priority (later sources override earlier ones): + // 1. Event default config (lowest priority) + // 2. Chip-specific default config + // 3. User config (highest priority) + let mut config = Self::parse_from_toml_path(path, Some(default_config_str)); + config.storage_user_set = user_config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + + config.auto_calculate_parameters(); + + config + } + + /// Auto calculate some parameters in toml: + /// - Update morse_max_num to fit all configured morses + /// - Update max_patterns_per_key to fit the max number of configured (pattern, action) pairs per morse key + /// - Update peripheral number based on the number of split boards + /// - TODO: Update controller number based on the number of split boards + pub(crate) fn auto_calculate_parameters(&mut self) { + // Update the number of peripherals + if let Some(split) = &self.split + && split.peripheral.len() > self.rmk.split_peripherals_num + { + // eprintln!( + // "The number of split peripherals is updated to {} from {}", + // split.peripheral.len(), + // self.rmk.split_peripherals_num + // ); + self.rmk.split_peripherals_num = split.peripheral.len(); + } + + if let Some(behavior) = &self.behavior { + // Update the max_patterns_per_key + if let Some(morse) = &behavior.morse + && let Some(morses) = &morse.morses + { + let mut max_required_patterns = self.rmk.max_patterns_per_key; + + for morse in morses { + let tap_actions_len = morse.tap_actions.as_ref().map(|v| v.len()).unwrap_or(0); + let hold_actions_len = morse.hold_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + let n = tap_actions_len.max(hold_actions_len); + if n > 15 { + panic!("The number of taps per morse is too large, the max number of taps is 15, got {n}"); + } + + let morse_actions_len = morse.morse_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + max_required_patterns = + max_required_patterns.max(tap_actions_len + hold_actions_len + morse_actions_len); + } + self.rmk.max_patterns_per_key = max_required_patterns; + + // Update the morse_max_num + self.rmk.morse_max_num = self.rmk.morse_max_num.max(morses.len()); + } + + // Update hold_trigger_key_position_max_num to fit every configured position + if let Some(morse) = &behavior.morse { + let regions = self.layout.as_ref().and_then(|layout| layout.regions.as_ref()); + let configured_len = |positions: Option<&Vec<[u8; 2]>>, names: Option<&Vec>| { + positions.map(Vec::len).unwrap_or(0) + + names + .into_iter() + .flatten() + .filter_map(|name| regions.and_then(|r| r.get(name))) + .map(Vec::len) + .sum::() + }; + let mut total = configured_len( + morse.hold_trigger_key_positions.as_ref(), + morse.hold_trigger_regions.as_ref(), + ); + if let Some(profiles) = &morse.profiles { + for profile in profiles.values() { + total += configured_len( + profile.hold_trigger_key_positions.as_ref(), + profile.hold_trigger_regions.as_ref(), + ); + } + } + self.rmk.hold_trigger_key_position_max_num = self.rmk.hold_trigger_key_position_max_num.max(total); + } + + let auto_mouse_layers = behavior.auto_mouse_layer.as_deref().unwrap_or_default(); + self.rmk.auto_mouse_layer_max_num.get_or_insert(auto_mouse_layers.len()); + + let mouse_layer_scales = behavior.mouse_layer_scale.as_deref().unwrap_or_default(); + self.rmk + .mouse_layer_scale_max_num + .get_or_insert(mouse_layer_scales.len()); + } else { + self.rmk.auto_mouse_layer_max_num.get_or_insert(0); + self.rmk.mouse_layer_scale_max_num.get_or_insert(0); + } + } +} + +/// Keyboard constants configuration for performance and hardware limits +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct RmkConstantsConfig { + /// Mouse key interval (ms) - controls mouse movement speed + #[serde_inline_default(20)] + pub mouse_key_interval: u16, + /// Mouse wheel interval (ms) - controls scrolling speed + #[serde_inline_default(80)] + pub mouse_wheel_interval: u16, + /// Maximum number of combos keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_combo_max_num")] + pub combo_max_num: usize, + /// Maximum number of keys pressed simultaneously in a combo + #[serde_inline_default(4)] + pub combo_max_length: usize, + /// Maximum number of forks for conditional key actions + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_fork_max_num")] + pub fork_max_num: usize, + /// Maximum number of morses keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_morse_max_num")] + pub morse_max_num: usize, + /// Capacity of the morse profile table (named profiles in `[behavior.morse.profiles]`) + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_morse_profile_max_num")] + pub morse_profile_max_num: usize, + /// Capacity of the hold trigger position table, counting every `hold_trigger_key_positions` + /// entry across all profiles. Raised automatically to fit `keyboard.toml`; the default + /// covers a typical home row mod setup at 3 bytes per entry. + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_hold_trigger_key_position_max_num")] + pub hold_trigger_key_position_max_num: usize, + /// Maximum number of patterns a morse key can handle + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_max_patterns_per_key")] + pub max_patterns_per_key: usize, + /// Macro space size in bytes for storing sequences + #[serde_inline_default(256)] + pub macro_space_size: usize, + /// Default debounce time in ms + #[serde_inline_default(20)] + pub debounce_time: u16, + /// Report channel size + #[serde_inline_default(16)] + pub report_channel_size: usize, + /// Vial channel size + #[serde_inline_default(4)] + pub vial_channel_size: usize, + /// Flash channel size + #[serde_inline_default(4)] + pub flash_channel_size: usize, + /// The number of the split peripherals + #[serde_inline_default(0)] + pub split_peripherals_num: usize, + /// The number of available BLE profiles + #[serde_inline_default(3)] + pub ble_profiles_num: usize, + /// BLE Split Central sleep timeout in seconds (0 = disabled) + #[serde_inline_default(0)] + pub split_central_sleep_timeout_seconds: u32, + /// Maximum BLE peripheral latency on external power, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_powered: u16, + /// Maximum BLE peripheral latency on battery, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_battery: u16, + /// Maximum macro data chunk size for protocol transfers (bytes). + /// Smaller values reduce firmware RAM usage but require more round-trips. + #[serde_inline_default(64)] + pub protocol_macro_chunk_size: usize, + /// Maximum number of auto mouse layer entries; auto-derived from `[[behavior.auto_mouse_layer]]` if unset. + #[serde(default)] + pub auto_mouse_layer_max_num: Option, + /// Maximum number of per-layer mouse scaling entries; auto-derived from `[[behavior.mouse_layer_scale]]` if unset. + #[serde(default)] + pub mouse_layer_scale_max_num: Option, + /// Exact RAM of each Rynk RX/TX frame buffer (bytes), payload capacity and bulk counts derive from it. + /// Default 488 fills exactly two BLE notifications. + #[serde_inline_default(488)] + pub rynk_buffer_size: usize, +} + +fn check_combo_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "combo_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_morse_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "morse_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +/// The profile index is a `u8` in `KeyAction::TapHold` and an index with no +/// table entry means "use the default profile", so the table may never cover +/// the full `u8` range: capacity ≤ 255 keeps at least one index always vacant. +fn check_hold_trigger_key_position_max_num<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = usize::deserialize(deserializer)?; + if value > 255 { + panic!( + "❌ Parse `keyboard.toml` error: hold_trigger_key_position_max_num must be between 0 and 255, got {value}" + ); + } + Ok(value) +} + +fn check_morse_profile_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > 255 { + panic!("❌ Parse `keyboard.toml` error: morse_profile_max_num must be between 0 and 255, got {value}"); + } + Ok(value) +} + +fn check_max_patterns_per_key<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if !(4..=65536).contains(&value) { + return Err(de::Error::custom(format!( + "max_patterns_per_key must be between 4 and 65536, got {value}" + ))); + } + Ok(value) +} + +fn check_fork_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "fork_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_split_central_max_latency<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value >= 500 { + return Err(de::Error::custom(format!( + "split_central_max_latency must be between 0 and 499, got {value}" + ))); + } + Ok(value) +} + +/// This separate Default impl is needed when `[rmk]` section is not set in keyboard.toml +impl Default for RmkConstantsConfig { + fn default() -> Self { + Self { + mouse_key_interval: 20, + mouse_wheel_interval: 80, + combo_max_num: 8, + combo_max_length: 4, + fork_max_num: 8, + morse_max_num: 8, + morse_profile_max_num: 16, + hold_trigger_key_position_max_num: 16, + max_patterns_per_key: 8, + macro_space_size: 256, + debounce_time: 20, + report_channel_size: 16, + vial_channel_size: 4, + flash_channel_size: 4, + split_peripherals_num: 0, + ble_profiles_num: 3, + split_central_sleep_timeout_seconds: 0, + split_central_max_latency_powered: 30, + split_central_max_latency_battery: 30, + protocol_macro_chunk_size: 64, + auto_mouse_layer_max_num: None, + mouse_layer_scale_max_num: None, + rynk_buffer_size: 488, + } + } +} + +/// Event channel configuration for a single event type +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct EventChannelConfig { + /// Channel buffer size + pub channel_size: usize, + /// Number of publishers + pub pubs: usize, + /// Number of subscribers + pub subs: usize, +} + +impl Default for EventChannelConfig { + fn default() -> Self { + Self { + channel_size: 1, + pubs: 1, + subs: 1, + } + } +} + +/// Macro to define EventConfig and related code without repetition +macro_rules! define_event_config { + ($($field:ident),* $(,)?) => { + /// Event configuration for all controller events + /// Default values are loaded from event_default.toml + #[derive(Clone, Debug, Deserialize)] + #[serde(deny_unknown_fields, default)] + pub(crate) struct EventConfig { + $(pub $field: EventChannelConfig,)* + } + + /// Cached default EventConfig parsed from event_default.toml + static EVENT_CONFIG_DEFAULTS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + #[derive(Deserialize)] + struct Inner { $($field: EventChannelConfig,)* } + #[derive(Deserialize)] + struct Wrapper { event: Inner } + let w: Wrapper = toml::from_str(EVENT_DEFAULT_CONFIG).expect("Failed to parse event_default.toml"); + EventConfig { $($field: w.event.$field,)* } + }); + + impl Default for EventConfig { + fn default() -> Self { + EVENT_CONFIG_DEFAULTS.clone() + } + } + }; +} + +define_event_config!( + // Connection events + connection_status_change, + // Input events + modifier, + keyboard, + // Keyboard state events + layer_change, + wpm_update, + led_indicator, + sleep_state, + lighting_changed, + // Power events + battery_status, + battery_adc, + charging_state, + // Pointing device events + pointing, + // Split events + peripheral_connected, + central_connected, + peripheral_battery, + clear_peer, + // DFU events + dfu_status, + // Action events + action, +); + +/// The `[layout]` section: the physical key arrangement plus the rendered layout. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayoutTomlConfig { + pub rows: u8, + pub cols: u8, + /// The physical arrangement: an ordered map of `(row,col)` positions with + /// optional hand, shape (`@2u`), gaps (`[1.5]`), row-steps (`[y=]`), and + /// encoders (`(e,0)`). Its order also defines the order of `[[keymap.layer]]`. + pub map: Option, + /// Reusable named sets of matrix coordinates for positional behaviors. + pub regions: Option>>, + // Rendered-layout fields. + pub default_variant: Option, + pub shapes: Option>, + pub variant: Option>, +} + +/// A named shape from `[layout.shapes]`. Every field optional; widths/ +/// heights default to 1u, nudges/rotation to 0, and `w2/h2/x2/y2` are an +/// optional second rectangle for L-shaped caps. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ShapeToml { + pub w: Option, + pub h: Option, + pub x: Option, + pub y: Option, + pub r: Option, + pub w2: Option, + pub h2: Option, + pub x2: Option, + pub y2: Option, +} + +/// One `[[layout.variant]]` render overlay: reshape some keys, hide others. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct VariantToml { + pub name: String, + pub shapes: Option>, + pub hidden: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingTomlConfig { + #[serde(default = "default_topology_revision")] + pub topology_revision: u32, + #[serde(default, rename = "zone")] + pub zones: Vec, + #[serde(default, rename = "output")] + pub outputs: Vec, + #[serde(default, rename = "emitter")] + pub emitters: Vec, + #[serde(default, rename = "layer_scene")] + pub layer_scenes: Vec, + #[serde(default, rename = "conditional_scene")] + pub conditional_scenes: Vec, + pub controls: Option, + pub background: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingControlsTomlConfig { + pub output_toggle_user_action: Option, + pub output_mode_cycle_user_action: Option, + /// Layers that wake lighting while held. A list, since any set of layers + /// may wake it; the host can replace the resolved mask at runtime. + pub wake_layers: Option>, + #[serde(default)] + pub initial_output_mode: LightingOutputModeToml, + #[serde(default)] + pub powered_only_scope: LightingPoweredOnlyScopeToml, + pub output_mode_indicator: Option, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingPoweredOnlyScopeToml { + #[default] + Authority, + Local, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingOutputModeToml { + #[default] + AlwaysOn, + AlwaysOff, + PoweredOnly, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputModeIndicatorTomlConfig { + pub target: LightingTargetTomlConfig, + pub always_on: LightingEffectTomlConfig, + pub always_off: LightingEffectTomlConfig, + pub powered_only: LightingEffectTomlConfig, +} + +fn default_topology_revision() -> u32 { + 1 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingZoneTomlConfig { + pub id: u8, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputTomlConfig { + pub node: u8, + pub id: u8, + pub pixel_count: u16, + pub capabilities: Vec, + #[serde(default)] + pub sparse: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingEmitterTomlConfig { + pub id: u16, + pub key: Option<[u8; 2]>, + pub position: Option<[f32; 3]>, + #[serde(default)] + pub zones: Vec, + pub node: u8, + pub output: u8, + pub physical_index: u16, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerSceneTomlConfig { + pub layer: u8, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingConditionalSceneTomlConfig { + pub layer: Option, + pub battery: Option, + pub output_mode: Option, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerConditionTomlConfig { + pub layer: u8, + #[serde(default = "default_true")] + pub active: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBatteryConditionTomlConfig { + pub node: u8, + pub min_level: Option, + pub max_level: Option, + #[serde(default)] + pub charge: LightingChargeConditionToml, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingChargeConditionToml { + #[default] + Any, + Charging, + Discharging, + Unknown, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingSceneCellTomlConfig { + pub target: LightingTargetTomlConfig, + pub effect: LightingEffectTomlConfig, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub(crate) enum LightingTargetTomlConfig { + Led { led: u16 }, + Key { key: [u8; 2] }, + Zone { zone: u8 }, + All { all: bool }, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub(crate) enum LightingEffectTomlConfig { + Solid { + color: [u8; 3], + }, + Blink { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + duty_percent: u8, + }, + Breathe { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + #[serde(default = "default_breathe_step_ms")] + step_ms: u16, + }, +} + +fn default_breathe_step_ms() -> u16 { + 16 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBackgroundTomlConfig { + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default)] + pub hue: u8, + #[serde(default)] + pub saturation: u8, + #[serde(default = "default_background_value")] + pub value: u8, + #[serde(default = "default_background_speed")] + pub speed: u8, + #[serde(default)] + pub mode: LightingBackgroundModeToml, +} + +fn default_background_value() -> u8 { + 32 +} + +fn default_background_speed() -> u8 { + 128 +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingBackgroundModeToml { + #[default] + Solid, + Breathe, +} + +/// The `[keymap]` section: layer count plus the per-layer key actions. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct KeymapTomlConfig { + /// Total layer count. Optional — defaults to the number of `[[keymap.layer]]` + /// blocks; set it larger to reserve extra empty layers (e.g. for Vial/Rynk). + pub layers: Option, + /// Per-layer key actions: `[[keymap.layer]]`. + #[serde(default)] + pub layer: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayerTomlConfig { + pub name: Option, + pub keys: String, + pub encoders: Option>, +} + +/// Configurations for keyboard info +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct KeyboardInfo { + /// Keyboard name + pub name: String, + /// Vender id + pub vendor_id: u16, + /// Product id + pub product_id: u16, + /// Manufacturer + pub manufacturer: Option, + /// Product name, if not set, it will use `name` as default + pub product_name: Option, + /// Serial number + pub serial_number: Option, + /// Board name(if a supported board is used) + pub board: Option, + /// Chip model + pub chip: Option, + /// enable usb + pub usb_enable: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub enum MatrixType { + #[default] + #[serde(rename = "normal")] + Normal, + #[serde(rename = "direct_pin")] + DirectPin, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DebouncerType { + #[default] + Default, + Fast, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MatrixConfig { + #[serde(default)] + pub matrix_type: MatrixType, + pub row_pins: Option>, + pub col_pins: Option>, + pub direct_pins: Option>>, + #[serde(default = "default_true")] + pub direct_pin_low_active: bool, + #[serde(default = "default_false")] + pub row2col: bool, + #[serde(default)] + pub debouncer: DebouncerType, + pub bootmagic: Option<(u8, u8)>, +} + +/// Config for storage +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct StorageConfig { + /// Start address of local storage, MUST BE start of a sector. + /// If start_addr is set to 0(this is the default value), the last `num_sectors` sectors will be used. + pub start_addr: Option, + // Number of sectors used for storage, >= 2. + pub num_sectors: Option, + #[serde(default = "default_true")] + pub enabled: bool, + // Clear on the storage at reboot, set this to true if you want to reset the keymap + pub clear_storage: Option, + // Clear on the layout at reboot, set this to true if you want to reset the layout + pub clear_layout: Option, +} + +/// Config for DFU partition layout (embassy-boot). +/// +/// These values must match the bootloader's `memory.x` / linker script. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct DfuTomlConfig { + /// Offset of the boot state partition + pub state_offset: Option, + /// Size of the boot state partition + pub state_size: Option, + /// Offset of the DFU download partition + pub dfu_offset: Option, + /// Size of the DFU download partition + pub dfu_size: Option, + /// Flash page size in bytes (e.g. 4096 for RP2040). + /// Used with `flash_size` to auto-calculate partition addresses. + pub page_size: Option, + /// Total flash size in bytes. When set, DFU partition addresses are + /// calculated automatically using the rmk-boot formula. + /// Defaults to 2 MB (2097152) when omitted. + pub flash_size: Option, + /// Optional DFU activity LED pin, e.g. `"PIN_16"`. When set, the LED + /// is lit while a DFU download is in progress. + pub led: Option, + /// Unlock keys for DFU lock (optional) + pub unlock_keys: Option>, +} + +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BleConfig { + pub enabled: bool, + pub battery_adc_pin: Option, + pub charge_state: Option, + pub charge_led: Option, + pub adc_divider_measured: Option, + pub adc_divider_total: Option, + pub default_tx_power: Option, + pub use_2m_phy: Option, + pub passkey_entry: Option, + pub passkey_entry_timeout: Option, +} + +/// Default passkey entry timeout in seconds. +pub const DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 120; + +/// Minimum passkey entry timeout in seconds. +pub const MIN_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 30; + +/// nRF52840 DCDC REG0 output voltage +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum DcdcReg0Voltage { + #[serde(rename = "3V3")] + V3_3, + #[serde(rename = "1V8")] + V1_8, +} + +/// Config for chip-specific settings +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ChipConfig { + /// DCDC regulator 0 enabled (for nrf52840) + pub dcdc_reg0: Option, + /// DCDC regulator 1 enabled (for nrf52840, nrf52833) + pub dcdc_reg1: Option, + /// DCDC regulator 0 voltage (for nrf52840) + pub dcdc_reg0_voltage: Option, +} + +/// Config for lights +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LightConfig { + pub capslock: Option, + pub scrolllock: Option, + pub numslock: Option, +} + +/// Config for a single pin +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PinConfig { + pub pin: String, + pub low_active: bool, +} + +/// Configurations for dependencies +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DependencyConfig { + /// Enable defmt log or not + #[serde(default = "default_true")] + pub defmt_log: bool, +} + +impl Default for DependencyConfig { + fn default() -> Self { + Self { defmt_log: true } + } +} + +/// Intermediate resolved keymap grid (rows/cols/layers + per-layer actions). +/// Built once by `get_keymap_config` and unpacked into `Keymap`; never (de)serialized. +pub(crate) struct KeymapConfig { + pub rows: u8, + pub cols: u8, + pub layers: u8, + pub keymap: Vec>>, + pub encoder_map: Vec>, // Empty if there are no encoders or not configured +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KeyInfo { + pub hand: char, // 'L' or 'R' or other chars +} + +/// Configurations for actions behavior +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct BehaviorConfig { + pub tri_layer: Option, + pub one_shot: Option, + pub one_shot_modifiers: Option, + pub combo: Option, + #[serde(alias = "macro")] + pub macros: Option, + pub fork: Option, + pub morse: Option, + pub auto_mouse_layer: Option>, + pub mouse_layer_scale: Option>, +} + +/// Per-layer mouse movement and scroll scaling. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MouseLayerScaleConfig { + pub layer: u8, + pub r#move: Option<[u16; 2]>, + pub scroll: Option<[u16; 2]>, +} + +/// Configurations for auto mouse layer +/// +/// When motion is detected from a pointing device (e.g. PMW3610), the +/// specified `target_layer` is activated. The layer stays active until +/// `timeout` has elapsed without further motion, then it is deactivated. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct AutoMouseLayerConfig { + /// Pointing device id this entry applies to. When omitted, the entry acts as + /// a fallback for events whose `device_id` matches no other entry. + pub device_id: Option, + /// Layer index to activate on cursor motion + pub target_layer: u8, + /// Idle time after the last cursor motion before the layer is deactivated + /// (e.g. `"500ms"` or `"2s"`). + pub timeout: Option, + /// Minimum absolute axis delta required to be considered as motion. + /// Defaults to `1` (any motion). Helpful to filter out sensor noise. + pub threshold: Option, + /// When `true`, non-mouse key presses deactivate `target_layer` immediately (mouse HID keys and `extra_mouse_keys` excepted). + /// Macro-emitted keycodes, `Again`/`Repeat`, and `GraveEscape` cannot be classified and never deactivate the layer. + pub deactivate_on_key: Option, + /// Extra keycodes (e.g. modifiers) that do not trigger deactivation when `deactivate_on_key` is set. + /// Modifier keycodes listed here also exempt modifier-only actions containing them. + pub extra_mouse_keys: Option>, + /// When `true`, key presses that do NOT deactivate `target_layer` extend the timeout deadline + /// (i.e. reset it to now + `timeout`) at the moment the key's action resolves. + pub reset_timeout_on_key: Option, +} + +/// Per Key configurations profiles for morse, tap-hold, etc. +/// overrides the defaults given in TapHoldConfig +#[derive(Clone, Debug, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseProfile { + pub enable_flow_tap: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + /// overrides the global `[behavior.morse] prior_idle_time` for keys using this profile + pub prior_idle_time: Option, + + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves this tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, + + /// A key outside `hold_trigger_key_positions` settles this tap-hold as a tap when it is + /// released rather than when it is pressed + pub hold_trigger_on_release: Option, +} + +/// Configurations for tri layer +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct TriLayerConfig { + pub upper: u8, + pub lower: u8, + pub adjust: u8, +} + +/// Configurations for oneshot modifiers/layers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OneShotConfig { + pub timeout: Option, +} + +/// Configurations for oneshot modifiers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OneShotModifiersConfig { + pub activate_on_keypress: Option, + pub quick_release: Option, +} + +/// Configurations for combos +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CombosConfig { + #[serde(default)] + pub combos: Vec, + pub timeout: Option, + pub prior_idle_time: Option, +} + +/// Configurations for combo +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ComboConfig { + pub actions: Vec, + pub output: String, + pub layer: Option, +} + +/// Configurations for macros +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacrosConfig { + pub macros: Vec, +} + +/// Configurations for macro +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacroConfig { + pub operations: Vec, +} + +/// Macro operations (TOML deserialization type — resolved equivalent is in `resolved::behavior`) +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "operation", rename_all = "lowercase")] +pub(crate) enum MacroOperation { + Tap { keycode: String }, + Down { keycode: String }, + Up { keycode: String }, + Delay { duration: DurationMillis }, + Text { text: String }, +} + +/// Configurations for forks +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForksConfig { + pub forks: Vec, +} + +/// Configurations for fork +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForkConfig { + pub trigger: String, + pub negative_output: String, + pub positive_output: String, + pub match_any: Option, + pub match_none: Option, + pub kept_modifiers: Option, + pub bindable: Option, +} + +/// Configurations for morse keys +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorsesConfig { + pub enable_flow_tap: Option, //default: false + /// used in permissive_hold mode + pub prior_idle_time: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, + + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves the tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, + /// A key outside `hold_trigger_key_positions` settles the tap-hold as a tap when it is + /// released rather than when it is pressed + pub hold_trigger_on_release: Option, + + /// these can be used to overrides the defaults given above + pub profiles: Option>, + + /// the definition of morse / tap dance keys + pub morses: Option>, +} + +/// Configurations for morse +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseConfig { + // name of morse profile (to address BehaviorConfig::morse.profiles[self.profile]) + pub profile: Option, + + pub tap: Option, + pub hold: Option, + pub hold_after_tap: Option, + pub double_tap: Option, + /// Array of tap actions for each tap count (0-indexed) + pub tap_actions: Option>, + /// Array of hold actions for each tap count (0-indexed) + pub hold_actions: Option>, + /// Array of morse patter->action pairs count (0-indexed) + pub morse_actions: Option>, +} + +/// Configurations for morse action pairs +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseActionPair { + pub pattern: String, // for example morse code of "B": "-..." or "_..." or "1000" + pub action: String, // "B" +} + +/// Split connection transport +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SplitConnection { + #[default] + Ble, + Serial, +} + +/// Configurations for split keyboards +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitConfig { + pub connection: SplitConnection, + pub central: SplitBoardConfig, + pub peripheral: Vec, +} + +/// Configurations for each split board +/// +/// The transport field must match `split.connection`: `serial` is required for +/// serial splits and forbidden for BLE splits; `ble_addr` is optional for BLE +/// splits (dongle setups omit it) and forbidden for serial splits. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitBoardConfig { + /// Row number of the split board + pub rows: usize, + /// Col number of the split board + pub cols: usize, + /// Row offset of the split board + pub row_offset: usize, + /// Col offset of the split board + pub col_offset: usize, + /// Ble address + pub ble_addr: Option<[u8; 6]>, + /// Serial config, the vector length should be 1 for peripheral + pub serial: Option>, + /// Matrix config for the split + pub matrix: MatrixConfig, + /// Input device config for the split + pub input_device: Option, + /// Display config for the split board + pub display: Option, + /// Battery ADC pin for this split board + pub battery_adc_pin: Option, + /// ADC divider measured value for battery + pub adc_divider_measured: Option, + /// ADC divider total value for battery + pub adc_divider_total: Option, + /// Output Pin config for the split + pub output: Option>, + /// Path to the peripheral firmware binary for automatic dfu_split update. + /// Relative to the project's `Cargo.toml`. When set, the generated code + /// includes the binary with `include_bytes!` and registers it via + /// [`set_firmware_update_data`](crate::set_firmware_update_data). + pub firmware: Option, + /// DFU update policy for this peripheral. "MatchHash" (default) only + /// flashes when the firmware differs; "force" always flashes. + pub update_policy: Option, +} + +/// Serial port config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SerialConfig { + pub instance: String, + pub tx_pin: String, + pub rx_pin: String, +} + +/// Duration in milliseconds +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct DurationMillis(#[serde(deserialize_with = "parse_duration_millis")] pub u64); + +const fn default_true() -> bool { + true +} + +const fn default_false() -> bool { + false +} + +const fn default_pointing_report_hz() -> u16 { + 125 +} + +fn parse_duration_millis<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { + let input: String = de::Deserialize::deserialize(deserializer)?; + let num = input.trim_end_matches(|c: char| !c.is_numeric()); + let unit = &input[num.len()..]; + let num: u64 = num.parse().map_err(|_| { + de::Error::custom(format!( + "Invalid number \"{num}\" in duration: number part must be a u64" + )) + })?; + + match unit { + "s" => Ok(num * 1000), + "ms" => Ok(num), + other => Err(de::Error::custom(format!( + "Invalid duration unit \"{other}\": unit part must be either \"s\" or \"ms\"" + ))), + } +} + +/// Configuration for host tools +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct HostConfig { + /// Whether Vial is enabled + #[serde_inline_default(true)] + pub vial_enabled: bool, + /// Whether the RMK-native Rynk protocol is enabled. Mutually exclusive + /// with `vial_enabled` (the underlying Cargo features conflict). + #[serde_inline_default(false)] + pub rynk_enabled: bool, + /// Physical keys (row, col) held simultaneously to unlock (optional). + /// Shared by the Vial lock and the Rynk lock gate. + pub unlock_keys: Option>, + /// Start (and stay) unlocked, bypassing the unlock-key combo (default: + /// false). Renamed from `vial_insecure`; the old name still parses. + #[serde(alias = "vial_insecure")] + #[serde_inline_default(false)] + pub insecure: bool, + /// Move the Rynk config-write tier (`SetKeyAction`, `SetMacro`, …) into the + /// locked set, so writes also require unlock (default: false). + #[serde_inline_default(false)] + pub write_requires_unlock: bool, + /// Require the Rynk physical-presence unlock before entering either the + /// central or a split peripheral bootloader (default: true). + #[serde_inline_default(true)] + pub bootloader_requires_unlock: bool, +} + +impl Default for HostConfig { + fn default() -> Self { + Self { + vial_enabled: true, + rynk_enabled: false, + unlock_keys: None, + insecure: false, + write_requires_unlock: false, + bootloader_requires_unlock: true, + } + } +} + +/// Configurations for input devices +/// +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InputDeviceConfig { + pub encoder: Option>, + pub pointing: Option>, + pub joystick: Option>, + pub pmw3610: Option>, + pub pmw33xx: Option>, + pub iqs5xx: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct JoystickConfig { + // Name of the joystick + pub name: String, + /// Device id used to match this joystick with its JoystickProcessor. + /// If omitted, ids are assigned sequentially starting from 0. + pub id: Option, + // Pin a of the joystick + pub pin_x: String, + // Pin b of the joystick + pub pin_y: String, + // Pin z of the joystick + pub pin_z: String, + pub transform: Vec>, + pub bias: Vec, + pub resolution: u16, +} + +/// PMW3610 optical mouse sensor configuration +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw3610Config { + /// Name of the sensor (used for variable naming) + pub name: String, + /// id of the device + pub id: Option, + /// SPI pins + pub spi: SpiConfig, + /// Optional motion interrupt pin + pub motion: Option, + /// CPI resolution (200-3200, step 200). Optional, uses sensor default if not set. + pub cpi: Option, + /// Invert X axis + #[serde(default)] + pub invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub swap_xy: bool, + /// Force awake mode (disable power saving) + #[serde(default)] + pub force_awake: bool, + /// Enable smart mode for better tracking on shiny surfaces + #[serde(default)] + pub smart_mode: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Pmw33xxType { + #[default] + PMW3360, + PMW3389, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw33xxConfig { + // Name of the sensor (used for variable naming) + pub name: String, + // id of the device + pub id: Option, + // Sensor Type (3360 or 3389) + pub sensor_type: Pmw33xxType, + // SPI pins + pub spi: SpiConfig, + // Optional motion interrupt pin + pub motion: Option, + // CPI resolution (100-12000, step 100).Optional, uses sensor default 1600 if not set. + pub cpi: Option, + // Rotational transform angle (-127 to 127) Optional, uses sensor default 0 if not set. + pub rot_trans_angle: Option, + // liftoff distance. Optional, uses sensor default 0 if not set. + pub liftoff_dist: Option, + // Invert X axis + #[serde(default)] + pub proc_invert_x: bool, + // Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + // Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, +} + +/// Azoteq IQS5xx trackpad configuration. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxConfig { + /// Name of the trackpad (used for variable naming). + pub name: String, + /// RMK pointing-device id (0-255). Defaults to 0. + pub id: Option, + /// I²C bus the trackpad is connected to. The bus is dedicated to this + /// device — sharing with other I²C peripherals (e.g. an OLED) is not yet + /// supported via TOML. + pub i2c: Iqs5xxI2cConfig, + /// Optional `RDY` pin. Strongly recommended; without it the driver falls + /// back to timed polling and may stall the bus through clock-stretching. + pub rdy: Option, + /// Invert X in the PointingProcessor. + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y in the PointingProcessor. + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y in the PointingProcessor. + #[serde(default)] + pub proc_swap_xy: bool, +} + +/// I²C bus configuration for the IQS5xx. Distinct from the generic `I2cConfig` +/// because the IQS5xx address is fixed (`0x74` by default; can be reprogrammed +/// at the IC, but not at runtime — exposing it would be misleading). +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxI2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EncoderConfig { + // Pin a of the encoder + pub pin_a: String, + // Pin b of the encoder + pub pin_b: String, + // Phase is the working mode of the rotary encoders. + // Available mode: + // - default: resolution = 1 + // - e8h7: phase table tuned for E8H7 encoders + // - resolution: customized resolution, the resolution value and reverse should be specified + // A typical [EC11 encoder](https://tech.alpsalpine.com/cms.media/product_catalog_ec_01_ec11e_en_611f078659.pdf)'s resolution is 2 + // In resolution mode, you can also specify the number of detent and pulses, the resolution will be calculated by `pulse * 4 / detent` + #[serde(default)] + pub phase: EncoderPhase, + // Resolution + pub resolution: Option, + // The number of detent + pub detent: Option, + // The number of pulse + pub pulse: Option, + // Whether the direction of the rotary encoder is reversed. + pub reverse: Option, + // Use MCU's internal pull-up resistor or not, defaults to false, the external pull-up resistor is needed + #[serde(default = "default_false")] + pub internal_pullup: bool, + // Debounce interval in milliseconds. Suppresses spurious events from mechanical contact bounce. + // Defaults to 0 (disabled) if not specified. + pub debounce_ms: Option, +} + +/// Rotary encoder phase (decoding) mode +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum EncoderPhase { + #[default] + Default, + E8h7, + Resolution, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields, untagged)] +pub enum EncoderResolution { + Value(u8), + Derived { detent: u8, pulse: u8 }, +} + +impl Default for EncoderResolution { + fn default() -> Self { + Self::Value(4) + } +} + +/// Pointing device config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PointingDeviceConfig { + pub interface: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CommunicationProtocol { + I2c(I2cConfig), + Spi(SpiConfig), +} + +/// SPI config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SpiConfig { + pub instance: String, + pub sck: String, + pub mosi: String, + pub miso: String, + pub cs: Option, + pub cpi: Option, + pub tx_dma: Option, + pub rx_dma: Option, +} + +/// I2C config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct I2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, + /// 7-bit I2C address. Defaults to 0x3C when omitted. + #[serde(default = "default_i2c_address")] + pub address: u8, +} + +const fn default_i2c_address() -> u8 { + 0x3C +} + +/// Display driver type +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DisplayDriver { + Ssd1306, + Sh1106, + Sh1107, + Sh1108, + Ssd1309, +} + +/// Display configuration +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DisplayConfig { + pub driver: DisplayDriver, + pub protocol: CommunicationProtocol, + pub size: String, + #[serde(default)] + pub rotation: u16, + pub renderer: Option, + /// Poll interval in milliseconds for periodic redraws (animations). + /// When absent, polling is disabled — the display only redraws on events. + pub render_interval: Option, + /// Minimum time in milliseconds between event-driven renders. + /// Prevents the display from being hammered by rapid events. Default: 10 ms. + pub min_render_interval: Option, +} + +/// Configuration for an output pin +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OutputConfig { + pub pin: String, + #[serde(default)] + pub low_active: bool, + #[serde(default)] + pub initial_state_active: bool, +} + +impl KeyboardTomlConfig { + pub(crate) fn get_output_config(&self) -> Result, String> { + let output_config = self.output.clone(); + let split = self.split.clone(); + match (output_config, split) { + (None, Some(s)) => Ok(s.central.output.unwrap_or_default()), + (Some(c), None) => Ok(c), + (None, None) => Ok(Default::default()), + _ => Err("Use [[split.output]] to define outputs for split in your keyboard.toml!".to_string()), + } + } + + pub(crate) fn get_dependency_config(&self) -> DependencyConfig { + self.dependency.clone().unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_event_config_default_values() { + let config = EventConfig::default(); + + // Check some key default values from event_default.toml + assert_eq!(config.keyboard.channel_size, 16); + assert_eq!(config.keyboard.pubs, 2); + assert_eq!(config.keyboard.subs, 3); + + assert_eq!(config.modifier.channel_size, 8); + assert_eq!(config.modifier.pubs, 1); + assert_eq!(config.modifier.subs, 2); + + assert_eq!(config.layer_change.channel_size, 1); + assert_eq!(config.layer_change.subs, 1); + + assert_eq!(config.led_indicator.channel_size, 2); + assert_eq!(config.led_indicator.pubs, 2); + assert_eq!(config.led_indicator.subs, 3); + + assert_eq!(config.lighting_changed.channel_size, 1); + assert_eq!(config.lighting_changed.pubs, 1); + assert_eq!(config.lighting_changed.subs, 1); + + assert_eq!(config.pointing.channel_size, 8); + assert_eq!(config.pointing.subs, 2); + + assert_eq!(config.action.channel_size, 16); + assert_eq!(config.action.pubs, 1); + assert_eq!(config.action.subs, 0); + } + + #[test] + fn test_event_config_user_override() { + // Simulate user config that overrides some event settings + let user_toml = r#" +[event.keyboard] +channel_size = 32 +"#; + // Parse with event defaults first, then user config + let config: KeyboardTomlConfig = Config::builder() + .add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)) + .add_source(File::from_str(user_toml, FileFormat::Toml)) + .build() + .unwrap() + .try_deserialize() + .unwrap(); + + // User-overridden values + assert_eq!(config.event.keyboard.channel_size, 32); + assert_eq!(config.event.keyboard.pubs, 2); + assert_eq!(config.event.keyboard.subs, 3); + + // Non-overridden values should use defaults + assert_eq!(config.event.modifier.channel_size, 8); + assert_eq!(config.event.modifier.subs, 2); + assert_eq!(config.event.layer_change.subs, 1); + } + + #[test] + fn rmk_count_limits_fit_u8_capability_fields() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +combo_max_num = 255 +morse_max_num = 255 +fork_max_num = 255 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.combo_max_num, 255); + assert_eq!(ok.rmk.morse_max_num, 255); + assert_eq!(ok.rmk.fork_max_num, 255); + + for (field, message) in [ + ("combo_max_num", "combo_max_num must be between 0 and 255"), + ("morse_max_num", "morse_max_num must be between 0 and 255"), + ("fork_max_num", "fork_max_num must be between 0 and 255"), + ] { + let toml = format!("[rmk]\n{field} = 256\n"); + let err = toml::from_str::(&toml).unwrap_err(); + assert!(err.to_string().contains(message), "{err}"); + } + } + + #[test] + fn split_central_max_latency_matches_ble_limit() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +split_central_max_latency_powered = 499 +split_central_max_latency_battery = 498 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.split_central_max_latency_powered, 499); + assert_eq!(ok.rmk.split_central_max_latency_battery, 498); + + let err = toml::from_str::( + r#" +[rmk] +split_central_max_latency_battery = 500 +"#, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("split_central_max_latency must be between 0 and 499"), + "{err}" + ); + } + + #[test] + fn test_event_config_partial_override_with_event_defaults_loader() { + let user_toml = r#" +[event.layer_change] +subs = 2 +"#; + let path = std::env::temp_dir().join(format!( + "rmk-event-defaults-loader-{}-{}.toml", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::write(&path, user_toml).unwrap(); + + let config = KeyboardTomlConfig::new_from_toml_path_with_event_defaults(&path); + std::fs::remove_file(path).unwrap(); + + assert_eq!(config.event.layer_change.channel_size, 1); + assert_eq!(config.event.layer_change.pubs, 2); + assert_eq!(config.event.layer_change.subs, 2); + } +} diff --git a/resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/preimage.1 b/resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/preimage.1 new file mode 100644 index 0000000..ea08704 --- /dev/null +++ b/resolutions/rerere/9d8fc76b977762dc5334289367875026faf74445/preimage.1 @@ -0,0 +1,1907 @@ +use std::collections::HashMap; +use std::path::Path; + +use config::{Config, File, FileFormat}; +use serde::{Deserialize, de}; +use serde_inline_default::serde_inline_default; + +/// Event channel default configuration +const EVENT_DEFAULT_CONFIG: &str = include_str!("default_config/event_default.toml"); + +pub(crate) mod chip; +pub(crate) mod communication; +pub mod resolved; +#[rustfmt::skip] +pub mod usb_interrupt_map; +pub(crate) mod behavior; +pub(crate) mod board; +pub(crate) mod dfu; +pub(crate) mod display; +pub(crate) mod host; +pub(crate) mod keycode_alias; +pub(crate) mod keymap; +pub mod layout; +pub use layout::{STOCK_WIDTHS, layout_blob_from_toml, layout_info_from_toml}; +pub(crate) mod light; +pub(crate) mod storage; + +/// Protocol-level capacity ceilings for wire-format Vec sizes. +/// +/// These define the maximum values any firmware may use for protocol +/// Vec capacities (`COMBO_SIZE`, `MORSE_SIZE`, etc.). The host tool compiles +/// against these as upper bounds. Any firmware with `rynk` enabled +/// must satisfy `value <= ceiling` at compile time. +/// +/// Constant names mirror the generated constants with a `MAX_` prefix: +/// `COMBO_SIZE` is bounded by `MAX_COMBO_SIZE`, etc. +pub mod protocol_limits { + /// Max keys in a combo trigger — ceiling for `COMBO_SIZE` + pub const MAX_COMBO_SIZE: usize = 16; + /// Max pattern entries per morse key — ceiling for `MORSE_SIZE` + pub const MAX_MORSE_SIZE: usize = 32; + /// Max bytes per macro data chunk — ceiling for `MACRO_DATA_SIZE` + pub const MAX_MACRO_DATA_SIZE: usize = 256; + /// Max key positions in an unlock challenge. + pub const MAX_UNLOCK_KEYS_SIZE: usize = 4; +} + +pub(crate) fn validate_unlock_keys( + section: &str, + unlock_keys: &[[u8; 2]], + layout: Option<&LayoutTomlConfig>, +) -> Result<(), String> { + if unlock_keys.len() > protocol_limits::MAX_UNLOCK_KEYS_SIZE { + return Err(format!( + "{section}.unlock_keys has {} entries, the max is {}", + unlock_keys.len(), + protocol_limits::MAX_UNLOCK_KEYS_SIZE + )); + } + + if let Some(layout) = layout { + for key in unlock_keys { + let (row, col) = (key[0], key[1]); + if row >= layout.rows || col >= layout.cols { + return Err(format!( + "{section}.unlock_keys position ({row}, {col}) is outside the {}x{} matrix", + layout.rows, layout.cols + )); + } + } + } + + Ok(()) +} + +/// Configurations for RMK keyboard. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub struct KeyboardTomlConfig { + /// Basic keyboard info + keyboard: Option, + /// Matrix of the keyboard, only for non-split keyboards + matrix: Option, + // Aliases for key maps + aliases: Option>, + /// Keymap config: layer count and the per-layer key actions (`[[keymap.layer]]`). + keymap: Option, + /// Layout config: the physical key arrangement (`map`) plus the rendered layout. + /// For split keyboards, the total row/col is defined in this section. + layout: Option, + /// Topology-aware lighting. Key geometry is always derived from + /// `[layout].map`; emitters add semantic identity and electrical routing. + lighting: Option, + /// Behavior config + behavior: Option, + /// Light config + light: Option, + /// Storage config + storage: Option, + /// DFU partition config (embassy-boot) + dfu: Option, + /// Ble config + pub(crate) ble: Option, + /// Chip-specific configs (e.g., [chip.nrf52840]) + chip: Option>, + /// Dependency config + dependency: Option, + /// Split config + split: Option, + /// Input device config + input_device: Option, + /// Display config + display: Option, + /// Output Pin config + output: Option>, + /// Set host configurations + pub(crate) host: Option, + /// RMK config constants + #[serde(default)] + pub(crate) rmk: RmkConstantsConfig, + /// Event channel configuration + /// Default values are loaded from event_default.toml in new_from_toml_path() + /// build.rs also loads event defaults via new_from_toml_path_with_event_defaults() + #[serde(default)] + pub(crate) event: EventConfig, + /// Whether the user explicitly set a [storage] section in keyboard.toml. + #[serde(skip)] + pub(crate) storage_user_set: bool, +} + +impl KeyboardTomlConfig { + fn parse_from_toml_path>(config_toml_path: P, chip_default_config: Option<&str>) -> Self { + let path = config_toml_path.as_ref(); + let path_str = path + .to_str() + .unwrap_or_else(|| panic!("Config path is not valid UTF-8: {:?}", path)); + + let mut builder = Config::builder().add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)); + if let Some(default_config) = chip_default_config { + builder = builder.add_source(File::from_str(default_config, FileFormat::Toml)); + } + builder + .add_source(File::with_name(path_str)) + .build() + .unwrap_or_else(|e| panic!("Parse {:?} error: {}", path, e)) + .try_deserialize() + .unwrap_or_else(|e| panic!("Deserialize {:?} error: {}", path, e)) + } + + /// Load keyboard.toml with event defaults only. + /// + /// This is used in build.rs where we only need [rmk] and [event] constants, + /// and should not require `[keyboard.board]`/`[keyboard.chip]`. + pub fn new_from_toml_path_with_event_defaults>(config_toml_path: P) -> Self { + let mut config = Self::parse_from_toml_path(config_toml_path, None); + config.storage_user_set = config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + config.auto_calculate_parameters(); + config + } + + pub fn new_from_toml_path>(config_toml_path: P) -> Self { + let path = config_toml_path.as_ref(); + + // First pass: load user config with event defaults to get chip model. + // This allows user's keyboard.toml to omit [event] section. + let user_config = Self::parse_from_toml_path(path, None); + + let default_config_str = user_config + .get_chip_model() + .and_then(|chip| chip.get_default_config_str()) + .unwrap_or_else(|e| panic!("❌ keyboard.toml error: {e}")); + + // Second pass: load with all three config sources + // Config priority (later sources override earlier ones): + // 1. Event default config (lowest priority) + // 2. Chip-specific default config + // 3. User config (highest priority) + let mut config = Self::parse_from_toml_path(path, Some(default_config_str)); + config.storage_user_set = user_config + .storage + .as_ref() + .is_some_and(|s| s.start_addr.is_some() || s.num_sectors.is_some()); + + config.auto_calculate_parameters(); + + config + } + + /// Auto calculate some parameters in toml: + /// - Update morse_max_num to fit all configured morses + /// - Update max_patterns_per_key to fit the max number of configured (pattern, action) pairs per morse key + /// - Update peripheral number based on the number of split boards + /// - TODO: Update controller number based on the number of split boards + pub(crate) fn auto_calculate_parameters(&mut self) { + // Update the number of peripherals + if let Some(split) = &self.split + && split.peripheral.len() > self.rmk.split_peripherals_num + { + // eprintln!( + // "The number of split peripherals is updated to {} from {}", + // split.peripheral.len(), + // self.rmk.split_peripherals_num + // ); + self.rmk.split_peripherals_num = split.peripheral.len(); + } + + if let Some(behavior) = &self.behavior { + // Update the max_patterns_per_key + if let Some(morse) = &behavior.morse + && let Some(morses) = &morse.morses + { + let mut max_required_patterns = self.rmk.max_patterns_per_key; + + for morse in morses { + let tap_actions_len = morse.tap_actions.as_ref().map(|v| v.len()).unwrap_or(0); + let hold_actions_len = morse.hold_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + let n = tap_actions_len.max(hold_actions_len); + if n > 15 { + panic!("The number of taps per morse is too large, the max number of taps is 15, got {n}"); + } + + let morse_actions_len = morse.morse_actions.as_ref().map(|v| v.len()).unwrap_or(0); + + max_required_patterns = + max_required_patterns.max(tap_actions_len + hold_actions_len + morse_actions_len); + } + self.rmk.max_patterns_per_key = max_required_patterns; + + // Update the morse_max_num + self.rmk.morse_max_num = self.rmk.morse_max_num.max(morses.len()); + } + + // Update hold_trigger_key_position_max_num to fit every configured position + if let Some(morse) = &behavior.morse { + let regions = self.layout.as_ref().and_then(|layout| layout.regions.as_ref()); + let configured_len = |positions: Option<&Vec<[u8; 2]>>, names: Option<&Vec>| { + positions.map(Vec::len).unwrap_or(0) + + names + .into_iter() + .flatten() + .filter_map(|name| regions.and_then(|r| r.get(name))) + .map(Vec::len) + .sum::() + }; + let mut total = configured_len( + morse.hold_trigger_key_positions.as_ref(), + morse.hold_trigger_regions.as_ref(), + ); + if let Some(profiles) = &morse.profiles { + for profile in profiles.values() { + total += configured_len( + profile.hold_trigger_key_positions.as_ref(), + profile.hold_trigger_regions.as_ref(), + ); + } + } + self.rmk.hold_trigger_key_position_max_num = self.rmk.hold_trigger_key_position_max_num.max(total); + } + + let auto_mouse_layers = behavior.auto_mouse_layer.as_deref().unwrap_or_default(); + self.rmk.auto_mouse_layer_max_num.get_or_insert(auto_mouse_layers.len()); + + let mouse_layer_scales = behavior.mouse_layer_scale.as_deref().unwrap_or_default(); + self.rmk + .mouse_layer_scale_max_num + .get_or_insert(mouse_layer_scales.len()); + } else { + self.rmk.auto_mouse_layer_max_num.get_or_insert(0); + self.rmk.mouse_layer_scale_max_num.get_or_insert(0); + } + } +} + +/// Keyboard constants configuration for performance and hardware limits +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct RmkConstantsConfig { + /// Mouse key interval (ms) - controls mouse movement speed + #[serde_inline_default(20)] + pub mouse_key_interval: u16, + /// Mouse wheel interval (ms) - controls scrolling speed + #[serde_inline_default(80)] + pub mouse_wheel_interval: u16, + /// Maximum number of combos keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_combo_max_num")] + pub combo_max_num: usize, + /// Maximum number of keys pressed simultaneously in a combo + #[serde_inline_default(4)] + pub combo_max_length: usize, + /// Maximum number of forks for conditional key actions + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_fork_max_num")] + pub fork_max_num: usize, + /// Maximum number of morses keyboard can store + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_morse_max_num")] + pub morse_max_num: usize, + /// Capacity of the morse profile table (named profiles in `[behavior.morse.profiles]`) + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_morse_profile_max_num")] + pub morse_profile_max_num: usize, + /// Capacity of the hold trigger position table, counting every `hold_trigger_key_positions` + /// entry across all profiles. Raised automatically to fit `keyboard.toml`; the default + /// covers a typical home row mod setup at 3 bytes per entry. + #[serde_inline_default(16)] + #[serde(deserialize_with = "check_hold_trigger_key_position_max_num")] + pub hold_trigger_key_position_max_num: usize, + /// Maximum number of patterns a morse key can handle + #[serde_inline_default(8)] + #[serde(deserialize_with = "check_max_patterns_per_key")] + pub max_patterns_per_key: usize, + /// Macro space size in bytes for storing sequences + #[serde_inline_default(256)] + pub macro_space_size: usize, + /// Default debounce time in ms + #[serde_inline_default(20)] + pub debounce_time: u16, + /// Report channel size + #[serde_inline_default(16)] + pub report_channel_size: usize, + /// Vial channel size + #[serde_inline_default(4)] + pub vial_channel_size: usize, + /// Flash channel size + #[serde_inline_default(4)] + pub flash_channel_size: usize, + /// The number of the split peripherals + #[serde_inline_default(0)] + pub split_peripherals_num: usize, + /// The number of available BLE profiles + #[serde_inline_default(3)] + pub ble_profiles_num: usize, + /// BLE Split Central sleep timeout in seconds (0 = disabled) + #[serde_inline_default(0)] + pub split_central_sleep_timeout_seconds: u32, + /// Maximum BLE peripheral latency on external power, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_powered: u16, + /// Maximum BLE peripheral latency on battery, in active connection events. + #[serde_inline_default(30)] + #[serde(deserialize_with = "check_split_central_max_latency")] + pub split_central_max_latency_battery: u16, + /// Maximum macro data chunk size for protocol transfers (bytes). + /// Smaller values reduce firmware RAM usage but require more round-trips. + #[serde_inline_default(64)] + pub protocol_macro_chunk_size: usize, + /// Maximum number of auto mouse layer entries; auto-derived from `[[behavior.auto_mouse_layer]]` if unset. + #[serde(default)] + pub auto_mouse_layer_max_num: Option, + /// Maximum number of per-layer mouse scaling entries; auto-derived from `[[behavior.mouse_layer_scale]]` if unset. + #[serde(default)] + pub mouse_layer_scale_max_num: Option, + /// Exact RAM of each Rynk RX/TX frame buffer (bytes), payload capacity and bulk counts derive from it. + /// Default 488 fills exactly two BLE notifications. + #[serde_inline_default(488)] + pub rynk_buffer_size: usize, +} + +fn check_combo_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "combo_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_morse_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "morse_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +/// The profile index is a `u8` in `KeyAction::TapHold` and an index with no +/// table entry means "use the default profile", so the table may never cover +/// the full `u8` range: capacity ≤ 255 keeps at least one index always vacant. +fn check_hold_trigger_key_position_max_num<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = usize::deserialize(deserializer)?; + if value > 255 { + panic!( + "❌ Parse `keyboard.toml` error: hold_trigger_key_position_max_num must be between 0 and 255, got {value}" + ); + } + Ok(value) +} + +fn check_morse_profile_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > 255 { + panic!("❌ Parse `keyboard.toml` error: morse_profile_max_num must be between 0 and 255, got {value}"); + } + Ok(value) +} + +fn check_max_patterns_per_key<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if !(4..=65536).contains(&value) { + return Err(de::Error::custom(format!( + "max_patterns_per_key must be between 4 and 65536, got {value}" + ))); + } + Ok(value) +} + +fn check_fork_max_num<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value > u8::MAX as usize { + return Err(de::Error::custom(format!( + "fork_max_num must be between 0 and 255, got {value}" + ))); + } + Ok(value) +} + +fn check_split_central_max_latency<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let value = Deserialize::deserialize(deserializer)?; + if value >= 500 { + return Err(de::Error::custom(format!( + "split_central_max_latency must be between 0 and 499, got {value}" + ))); + } + Ok(value) +} + +/// This separate Default impl is needed when `[rmk]` section is not set in keyboard.toml +impl Default for RmkConstantsConfig { + fn default() -> Self { + Self { + mouse_key_interval: 20, + mouse_wheel_interval: 80, + combo_max_num: 8, + combo_max_length: 4, + fork_max_num: 8, + morse_max_num: 8, + morse_profile_max_num: 16, + hold_trigger_key_position_max_num: 16, + max_patterns_per_key: 8, + macro_space_size: 256, + debounce_time: 20, + report_channel_size: 16, + vial_channel_size: 4, + flash_channel_size: 4, + split_peripherals_num: 0, + ble_profiles_num: 3, + split_central_sleep_timeout_seconds: 0, + split_central_max_latency_powered: 30, + split_central_max_latency_battery: 30, + protocol_macro_chunk_size: 64, + auto_mouse_layer_max_num: None, + mouse_layer_scale_max_num: None, + rynk_buffer_size: 488, + } + } +} + +/// Event channel configuration for a single event type +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct EventChannelConfig { + /// Channel buffer size + pub channel_size: usize, + /// Number of publishers + pub pubs: usize, + /// Number of subscribers + pub subs: usize, +} + +impl Default for EventChannelConfig { + fn default() -> Self { + Self { + channel_size: 1, + pubs: 1, + subs: 1, + } + } +} + +/// Macro to define EventConfig and related code without repetition +macro_rules! define_event_config { + ($($field:ident),* $(,)?) => { + /// Event configuration for all controller events + /// Default values are loaded from event_default.toml + #[derive(Clone, Debug, Deserialize)] + #[serde(deny_unknown_fields, default)] + pub(crate) struct EventConfig { + $(pub $field: EventChannelConfig,)* + } + + /// Cached default EventConfig parsed from event_default.toml + static EVENT_CONFIG_DEFAULTS: std::sync::LazyLock = std::sync::LazyLock::new(|| { + #[derive(Deserialize)] + struct Inner { $($field: EventChannelConfig,)* } + #[derive(Deserialize)] + struct Wrapper { event: Inner } + let w: Wrapper = toml::from_str(EVENT_DEFAULT_CONFIG).expect("Failed to parse event_default.toml"); + EventConfig { $($field: w.event.$field,)* } + }); + + impl Default for EventConfig { + fn default() -> Self { + EVENT_CONFIG_DEFAULTS.clone() + } + } + }; +} + +define_event_config!( + // Connection events + connection_status_change, + // Input events + modifier, + keyboard, + // Keyboard state events + layer_change, + wpm_update, + led_indicator, + sleep_state, + lighting_changed, + // Power events + battery_status, + battery_adc, + charging_state, + // Pointing device events + pointing, + // Split events + peripheral_connected, + central_connected, + peripheral_battery, + clear_peer, + // DFU events + dfu_status, + // Action events + action, +); + +/// The `[layout]` section: the physical key arrangement plus the rendered layout. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayoutTomlConfig { + pub rows: u8, + pub cols: u8, + /// The physical arrangement: an ordered map of `(row,col)` positions with + /// optional hand, shape (`@2u`), gaps (`[1.5]`), row-steps (`[y=]`), and + /// encoders (`(e,0)`). Its order also defines the order of `[[keymap.layer]]`. + pub map: Option, + /// Reusable named sets of matrix coordinates for positional behaviors. + pub regions: Option>>, + // Rendered-layout fields. + pub default_variant: Option, + pub shapes: Option>, + pub variant: Option>, +} + +/// A named shape from `[layout.shapes]`. Every field optional; widths/ +/// heights default to 1u, nudges/rotation to 0, and `w2/h2/x2/y2` are an +/// optional second rectangle for L-shaped caps. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ShapeToml { + pub w: Option, + pub h: Option, + pub x: Option, + pub y: Option, + pub r: Option, + pub w2: Option, + pub h2: Option, + pub x2: Option, + pub y2: Option, +} + +/// One `[[layout.variant]]` render overlay: reshape some keys, hide others. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct VariantToml { + pub name: String, + pub shapes: Option>, + pub hidden: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingTomlConfig { + #[serde(default = "default_topology_revision")] + pub topology_revision: u32, + #[serde(default, rename = "zone")] + pub zones: Vec, + #[serde(default, rename = "output")] + pub outputs: Vec, + #[serde(default, rename = "emitter")] + pub emitters: Vec, + #[serde(default, rename = "layer_scene")] + pub layer_scenes: Vec, + #[serde(default, rename = "conditional_scene")] + pub conditional_scenes: Vec, + pub controls: Option, + pub background: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingControlsTomlConfig { + pub output_toggle_user_action: Option, + pub output_mode_cycle_user_action: Option, + /// Layers that wake lighting while held. A list, since any set of layers + /// may wake it; the host can replace the resolved mask at runtime. + pub wake_layers: Option>, + #[serde(default)] + pub initial_output_mode: LightingOutputModeToml, + #[serde(default)] + pub powered_only_scope: LightingPoweredOnlyScopeToml, + pub output_mode_indicator: Option, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingPoweredOnlyScopeToml { + #[default] + Authority, + Local, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingOutputModeToml { + #[default] + AlwaysOn, + AlwaysOff, + PoweredOnly, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputModeIndicatorTomlConfig { + pub target: LightingTargetTomlConfig, + pub always_on: LightingEffectTomlConfig, + pub always_off: LightingEffectTomlConfig, + pub powered_only: LightingEffectTomlConfig, +} + +fn default_topology_revision() -> u32 { + 1 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingZoneTomlConfig { + pub id: u8, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingOutputTomlConfig { + pub node: u8, + pub id: u8, + pub pixel_count: u16, + pub capabilities: Vec, + #[serde(default)] + pub sparse: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingEmitterTomlConfig { + pub id: u16, + pub key: Option<[u8; 2]>, + pub position: Option<[f32; 3]>, + #[serde(default)] + pub zones: Vec, + pub node: u8, + pub output: u8, + pub physical_index: u16, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerSceneTomlConfig { + pub layer: u8, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingConditionalSceneTomlConfig { + pub layer: Option, + pub battery: Option, + pub output_mode: Option, + #[serde(default, rename = "cell")] + pub cells: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingLayerConditionTomlConfig { + pub layer: u8, + #[serde(default = "default_true")] + pub active: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBatteryConditionTomlConfig { + pub node: u8, + pub min_level: Option, + pub max_level: Option, + #[serde(default)] + pub charge: LightingChargeConditionToml, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingChargeConditionToml { + #[default] + Any, + Charging, + Discharging, + Unknown, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingSceneCellTomlConfig { + pub target: LightingTargetTomlConfig, + pub effect: LightingEffectTomlConfig, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub(crate) enum LightingTargetTomlConfig { + Led { led: u16 }, + Key { key: [u8; 2] }, + Zone { zone: u8 }, + All { all: bool }, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub(crate) enum LightingEffectTomlConfig { + Solid { + color: [u8; 3], + }, + Blink { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + duty_percent: u8, + }, + Breathe { + color: [u8; 3], + period_ms: u32, + #[serde(default)] + phase_ms: u32, + #[serde(default = "default_breathe_step_ms")] + step_ms: u16, + }, +} + +fn default_breathe_step_ms() -> u16 { + 16 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LightingBackgroundTomlConfig { + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default)] + pub hue: u8, + #[serde(default)] + pub saturation: u8, + #[serde(default = "default_background_value")] + pub value: u8, + #[serde(default = "default_background_speed")] + pub speed: u8, + #[serde(default)] + pub mode: LightingBackgroundModeToml, +} + +fn default_background_value() -> u8 { + 32 +} + +fn default_background_speed() -> u8 { + 128 +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LightingBackgroundModeToml { + #[default] + Solid, + Breathe, +} + +/// The `[keymap]` section: layer count plus the per-layer key actions. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct KeymapTomlConfig { + /// Total layer count. Optional — defaults to the number of `[[keymap.layer]]` + /// blocks; set it larger to reserve extra empty layers (e.g. for Vial/Rynk). + pub layers: Option, + /// Per-layer key actions: `[[keymap.layer]]`. + #[serde(default)] + pub layer: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[allow(unused)] +pub(crate) struct LayerTomlConfig { + pub name: Option, + pub keys: String, + pub encoders: Option>, +} + +/// Configurations for keyboard info +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct KeyboardInfo { + /// Keyboard name + pub name: String, + /// Vender id + pub vendor_id: u16, + /// Product id + pub product_id: u16, + /// Manufacturer + pub manufacturer: Option, + /// Product name, if not set, it will use `name` as default + pub product_name: Option, + /// Serial number + pub serial_number: Option, + /// Board name(if a supported board is used) + pub board: Option, + /// Chip model + pub chip: Option, + /// enable usb + pub usb_enable: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub enum MatrixType { + #[default] + #[serde(rename = "normal")] + Normal, + #[serde(rename = "direct_pin")] + DirectPin, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DebouncerType { + #[default] + Default, + Fast, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MatrixConfig { + #[serde(default)] + pub matrix_type: MatrixType, + pub row_pins: Option>, + pub col_pins: Option>, + pub direct_pins: Option>>, + #[serde(default = "default_true")] + pub direct_pin_low_active: bool, + #[serde(default = "default_false")] + pub row2col: bool, + #[serde(default)] + pub debouncer: DebouncerType, + pub bootmagic: Option<(u8, u8)>, +} + +/// Config for storage +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct StorageConfig { + /// Start address of local storage, MUST BE start of a sector. + /// If start_addr is set to 0(this is the default value), the last `num_sectors` sectors will be used. + pub start_addr: Option, + // Number of sectors used for storage, >= 2. + pub num_sectors: Option, + #[serde(default = "default_true")] + pub enabled: bool, + // Clear on the storage at reboot, set this to true if you want to reset the keymap + pub clear_storage: Option, + // Clear on the layout at reboot, set this to true if you want to reset the layout + pub clear_layout: Option, +} + +/// Config for DFU partition layout (embassy-boot). +/// +/// These values must match the bootloader's `memory.x` / linker script. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct DfuTomlConfig { + /// Offset of the boot state partition + pub state_offset: Option, + /// Size of the boot state partition + pub state_size: Option, + /// Offset of the DFU download partition + pub dfu_offset: Option, + /// Size of the DFU download partition + pub dfu_size: Option, + /// Flash page size in bytes (e.g. 4096 for RP2040). + /// Used with `flash_size` to auto-calculate partition addresses. + pub page_size: Option, + /// Total flash size in bytes. When set, DFU partition addresses are + /// calculated automatically using the rmk-boot formula. + /// Defaults to 2 MB (2097152) when omitted. + pub flash_size: Option, + /// Optional DFU activity LED pin, e.g. `"PIN_16"`. When set, the LED + /// is lit while a DFU download is in progress. + pub led: Option, + /// Unlock keys for DFU lock (optional) + pub unlock_keys: Option>, +} + +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BleConfig { + pub enabled: bool, + pub battery_adc_pin: Option, + pub charge_state: Option, + pub charge_led: Option, + pub adc_divider_measured: Option, + pub adc_divider_total: Option, + pub default_tx_power: Option, + pub use_2m_phy: Option, + pub passkey_entry: Option, + pub passkey_entry_timeout: Option, +} + +/// Default passkey entry timeout in seconds. +pub const DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 120; + +/// Minimum passkey entry timeout in seconds. +pub const MIN_PASSKEY_ENTRY_TIMEOUT_SECS: u32 = 30; + +/// nRF52840 DCDC REG0 output voltage +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum DcdcReg0Voltage { + #[serde(rename = "3V3")] + V3_3, + #[serde(rename = "1V8")] + V1_8, +} + +/// Config for chip-specific settings +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ChipConfig { + /// DCDC regulator 0 enabled (for nrf52840) + pub dcdc_reg0: Option, + /// DCDC regulator 1 enabled (for nrf52840, nrf52833) + pub dcdc_reg1: Option, + /// DCDC regulator 0 voltage (for nrf52840) + pub dcdc_reg0_voltage: Option, +} + +/// Config for lights +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LightConfig { + pub capslock: Option, + pub scrolllock: Option, + pub numslock: Option, +} + +/// Config for a single pin +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PinConfig { + pub pin: String, + pub low_active: bool, +} + +/// Configurations for dependencies +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DependencyConfig { + /// Enable defmt log or not + #[serde(default = "default_true")] + pub defmt_log: bool, +} + +impl Default for DependencyConfig { + fn default() -> Self { + Self { defmt_log: true } + } +} + +/// Intermediate resolved keymap grid (rows/cols/layers + per-layer actions). +/// Built once by `get_keymap_config` and unpacked into `Keymap`; never (de)serialized. +pub(crate) struct KeymapConfig { + pub rows: u8, + pub cols: u8, + pub layers: u8, + pub keymap: Vec>>, + pub encoder_map: Vec>, // Empty if there are no encoders or not configured +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KeyInfo { + pub hand: char, // 'L' or 'R' or other chars +} + +/// Configurations for actions behavior +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct BehaviorConfig { + pub tri_layer: Option, + pub one_shot: Option, + pub one_shot_modifiers: Option, + pub combo: Option, + #[serde(alias = "macro")] + pub macros: Option, + pub fork: Option, + pub morse: Option, + pub auto_mouse_layer: Option>, + pub mouse_layer_scale: Option>, +} + +/// Per-layer mouse movement and scroll scaling. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MouseLayerScaleConfig { + pub layer: u8, + pub r#move: Option<[u16; 2]>, + pub scroll: Option<[u16; 2]>, +} + +/// Configurations for auto mouse layer +/// +/// When motion is detected from a pointing device (e.g. PMW3610), the +/// specified `target_layer` is activated. The layer stays active until +/// `timeout` has elapsed without further motion, then it is deactivated. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct AutoMouseLayerConfig { + /// Pointing device id this entry applies to. When omitted, the entry acts as + /// a fallback for events whose `device_id` matches no other entry. + pub device_id: Option, + /// Layer index to activate on cursor motion + pub target_layer: u8, + /// Idle time after the last cursor motion before the layer is deactivated + /// (e.g. `"500ms"` or `"2s"`). + pub timeout: Option, + /// Minimum absolute axis delta required to be considered as motion. + /// Defaults to `1` (any motion). Helpful to filter out sensor noise. + pub threshold: Option, + /// When `true`, non-mouse key presses deactivate `target_layer` immediately (mouse HID keys and `extra_mouse_keys` excepted). + /// Macro-emitted keycodes, `Again`/`Repeat`, and `GraveEscape` cannot be classified and never deactivate the layer. + pub deactivate_on_key: Option, + /// Extra keycodes (e.g. modifiers) that do not trigger deactivation when `deactivate_on_key` is set. + /// Modifier keycodes listed here also exempt modifier-only actions containing them. + pub extra_mouse_keys: Option>, + /// When `true`, key presses that do NOT deactivate `target_layer` extend the timeout deadline + /// (i.e. reset it to now + `timeout`) at the moment the key's action resolves. + pub reset_timeout_on_key: Option, +} + +/// Per Key configurations profiles for morse, tap-hold, etc. +/// overrides the defaults given in TapHoldConfig +#[derive(Clone, Debug, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseProfile { + pub enable_flow_tap: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + /// overrides the global `[behavior.morse] prior_idle_time` for keys using this profile + pub prior_idle_time: Option, + + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves this tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, + + /// A key outside `hold_trigger_key_positions` settles this tap-hold as a tap when it is + /// released rather than when it is pressed + pub hold_trigger_on_release: Option, +} + +/// Configurations for tri layer +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct TriLayerConfig { + pub upper: u8, + pub lower: u8, + pub adjust: u8, +} + +/// Configurations for oneshot modifiers/layers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OneShotConfig { + pub timeout: Option, +} + +/// Configurations for oneshot modifiers +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OneShotModifiersConfig { + pub activate_on_keypress: Option, + pub quick_release: Option, +} + +/// Configurations for combos +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CombosConfig { + #[serde(default)] + pub combos: Vec, + pub timeout: Option, + pub prior_idle_time: Option, +} + +/// Configurations for combo +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ComboConfig { + pub actions: Vec, + pub output: String, + pub layer: Option, +} + +/// Configurations for macros +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacrosConfig { + pub macros: Vec, +} + +/// Configurations for macro +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MacroConfig { + pub operations: Vec, +} + +/// Macro operations (TOML deserialization type — resolved equivalent is in `resolved::behavior`) +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "operation", rename_all = "lowercase")] +pub(crate) enum MacroOperation { + Tap { keycode: String }, + Down { keycode: String }, + Up { keycode: String }, + Delay { duration: DurationMillis }, + Text { text: String }, +} + +/// Configurations for forks +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForksConfig { + pub forks: Vec, +} + +/// Configurations for fork +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ForkConfig { + pub trigger: String, + pub negative_output: String, + pub positive_output: String, + pub match_any: Option, + pub match_none: Option, + pub kept_modifiers: Option, + pub bindable: Option, +} + +/// Configurations for morse keys +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorsesConfig { + pub enable_flow_tap: Option, //default: false + /// used in permissive_hold mode + pub prior_idle_time: Option, + + /// if true, tap-hold key will always send tap action when tapped with the same hand only + pub unilateral_tap: Option, + + /// The decision mode of the morse/tap-hold key (only one of permissive_hold, hold_on_other_press, + /// tap_unless_interrupted and normal_mode can be true) + /// if none of them is given, normal mode will be the default + pub permissive_hold: Option, + pub hold_on_other_press: Option, + pub tap_unless_interrupted: Option, + pub normal_mode: Option, + + /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds) + pub hold_timeout: Option, + + /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds) + pub gap_timeout: Option, + + pub quick_tap_timeout: Option, + + /// if true, a hold resolved by timeout alone sends the tap action instead when the key is + /// released without any other key having been pressed + pub retro_tap: Option, + + /// Key positions allowed to trigger the hold, as `[row, col]` pairs. When set, any other + /// key resolves the tap-hold as a tap. Same as ZMK's `hold-trigger-key-positions`. + pub hold_trigger_key_positions: Option>, + + /// Named `[layout.regions]` whose positions are added to `hold_trigger_key_positions`. + pub hold_trigger_regions: Option>, +<<<<<<< +======= + + /// A key outside `hold_trigger_key_positions` settles the tap-hold as a tap when it is + /// released rather than when it is pressed + pub hold_trigger_on_release: Option, + +>>>>>>> + /// these can be used to overrides the defaults given above + pub profiles: Option>, + + /// the definition of morse / tap dance keys + pub morses: Option>, +} + +/// Configurations for morse +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseConfig { + // name of morse profile (to address BehaviorConfig::morse.profiles[self.profile]) + pub profile: Option, + + pub tap: Option, + pub hold: Option, + pub hold_after_tap: Option, + pub double_tap: Option, + /// Array of tap actions for each tap count (0-indexed) + pub tap_actions: Option>, + /// Array of hold actions for each tap count (0-indexed) + pub hold_actions: Option>, + /// Array of morse patter->action pairs count (0-indexed) + pub morse_actions: Option>, +} + +/// Configurations for morse action pairs +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct MorseActionPair { + pub pattern: String, // for example morse code of "B": "-..." or "_..." or "1000" + pub action: String, // "B" +} + +/// Split connection transport +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SplitConnection { + #[default] + Ble, + Serial, +} + +/// Configurations for split keyboards +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitConfig { + pub connection: SplitConnection, + pub central: SplitBoardConfig, + pub peripheral: Vec, +} + +/// Configurations for each split board +/// +/// The transport field must match `split.connection`: `serial` is required for +/// serial splits and forbidden for BLE splits; `ble_addr` is optional for BLE +/// splits (dongle setups omit it) and forbidden for serial splits. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SplitBoardConfig { + /// Row number of the split board + pub rows: usize, + /// Col number of the split board + pub cols: usize, + /// Row offset of the split board + pub row_offset: usize, + /// Col offset of the split board + pub col_offset: usize, + /// Ble address + pub ble_addr: Option<[u8; 6]>, + /// Serial config, the vector length should be 1 for peripheral + pub serial: Option>, + /// Matrix config for the split + pub matrix: MatrixConfig, + /// Input device config for the split + pub input_device: Option, + /// Display config for the split board + pub display: Option, + /// Battery ADC pin for this split board + pub battery_adc_pin: Option, + /// ADC divider measured value for battery + pub adc_divider_measured: Option, + /// ADC divider total value for battery + pub adc_divider_total: Option, + /// Output Pin config for the split + pub output: Option>, + /// Path to the peripheral firmware binary for automatic dfu_split update. + /// Relative to the project's `Cargo.toml`. When set, the generated code + /// includes the binary with `include_bytes!` and registers it via + /// [`set_firmware_update_data`](crate::set_firmware_update_data). + pub firmware: Option, + /// DFU update policy for this peripheral. "MatchHash" (default) only + /// flashes when the firmware differs; "force" always flashes. + pub update_policy: Option, +} + +/// Serial port config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SerialConfig { + pub instance: String, + pub tx_pin: String, + pub rx_pin: String, +} + +/// Duration in milliseconds +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct DurationMillis(#[serde(deserialize_with = "parse_duration_millis")] pub u64); + +const fn default_true() -> bool { + true +} + +const fn default_false() -> bool { + false +} + +const fn default_pointing_report_hz() -> u16 { + 125 +} + +fn parse_duration_millis<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { + let input: String = de::Deserialize::deserialize(deserializer)?; + let num = input.trim_end_matches(|c: char| !c.is_numeric()); + let unit = &input[num.len()..]; + let num: u64 = num.parse().map_err(|_| { + de::Error::custom(format!( + "Invalid number \"{num}\" in duration: number part must be a u64" + )) + })?; + + match unit { + "s" => Ok(num * 1000), + "ms" => Ok(num), + other => Err(de::Error::custom(format!( + "Invalid duration unit \"{other}\": unit part must be either \"s\" or \"ms\"" + ))), + } +} + +/// Configuration for host tools +#[serde_inline_default] +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct HostConfig { + /// Whether Vial is enabled + #[serde_inline_default(true)] + pub vial_enabled: bool, + /// Whether the RMK-native Rynk protocol is enabled. Mutually exclusive + /// with `vial_enabled` (the underlying Cargo features conflict). + #[serde_inline_default(false)] + pub rynk_enabled: bool, + /// Physical keys (row, col) held simultaneously to unlock (optional). + /// Shared by the Vial lock and the Rynk lock gate. + pub unlock_keys: Option>, + /// Start (and stay) unlocked, bypassing the unlock-key combo (default: + /// false). Renamed from `vial_insecure`; the old name still parses. + #[serde(alias = "vial_insecure")] + #[serde_inline_default(false)] + pub insecure: bool, + /// Move the Rynk config-write tier (`SetKeyAction`, `SetMacro`, …) into the + /// locked set, so writes also require unlock (default: false). + #[serde_inline_default(false)] + pub write_requires_unlock: bool, + /// Require the Rynk physical-presence unlock before entering either the + /// central or a split peripheral bootloader (default: true). + #[serde_inline_default(true)] + pub bootloader_requires_unlock: bool, +} + +impl Default for HostConfig { + fn default() -> Self { + Self { + vial_enabled: true, + rynk_enabled: false, + unlock_keys: None, + insecure: false, + write_requires_unlock: false, + bootloader_requires_unlock: true, + } + } +} + +/// Configurations for input devices +/// +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InputDeviceConfig { + pub encoder: Option>, + pub pointing: Option>, + pub joystick: Option>, + pub pmw3610: Option>, + pub pmw33xx: Option>, + pub iqs5xx: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct JoystickConfig { + // Name of the joystick + pub name: String, + /// Device id used to match this joystick with its JoystickProcessor. + /// If omitted, ids are assigned sequentially starting from 0. + pub id: Option, + // Pin a of the joystick + pub pin_x: String, + // Pin b of the joystick + pub pin_y: String, + // Pin z of the joystick + pub pin_z: String, + pub transform: Vec>, + pub bias: Vec, + pub resolution: u16, +} + +/// PMW3610 optical mouse sensor configuration +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw3610Config { + /// Name of the sensor (used for variable naming) + pub name: String, + /// id of the device + pub id: Option, + /// SPI pins + pub spi: SpiConfig, + /// Optional motion interrupt pin + pub motion: Option, + /// CPI resolution (200-3200, step 200). Optional, uses sensor default if not set. + pub cpi: Option, + /// Invert X axis + #[serde(default)] + pub invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub swap_xy: bool, + /// Force awake mode (disable power saving) + #[serde(default)] + pub force_awake: bool, + /// Enable smart mode for better tracking on shiny surfaces + #[serde(default)] + pub smart_mode: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Pmw33xxType { + #[default] + PMW3360, + PMW3389, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pmw33xxConfig { + // Name of the sensor (used for variable naming) + pub name: String, + // id of the device + pub id: Option, + // Sensor Type (3360 or 3389) + pub sensor_type: Pmw33xxType, + // SPI pins + pub spi: SpiConfig, + // Optional motion interrupt pin + pub motion: Option, + // CPI resolution (100-12000, step 100).Optional, uses sensor default 1600 if not set. + pub cpi: Option, + // Rotational transform angle (-127 to 127) Optional, uses sensor default 0 if not set. + pub rot_trans_angle: Option, + // liftoff distance. Optional, uses sensor default 0 if not set. + pub liftoff_dist: Option, + // Invert X axis + #[serde(default)] + pub proc_invert_x: bool, + // Invert Y axis + #[serde(default)] + pub proc_invert_y: bool, + // Swap X and Y axes + #[serde(default)] + pub proc_swap_xy: bool, + /// Report rate (Hz). Motion will be accumulated and emitted at this rate. + #[serde(default = "default_pointing_report_hz")] + pub report_hz: u16, +} + +/// Azoteq IQS5xx trackpad configuration. +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxConfig { + /// Name of the trackpad (used for variable naming). + pub name: String, + /// RMK pointing-device id (0-255). Defaults to 0. + pub id: Option, + /// I²C bus the trackpad is connected to. The bus is dedicated to this + /// device — sharing with other I²C peripherals (e.g. an OLED) is not yet + /// supported via TOML. + pub i2c: Iqs5xxI2cConfig, + /// Optional `RDY` pin. Strongly recommended; without it the driver falls + /// back to timed polling and may stall the bus through clock-stretching. + pub rdy: Option, + /// Invert X in the PointingProcessor. + #[serde(default)] + pub proc_invert_x: bool, + /// Invert Y in the PointingProcessor. + #[serde(default)] + pub proc_invert_y: bool, + /// Swap X and Y in the PointingProcessor. + #[serde(default)] + pub proc_swap_xy: bool, +} + +/// I²C bus configuration for the IQS5xx. Distinct from the generic `I2cConfig` +/// because the IQS5xx address is fixed (`0x74` by default; can be reprogrammed +/// at the IC, but not at runtime — exposing it would be misleading). +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Iqs5xxI2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EncoderConfig { + // Pin a of the encoder + pub pin_a: String, + // Pin b of the encoder + pub pin_b: String, + // Phase is the working mode of the rotary encoders. + // Available mode: + // - default: resolution = 1 + // - e8h7: phase table tuned for E8H7 encoders + // - resolution: customized resolution, the resolution value and reverse should be specified + // A typical [EC11 encoder](https://tech.alpsalpine.com/cms.media/product_catalog_ec_01_ec11e_en_611f078659.pdf)'s resolution is 2 + // In resolution mode, you can also specify the number of detent and pulses, the resolution will be calculated by `pulse * 4 / detent` + #[serde(default)] + pub phase: EncoderPhase, + // Resolution + pub resolution: Option, + // The number of detent + pub detent: Option, + // The number of pulse + pub pulse: Option, + // Whether the direction of the rotary encoder is reversed. + pub reverse: Option, + // Use MCU's internal pull-up resistor or not, defaults to false, the external pull-up resistor is needed + #[serde(default = "default_false")] + pub internal_pullup: bool, + // Debounce interval in milliseconds. Suppresses spurious events from mechanical contact bounce. + // Defaults to 0 (disabled) if not specified. + pub debounce_ms: Option, +} + +/// Rotary encoder phase (decoding) mode +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum EncoderPhase { + #[default] + Default, + E8h7, + Resolution, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields, untagged)] +pub enum EncoderResolution { + Value(u8), + Derived { detent: u8, pulse: u8 }, +} + +impl Default for EncoderResolution { + fn default() -> Self { + Self::Value(4) + } +} + +/// Pointing device config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PointingDeviceConfig { + pub interface: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CommunicationProtocol { + I2c(I2cConfig), + Spi(SpiConfig), +} + +/// SPI config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SpiConfig { + pub instance: String, + pub sck: String, + pub mosi: String, + pub miso: String, + pub cs: Option, + pub cpi: Option, + pub tx_dma: Option, + pub rx_dma: Option, +} + +/// I2C config +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct I2cConfig { + pub instance: String, + pub sda: String, + pub scl: String, + /// 7-bit I2C address. Defaults to 0x3C when omitted. + #[serde(default = "default_i2c_address")] + pub address: u8, +} + +const fn default_i2c_address() -> u8 { + 0x3C +} + +/// Display driver type +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DisplayDriver { + Ssd1306, + Sh1106, + Sh1107, + Sh1108, + Ssd1309, +} + +/// Display configuration +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DisplayConfig { + pub driver: DisplayDriver, + pub protocol: CommunicationProtocol, + pub size: String, + #[serde(default)] + pub rotation: u16, + pub renderer: Option, + /// Poll interval in milliseconds for periodic redraws (animations). + /// When absent, polling is disabled — the display only redraws on events. + pub render_interval: Option, + /// Minimum time in milliseconds between event-driven renders. + /// Prevents the display from being hammered by rapid events. Default: 10 ms. + pub min_render_interval: Option, +} + +/// Configuration for an output pin +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OutputConfig { + pub pin: String, + #[serde(default)] + pub low_active: bool, + #[serde(default)] + pub initial_state_active: bool, +} + +impl KeyboardTomlConfig { + pub(crate) fn get_output_config(&self) -> Result, String> { + let output_config = self.output.clone(); + let split = self.split.clone(); + match (output_config, split) { + (None, Some(s)) => Ok(s.central.output.unwrap_or_default()), + (Some(c), None) => Ok(c), + (None, None) => Ok(Default::default()), + _ => Err("Use [[split.output]] to define outputs for split in your keyboard.toml!".to_string()), + } + } + + pub(crate) fn get_dependency_config(&self) -> DependencyConfig { + self.dependency.clone().unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_event_config_default_values() { + let config = EventConfig::default(); + + // Check some key default values from event_default.toml + assert_eq!(config.keyboard.channel_size, 16); + assert_eq!(config.keyboard.pubs, 2); + assert_eq!(config.keyboard.subs, 3); + + assert_eq!(config.modifier.channel_size, 8); + assert_eq!(config.modifier.pubs, 1); + assert_eq!(config.modifier.subs, 2); + + assert_eq!(config.layer_change.channel_size, 1); + assert_eq!(config.layer_change.subs, 1); + + assert_eq!(config.led_indicator.channel_size, 2); + assert_eq!(config.led_indicator.pubs, 2); + assert_eq!(config.led_indicator.subs, 3); + + assert_eq!(config.lighting_changed.channel_size, 1); + assert_eq!(config.lighting_changed.pubs, 1); + assert_eq!(config.lighting_changed.subs, 1); + + assert_eq!(config.pointing.channel_size, 8); + assert_eq!(config.pointing.subs, 2); + + assert_eq!(config.action.channel_size, 16); + assert_eq!(config.action.pubs, 1); + assert_eq!(config.action.subs, 0); + } + + #[test] + fn test_event_config_user_override() { + // Simulate user config that overrides some event settings + let user_toml = r#" +[event.keyboard] +channel_size = 32 +"#; + // Parse with event defaults first, then user config + let config: KeyboardTomlConfig = Config::builder() + .add_source(File::from_str(EVENT_DEFAULT_CONFIG, FileFormat::Toml)) + .add_source(File::from_str(user_toml, FileFormat::Toml)) + .build() + .unwrap() + .try_deserialize() + .unwrap(); + + // User-overridden values + assert_eq!(config.event.keyboard.channel_size, 32); + assert_eq!(config.event.keyboard.pubs, 2); + assert_eq!(config.event.keyboard.subs, 3); + + // Non-overridden values should use defaults + assert_eq!(config.event.modifier.channel_size, 8); + assert_eq!(config.event.modifier.subs, 2); + assert_eq!(config.event.layer_change.subs, 1); + } + + #[test] + fn rmk_count_limits_fit_u8_capability_fields() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +combo_max_num = 255 +morse_max_num = 255 +fork_max_num = 255 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.combo_max_num, 255); + assert_eq!(ok.rmk.morse_max_num, 255); + assert_eq!(ok.rmk.fork_max_num, 255); + + for (field, message) in [ + ("combo_max_num", "combo_max_num must be between 0 and 255"), + ("morse_max_num", "morse_max_num must be between 0 and 255"), + ("fork_max_num", "fork_max_num must be between 0 and 255"), + ] { + let toml = format!("[rmk]\n{field} = 256\n"); + let err = toml::from_str::(&toml).unwrap_err(); + assert!(err.to_string().contains(message), "{err}"); + } + } + + #[test] + fn split_central_max_latency_matches_ble_limit() { + let ok: KeyboardTomlConfig = toml::from_str( + r#" +[rmk] +split_central_max_latency_powered = 499 +split_central_max_latency_battery = 498 +"#, + ) + .unwrap(); + assert_eq!(ok.rmk.split_central_max_latency_powered, 499); + assert_eq!(ok.rmk.split_central_max_latency_battery, 498); + + let err = toml::from_str::( + r#" +[rmk] +split_central_max_latency_battery = 500 +"#, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("split_central_max_latency must be between 0 and 499"), + "{err}" + ); + } + + #[test] + fn test_event_config_partial_override_with_event_defaults_loader() { + let user_toml = r#" +[event.layer_change] +subs = 2 +"#; + let path = std::env::temp_dir().join(format!( + "rmk-event-defaults-loader-{}-{}.toml", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::write(&path, user_toml).unwrap(); + + let config = KeyboardTomlConfig::new_from_toml_path_with_event_defaults(&path); + std::fs::remove_file(path).unwrap(); + + assert_eq!(config.event.layer_change.channel_size, 1); + assert_eq!(config.event.layer_change.pubs, 2); + assert_eq!(config.event.layer_change.subs, 2); + } +} diff --git a/resolutions/rerere/INDEX.toml b/resolutions/rerere/INDEX.toml index 22f61cd..331d2f9 100644 --- a/resolutions/rerere/INDEX.toml +++ b/resolutions/rerere/INDEX.toml @@ -227,7 +227,7 @@ recorded = "2026-08-05T09:48:21.226893030+00:00" entry = "feat/morse-hold-trigger-on-release" hash = "9d8fc76b977762dc5334289367875026faf74445" paths = ["rmk-config/src/lib.rs"] -recorded = "2026-08-05T09:48:21.226893541+00:00" +recorded = "2026-08-08T06:51:15.178385347+00:00" [[resolution]] entry = "feat/morse-hold-trigger-positions" @@ -247,6 +247,12 @@ hash = "2d5237a51e4c179d53542f13ff9c3f04e12f6696" paths = ["rmk/src/keymap.rs"] recorded = "2026-08-05T15:53:36.021939073+00:00" +[[resolution]] +entry = "feat/morse-hold-trigger-positions" +hash = "3f52228629b6938a12071de8cecb48baef7811f5" +paths = ["rmk-config/src/lib.rs"] +recorded = "2026-08-08T06:51:02.526715530+00:00" + [[resolution]] entry = "feat/morse-hold-trigger-positions" hash = "5c36805ac9f930e073cb73d6f2b6a6d46ad6c045"