From 4ca5293f447f8e9df761fc189e00f138a77b4bcd Mon Sep 17 00:00:00 2001 From: Haobo Gu Date: Mon, 11 May 2026 19:33:21 +0800 Subject: [PATCH 01/69] feat(rmk-types): replace rmk protocol module with rynk wire format Adds new rynk/ protocol module (buffer, cmd, header, fingerprint, mod) with the new wire format. Migrates per-domain modules (combo, encoder, fork, keymap, macro_data, morse, status, system) from the previous rmk/ namespace, and removes the old endpoint/topic snapshot-based layout. Also drops unused derives/imports across rmk-types/src/*.rs. Signed-off-by: Haobo Gu --- rmk-config/src/lib.rs | 8 +- rmk-config/src/resolved/build_constants.rs | 4 + rmk-types/Cargo.toml | 10 +- rmk-types/build.rs | 23 +- rmk-types/src/action/encoder.rs | 3 - rmk-types/src/action/key_action.rs | 3 - rmk-types/src/action/keyboard.rs | 3 - rmk-types/src/action/light.rs | 3 - rmk-types/src/action/mod.rs | 3 - rmk-types/src/battery.rs | 4 - rmk-types/src/ble.rs | 4 - rmk-types/src/combo.rs | 3 - rmk-types/src/connection.rs | 5 - rmk-types/src/fork.rs | 4 - rmk-types/src/keycode/consumer.rs | 3 - rmk-types/src/keycode/hid.rs | 3 - rmk-types/src/keycode/mod.rs | 4 - rmk-types/src/keycode/system_control.rs | 3 - rmk-types/src/led_indicator.rs | 13 - rmk-types/src/lib.rs | 2 +- rmk-types/src/modifier.rs | 13 - rmk-types/src/morse.rs | 53 +- rmk-types/src/mouse_button.rs | 13 - rmk-types/src/protocol/mod.rs | 13 +- rmk-types/src/protocol/rmk/endpoints.rs | 446 --------------- rmk-types/src/protocol/rmk/mod.rs | 521 ------------------ .../rmk/snapshots/endpoint_keys_base.snap | 36 -- .../rmk/snapshots/endpoint_keys_ble.snap | 12 - .../snapshots/endpoint_keys_ble_split.snap | 9 - .../rmk/snapshots/endpoint_keys_bulk.snap | 14 - .../rmk/snapshots/topic_keys_base.snap | 11 - .../rmk/snapshots/topic_keys_ble.snap | 8 - .../protocol/rmk/snapshots/wire_values.snap | 21 - rmk-types/src/protocol/rmk/topics.rs | 68 --- rmk-types/src/protocol/rynk/buffer.rs | 122 ++++ rmk-types/src/protocol/rynk/cmd.rs | 151 +++++ rmk-types/src/protocol/{rmk => rynk}/combo.rs | 14 +- .../src/protocol/{rmk => rynk}/encoder.rs | 7 +- rmk-types/src/protocol/{rmk => rynk}/fork.rs | 5 +- rmk-types/src/protocol/rynk/header.rs | 128 +++++ .../src/protocol/{rmk => rynk}/keymap.rs | 16 +- .../src/protocol/{rmk => rynk}/macro_data.rs | 9 +- rmk-types/src/protocol/rynk/mod.rs | 346 ++++++++++++ rmk-types/src/protocol/{rmk => rynk}/morse.rs | 14 +- .../protocol/rynk/snapshots/wire_values.snap | 20 + .../src/protocol/{rmk => rynk}/status.rs | 7 +- .../src/protocol/{rmk => rynk}/system.rs | 15 +- rmk-types/src/steno.rs | 3 - rmk/Cargo.toml | 12 +- rmk/src/host/context.rs | 4 +- rmk/src/lib.rs | 4 +- 51 files changed, 866 insertions(+), 1357 deletions(-) delete mode 100644 rmk-types/src/protocol/rmk/endpoints.rs delete mode 100644 rmk-types/src/protocol/rmk/mod.rs delete mode 100644 rmk-types/src/protocol/rmk/snapshots/endpoint_keys_base.snap delete mode 100644 rmk-types/src/protocol/rmk/snapshots/endpoint_keys_ble.snap delete mode 100644 rmk-types/src/protocol/rmk/snapshots/endpoint_keys_ble_split.snap delete mode 100644 rmk-types/src/protocol/rmk/snapshots/endpoint_keys_bulk.snap delete mode 100644 rmk-types/src/protocol/rmk/snapshots/topic_keys_base.snap delete mode 100644 rmk-types/src/protocol/rmk/snapshots/topic_keys_ble.snap delete mode 100644 rmk-types/src/protocol/rmk/snapshots/wire_values.snap delete mode 100644 rmk-types/src/protocol/rmk/topics.rs create mode 100644 rmk-types/src/protocol/rynk/buffer.rs create mode 100644 rmk-types/src/protocol/rynk/cmd.rs rename rmk-types/src/protocol/{rmk => rynk}/combo.rs (93%) rename rmk-types/src/protocol/{rmk => rynk}/encoder.rs (91%) rename rmk-types/src/protocol/{rmk => rynk}/fork.rs (89%) create mode 100644 rmk-types/src/protocol/rynk/header.rs rename rmk-types/src/protocol/{rmk => rynk}/keymap.rs (95%) rename rmk-types/src/protocol/{rmk => rynk}/macro_data.rs (91%) create mode 100644 rmk-types/src/protocol/rynk/mod.rs rename rmk-types/src/protocol/{rmk => rynk}/morse.rs (94%) create mode 100644 rmk-types/src/protocol/rynk/snapshots/wire_values.snap rename rmk-types/src/protocol/{rmk => rynk}/status.rs (91%) rename rmk-types/src/protocol/{rmk => rynk}/system.rs (95%) diff --git a/rmk-config/src/lib.rs b/rmk-config/src/lib.rs index 976ff5007..00ee09904 100644 --- a/rmk-config/src/lib.rs +++ b/rmk-config/src/lib.rs @@ -27,7 +27,7 @@ pub(crate) mod storage; /// /// 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 `rmk_protocol` enabled +/// 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: @@ -263,6 +263,11 @@ pub(crate) struct RmkConstantsConfig { /// Smaller values reduce firmware RAM usage but require more round-trips. #[serde_inline_default(64)] pub protocol_macro_chunk_size: usize, + /// Optional override for the Rynk RX/TX buffer size (bytes). When `None`, + /// the build emits `RYNK_BUFFER_SIZE = RYNK_MIN_BUFFER_SIZE`. The const + /// assertion in `rmk/src/host/rynk` rejects user values below the floor. + #[serde(default)] + pub rynk_buffer_size: Option, } fn check_combo_max_num<'de, D>(deserializer: D) -> Result @@ -330,6 +335,7 @@ impl Default for RmkConstantsConfig { split_central_sleep_timeout_seconds: 0, protocol_max_bulk_size: 8, protocol_macro_chunk_size: 64, + rynk_buffer_size: None, } } } diff --git a/rmk-config/src/resolved/build_constants.rs b/rmk-config/src/resolved/build_constants.rs index e1cd0d271..aa8ed3bad 100644 --- a/rmk-config/src/resolved/build_constants.rs +++ b/rmk-config/src/resolved/build_constants.rs @@ -48,6 +48,9 @@ pub struct BuildConstants { pub split_central_sleep_timeout_seconds: u32, pub protocol_max_bulk_size: usize, pub protocol_macro_chunk_size: usize, + /// Optional override for the Rynk RX/TX buffer size (bytes). `None` + /// instructs `rmk-types/build.rs` to fall back to `RYNK_MIN_BUFFER_SIZE`. + pub rynk_buffer_size: Option, pub events: Vec, pub passkey: Option, } @@ -174,6 +177,7 @@ impl crate::KeyboardTomlConfig { split_central_sleep_timeout_seconds: rmk.split_central_sleep_timeout_seconds, protocol_max_bulk_size: rmk.protocol_max_bulk_size, protocol_macro_chunk_size: rmk.protocol_macro_chunk_size, + rynk_buffer_size: rmk.rynk_buffer_size, events, passkey, }) diff --git a/rmk-types/Cargo.toml b/rmk-types/Cargo.toml index 3afdf4e8b..a3d7da92d 100644 --- a/rmk-types/Cargo.toml +++ b/rmk-types/Cargo.toml @@ -14,8 +14,6 @@ defmt = { version = "1.0", optional = true } serde = { version = "1", default-features = false, features = ["derive"] } postcard = { version = "1", features = ["experimental-derive"] } -postcard-schema = { version = "0.2", default-features = false, features = ["derive", "heapless-v0_9"], optional = true } -postcard-rpc = { version = "0.12", default-features = false, optional = true } heapless = { version = "0.9", default-features = false, features = ["serde"] } strum = { version = "0.28", default-features = false, features = ["derive"] } @@ -34,14 +32,14 @@ defmt = [ ] # Feature for proc-macro code generation only, not included in firmware _codegen = [] -# Feature for RMK native protocol schema/endpoint support -rmk_protocol = ["dep:postcard-rpc", "dep:postcard-schema"] +# Feature for RMK native (rynk) protocol support +rynk = [] # Bulk transfer endpoints for MCUs with sufficient RAM. # Enables multi-element bulk keymap/combo/morse endpoints and BULK_SIZE constant. -bulk = ["rmk_protocol"] +bulk = ["rynk"] # Host tool: uses protocol ceiling values for Vec capacities. # Enables all optional endpoint groups so the host can talk to any firmware. -host = ["rmk_protocol", "bulk", "_ble", "split"] +host = ["rynk", "bulk", "_ble", "split"] # Build-time only features: forwarded from rmk to control constant generation in build.rs. # Also used as `#[cfg(feature = "...")]` guards for protocol endpoint/topic gating. _ble = [] diff --git a/rmk-types/build.rs b/rmk-types/build.rs index cf597927d..403417903 100644 --- a/rmk-types/build.rs +++ b/rmk-types/build.rs @@ -154,8 +154,8 @@ fn generate_constants(bc: &BuildConstants) -> String { bc.protocol_macro_chunk_size )); // Compile-time check: firmware Vec sizes must not exceed protocol ceilings. - // Only enforce when the rmk_protocol feature is active. - if env::var("CARGO_FEATURE_RMK_PROTOCOL").is_ok() { + // Only enforce when the rynk feature is active. + if env::var("CARGO_FEATURE_RYNK").is_ok() { lines.push("const _: () = assert!(COMBO_SIZE <= MAX_COMBO_SIZE, \"firmware COMBO_SIZE exceeds protocol ceiling MAX_COMBO_SIZE\");".to_string()); lines.push("const _: () = assert!(MORSE_SIZE <= MAX_MORSE_SIZE, \"firmware MORSE_SIZE exceeds protocol ceiling MAX_MORSE_SIZE\");".to_string()); lines.push("const _: () = assert!(MACRO_DATA_SIZE <= MAX_MACRO_DATA_SIZE, \"firmware MACRO_DATA_SIZE exceeds protocol ceiling MAX_MACRO_DATA_SIZE\");".to_string()); @@ -168,6 +168,25 @@ fn generate_constants(bc: &BuildConstants) -> String { } } + // Rynk RX/TX buffer size. Only emitted when the `rynk` feature is on + // so the constant doesn't leak into non-Rynk builds. `None` falls back + // to `RYNK_MIN_BUFFER_SIZE`, which is the compile-time lower bound; + // the `rmk/src/host/rynk/mod.rs` const-assert rejects values below it. + if env::var("CARGO_FEATURE_RYNK").is_ok() { + match bc.rynk_buffer_size { + Some(n) => { + lines.push(format!("pub const RYNK_BUFFER_SIZE: usize = {n};")); + } + None => { + lines.push( + "pub const RYNK_BUFFER_SIZE: usize = \ + crate::protocol::rynk::RYNK_MIN_BUFFER_SIZE;" + .to_string(), + ); + } + } + } + // Event channels for ev in &bc.events { let upper = ev.name.to_uppercase(); diff --git a/rmk-types/src/action/encoder.rs b/rmk-types/src/action/encoder.rs index 212e720cf..34ff0d2a8 100644 --- a/rmk-types/src/action/encoder.rs +++ b/rmk-types/src/action/encoder.rs @@ -1,8 +1,6 @@ //! Rotary encoder actions. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use super::KeyAction; @@ -12,7 +10,6 @@ use super::KeyAction; /// Both fields default to `KeyAction::No` (no action). #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] pub struct EncoderAction { /// Action triggered when the encoder is rotated clockwise. pub clockwise: KeyAction, diff --git a/rmk-types/src/action/key_action.rs b/rmk-types/src/action/key_action.rs index 6679edc87..c56faa285 100644 --- a/rmk-types/src/action/key_action.rs +++ b/rmk-types/src/action/key_action.rs @@ -1,8 +1,6 @@ //! Composite key actions stored in the keymap. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use super::Action; @@ -12,7 +10,6 @@ use crate::morse::MorseProfile; /// It can be a single action like triggering a key, or a composite keyboard action like tap/hold #[derive(Debug, Copy, Clone, Eq, Serialize, Deserialize, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[non_exhaustive] pub enum KeyAction { /// No action diff --git a/rmk-types/src/action/keyboard.rs b/rmk-types/src/action/keyboard.rs index a3abed2c8..e7c97ccc2 100644 --- a/rmk-types/src/action/keyboard.rs +++ b/rmk-types/src/action/keyboard.rs @@ -1,14 +1,11 @@ //! Keyboard control actions. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; /// Actions for controlling the keyboard or changing the keyboard's state, for example, enable/disable a particular function #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "_codegen", derive(strum::VariantNames))] #[non_exhaustive] pub enum KeyboardAction { diff --git a/rmk-types/src/action/light.rs b/rmk-types/src/action/light.rs index 2e75d9463..303fa238a 100644 --- a/rmk-types/src/action/light.rs +++ b/rmk-types/src/action/light.rs @@ -1,14 +1,11 @@ //! Light control actions. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; /// Actions for controlling lights #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "_codegen", derive(strum::VariantNames))] #[non_exhaustive] pub enum LightAction { diff --git a/rmk-types/src/action/mod.rs b/rmk-types/src/action/mod.rs index ca86c6329..af167f632 100644 --- a/rmk-types/src/action/mod.rs +++ b/rmk-types/src/action/mod.rs @@ -22,8 +22,6 @@ pub use key_action::KeyAction; pub use keyboard::KeyboardAction; pub use light::LightAction; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use crate::keycode::{KeyCode, SpecialKey}; @@ -34,7 +32,6 @@ use crate::steno::StenoKey; /// A single basic action that a keyboard can execute. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[non_exhaustive] pub enum Action { /// Default action, no action. diff --git a/rmk-types/src/battery.rs b/rmk-types/src/battery.rs index f6afdcbdd..569405ef3 100644 --- a/rmk-types/src/battery.rs +++ b/rmk-types/src/battery.rs @@ -1,13 +1,10 @@ //! Battery status types. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; /// Charge state of the battery. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ChargeState { Charging, @@ -28,7 +25,6 @@ impl From for ChargeState { /// Battery status used for both status queries and event notifications. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum BatteryStatus { Unavailable, diff --git a/rmk-types/src/ble.rs b/rmk-types/src/ble.rs index 3deba148e..3a34b5056 100644 --- a/rmk-types/src/ble.rs +++ b/rmk-types/src/ble.rs @@ -1,13 +1,10 @@ //! BLE status types. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; /// BLE state (what the BLE subsystem is currently doing). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum BleState { /// The BLE is advertising. @@ -20,7 +17,6 @@ pub enum BleState { /// Unified BLE status: which profile is active and what the BLE is doing. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct BleStatus { pub profile: u8, diff --git a/rmk-types/src/combo.rs b/rmk-types/src/combo.rs index 13193d95f..7fa441a9e 100644 --- a/rmk-types/src/combo.rs +++ b/rmk-types/src/combo.rs @@ -2,8 +2,6 @@ use heapless::Vec; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use crate::action::KeyAction; @@ -19,7 +17,6 @@ use crate::constants::COMBO_SIZE; /// Note: `COMBO_SIZE` is a **wire-format** capacity — on firmware it equals /// `COMBO_MAX_LENGTH` (from `keyboard.toml`), on host it's a fixed upper bound. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Combo { pub actions: Vec, diff --git a/rmk-types/src/connection.rs b/rmk-types/src/connection.rs index 6e01d7ed7..b5827f999 100644 --- a/rmk-types/src/connection.rs +++ b/rmk-types/src/connection.rs @@ -1,15 +1,12 @@ //! Shared connection type definitions used across RMK crates. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use crate::ble::{BleState, BleStatus}; /// Connection type for the keyboard. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ConnectionType { Usb, @@ -20,7 +17,6 @@ pub enum ConnectionType { /// the bus is enumerated but transmission is gated on remote wakeup — the /// first key still needs to reach the USB writer to trigger that wakeup. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[derive(Default)] pub enum UsbState { @@ -35,7 +31,6 @@ pub enum UsbState { /// availability and routing. The active transport is derived on demand via /// [`Self::decide_active`] from the input fields below. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ConnectionStatus { pub usb: UsbState, diff --git a/rmk-types/src/fork.rs b/rmk-types/src/fork.rs index c00cd7557..e8c41ef07 100644 --- a/rmk-types/src/fork.rs +++ b/rmk-types/src/fork.rs @@ -3,8 +3,6 @@ use core::ops::{BitAnd, BitOr, Not}; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use crate::action::KeyAction; @@ -16,7 +14,6 @@ use crate::mouse_button::MouseButtons; /// /// A zero (default) value means "match nothing" — no modifiers, LEDs, or mouse buttons. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct StateBits { /// Active modifier combination to match. @@ -75,7 +72,6 @@ impl StateBits { /// When the trigger key is pressed, the fork checks current state against `match_any` /// and `match_none` to decide between `positive_output` and `negative_output`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Fork { /// The key action that activates this fork. Should not be `KeyAction::Transparent`. diff --git a/rmk-types/src/keycode/consumer.rs b/rmk-types/src/keycode/consumer.rs index a93085ac7..2c0b65c4c 100644 --- a/rmk-types/src/keycode/consumer.rs +++ b/rmk-types/src/keycode/consumer.rs @@ -1,8 +1,6 @@ //! Consumer page keycodes. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use super::hid::HidKeyCode; @@ -13,7 +11,6 @@ use super::hid::HidKeyCode; #[repr(u16)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] pub enum ConsumerKey { No = 0x00, // 15.5 Display Controls diff --git a/rmk-types/src/keycode/hid.rs b/rmk-types/src/keycode/hid.rs index 17a29b5a2..f0a405cab 100644 --- a/rmk-types/src/keycode/hid.rs +++ b/rmk-types/src/keycode/hid.rs @@ -1,8 +1,6 @@ //! USB HID keycodes. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use strum::FromRepr; @@ -14,7 +12,6 @@ use crate::modifier::ModifierCombination; #[repr(u8)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, FromRepr, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] pub enum HidKeyCode { /// Reserved, no-key. No = 0x0000, diff --git a/rmk-types/src/keycode/mod.rs b/rmk-types/src/keycode/mod.rs index f77b48371..c4972c73d 100644 --- a/rmk-types/src/keycode/mod.rs +++ b/rmk-types/src/keycode/mod.rs @@ -9,15 +9,12 @@ pub use ascii::{from_ascii, to_ascii}; pub use consumer::ConsumerKey; pub use hid::HidKeyCode; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; pub use system_control::SystemControlKey; /// Key codes which are not in the HID spec, but still commonly used #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "_codegen", derive(strum::VariantNames))] #[non_exhaustive] pub enum SpecialKey { @@ -29,7 +26,6 @@ pub enum SpecialKey { #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[non_exhaustive] pub enum KeyCode { Hid(HidKeyCode), diff --git a/rmk-types/src/keycode/system_control.rs b/rmk-types/src/keycode/system_control.rs index c8385065e..296c5b07d 100644 --- a/rmk-types/src/keycode/system_control.rs +++ b/rmk-types/src/keycode/system_control.rs @@ -1,8 +1,6 @@ //! System control keycodes. use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; use serde::{Deserialize, Serialize}; use super::hid::HidKeyCode; @@ -13,7 +11,6 @@ use super::hid::HidKeyCode; #[repr(u8)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] pub enum SystemControlKey { No = 0x00, PowerDown = 0x81, diff --git a/rmk-types/src/led_indicator.rs b/rmk-types/src/led_indicator.rs index 26ef28801..48d8d0da1 100644 --- a/rmk-types/src/led_indicator.rs +++ b/rmk-types/src/led_indicator.rs @@ -6,11 +6,6 @@ use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use bitfield_struct::bitfield; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::{ - Schema, - schema::{DataModelType, NamedType}, -}; use serde::{Deserialize, Serialize}; /// Indicators defined in the HID spec 11.1 @@ -41,14 +36,6 @@ pub struct LedIndicator { _reserved: u8, } -#[cfg(feature = "rmk_protocol")] -impl Schema for LedIndicator { - const SCHEMA: &'static NamedType = &NamedType { - name: "LedIndicator", - ty: &DataModelType::U8, - }; -} - impl BitOr for LedIndicator { type Output = Self; diff --git a/rmk-types/src/lib.rs b/rmk-types/src/lib.rs index cc80e4e8a..dc07873cc 100644 --- a/rmk-types/src/lib.rs +++ b/rmk-types/src/lib.rs @@ -23,7 +23,7 @@ //! //! ### Protocol //! - [`protocol::vial`] — Vial/Via protocol types -//! - [`protocol::rmk`] — RMK native protocol ICD (feature-gated: `rmk_protocol`) +//! - [`protocol::rynk`] — RMK native protocol ICD (feature-gated: `rynk`) //! //! ### Build-time //! - [`constants`] — Generated from `keyboard.toml` by `build.rs` diff --git a/rmk-types/src/modifier.rs b/rmk-types/src/modifier.rs index 423ec91e3..1cfe34f8d 100644 --- a/rmk-types/src/modifier.rs +++ b/rmk-types/src/modifier.rs @@ -7,11 +7,6 @@ use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use bitfield_struct::bitfield; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::{ - Schema, - schema::{DataModelType, NamedType}, -}; use serde::{Deserialize, Serialize}; use crate::keycode::HidKeyCode; @@ -51,14 +46,6 @@ crate::impl_debug_list!(ModifierCombination, |self| [ .into_iter() .filter_map(|(state, label)| state.then_some(label))); -#[cfg(feature = "rmk_protocol")] -impl Schema for ModifierCombination { - const SCHEMA: &'static NamedType = &NamedType { - name: "ModifierCombination", - ty: &DataModelType::U8, - }; -} - impl BitOr for ModifierCombination { type Output = Self; diff --git a/rmk-types/src/morse.rs b/rmk-types/src/morse.rs index dd28314d1..fdc9583b8 100644 --- a/rmk-types/src/morse.rs +++ b/rmk-types/src/morse.rs @@ -7,10 +7,6 @@ use heapless::LinearMap; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::Schema; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::schema::{DataModelType, NamedType, NamedValue}; use serde::{Deserialize, Serialize}; use crate::action::Action; @@ -23,7 +19,6 @@ use crate::constants::MORSE_SIZE; /// Mode for morse key behavior #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[repr(u8)] pub enum MorseMode { /// Same as QMK's permissive hold: https://docs.qmk.fm/tap_hold#tap-or-hold-decision-modes @@ -53,7 +48,6 @@ pub enum MorseMode { /// - `hold_timeout` (bits 12-0): hold timeout in ms (0 = None, max 8191) #[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize, MaxSize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] pub struct MorseProfile(u32); const TIMEOUT_MASK: u32 = 0x1FFF; @@ -232,7 +226,6 @@ impl From for u32 { /// MorsePattern is a sequence of maximum 15 taps or holds that can be encoded into an u16: /// 0x1 when empty, then 0 for tap or 1 for hold shifted from the right #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)] -#[cfg_attr(feature = "rmk_protocol", derive(Schema))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct MorsePattern(u16); @@ -354,36 +347,9 @@ impl PartialEq for Morse { impl Eq for Morse {} -/// Manual Schema impl because Morse uses custom serde for LinearMap. -/// The wire format is: (MorseProfile, Vec<(u16, Action)>). -/// -/// **Important:** This must stay in sync with the custom serde impl in -/// `morse_actions_serde`. If the wire format changes, update this Schema -/// accordingly. The `morse_schema_matches_wire_format` test validates -/// this invariant. -#[cfg(feature = "rmk_protocol")] -impl Schema for Morse { - const SCHEMA: &'static NamedType = &NamedType { - name: "Morse", - ty: &DataModelType::Struct(&[ - &NamedValue { - name: "profile", - ty: ::SCHEMA, - }, - &NamedValue { - name: "actions", - ty: &NamedType { - name: "MorseActions", - ty: &DataModelType::Seq(&NamedType { - name: "MorseActionEntry", - ty: &DataModelType::Tuple(&[::SCHEMA, ::SCHEMA]), - }), - }, - }, - ]), - }; -} - +/// Wire format note: `Morse` uses a custom serde impl for the `LinearMap` +/// of actions. The on-wire shape is `(MorseProfile, Vec<(u16, Action)>)`. +/// The `morse_wire_format` test below pins this contract. impl Morse { pub fn new_from_vial( tap: Action, @@ -742,15 +708,14 @@ mod tests { assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold)); } - /// Validates that the manual Schema impl matches the actual serde wire format. - /// - /// The Schema claims Morse serializes as: - /// struct { profile: MorseProfile, actions: Vec<(u16, Action)> } + /// Pins the on-wire shape of `Morse`: + /// `(MorseProfile, Vec<(u16, Action)>)` /// - /// We verify this by checking that a Morse value can be reconstructed by - /// manually deserializing its two fields in order using the same bytes. + /// `Morse` uses a custom serde impl for the `LinearMap` of actions; this + /// test verifies a Morse value can be reconstructed by manually + /// deserializing those two fields from the same byte stream. #[test] - fn morse_schema_matches_wire_format() { + fn morse_wire_format() { use postcard::to_slice; // Build a Morse with known data diff --git a/rmk-types/src/mouse_button.rs b/rmk-types/src/mouse_button.rs index 29a5a17e0..4ec8acb98 100644 --- a/rmk-types/src/mouse_button.rs +++ b/rmk-types/src/mouse_button.rs @@ -6,11 +6,6 @@ use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use bitfield_struct::bitfield; use postcard::experimental::max_size::MaxSize; -#[cfg(feature = "rmk_protocol")] -use postcard_schema::{ - Schema, - schema::{DataModelType, NamedType}, -}; use serde::{Deserialize, Serialize}; /// Mouse buttons @@ -99,11 +94,3 @@ impl MouseButtons { .with_button8(button8) } } - -#[cfg(feature = "rmk_protocol")] -impl Schema for MouseButtons { - const SCHEMA: &'static NamedType = &NamedType { - name: "MouseButtons", - ty: &DataModelType::U8, - }; -} diff --git a/rmk-types/src/protocol/mod.rs b/rmk-types/src/protocol/mod.rs index 062a837e5..9711e1e4e 100644 --- a/rmk-types/src/protocol/mod.rs +++ b/rmk-types/src/protocol/mod.rs @@ -4,14 +4,13 @@ //! //! - [`vial`] — Legacy Vial/Via protocol for compatibility with the Vial GUI. //! Always available. -//! - [`rmk`] — RMK native protocol built on postcard-rpc. Provides typed -//! endpoints for keymap, combo, morse, fork, encoder, macro, and status -//! queries over COBS-framed byte streams (USB bulk or BLE serial). -//! Enabled by the `rmk_protocol` feature. +//! - [`rynk`] — RMK native protocol. Carries `KeyAction`, `Combo`, `Morse`, +//! `Fork`, `EncoderAction`, `BatteryStatus`, `BleStatus` on the wire over +//! a 5-byte fixed header + postcard payload. Enabled by the `rynk` feature. //! //! The two protocols are mutually exclusive at the firmware level -//! (`rmk_protocol` and `vial` features cannot be enabled together). +//! (`rynk` and `vial` features cannot be enabled together). -#[cfg(feature = "rmk_protocol")] -pub mod rmk; +#[cfg(feature = "rynk")] +pub mod rynk; pub mod vial; diff --git a/rmk-types/src/protocol/rmk/endpoints.rs b/rmk-types/src/protocol/rmk/endpoints.rs deleted file mode 100644 index 05d72bc28..000000000 --- a/rmk-types/src/protocol/rmk/endpoints.rs +++ /dev/null @@ -1,446 +0,0 @@ -//! Endpoint declarations for the RMK protocol. -//! -//! Each `endpoints!` block declares one logical group. Feature-gated groups -//! provide an empty fallback constant when their feature is disabled, so the -//! combined `ENDPOINT_LIST` always type-checks. -//! -//! ## Adding a new conditional endpoint group -//! -//! 1. Add the `endpoints!` block with its `#[cfg(...)]` guard. -//! 2. Add a `#[cfg(not(...))]` empty fallback constant (`EndpointMap` with empty slices). -//! 3. Add the list to the single `ENDPOINT_LIST` definition. -//! 4. Add the group's endpoints to `endpoint_list_contains_all_non_bulk_endpoints` test. -//! 5. If feature-gated, add the group to the matching `endpoint_keys_*_locked` snapshot test. - -// The postcard-rpc endpoints! macro performs heavy const-eval for type uniqueness checks. -#![allow(long_running_const_eval)] - -use postcard_rpc::endpoints; - -use super::*; -use crate::action::{EncoderAction, KeyAction}; -#[cfg(feature = "_ble")] -use crate::battery::BatteryStatus; -#[cfg(feature = "_ble")] -use crate::ble::BleStatus; -use crate::combo::Combo; -use crate::connection::ConnectionType; -use crate::fork::Fork; -use crate::morse::Morse; - -endpoints! { - list = SYSTEM_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetVersion | () | ProtocolVersion | "sys/version" | - | GetCapabilities | () | DeviceCapabilities | "sys/caps" | - | GetLockStatus | () | LockStatus | "sys/lock_status" | - | UnlockRequest | () | UnlockChallenge | "sys/unlock" | - | LockRequest | () | () | "sys/lock" | - | Reboot | () | () | "sys/reboot" | - | BootloaderJump | () | () | "sys/bootloader" | - | StorageReset | StorageResetMode | () | "sys/storage_reset" | -} - -endpoints! { - list = KEYMAP_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetKeyAction | KeyPosition | KeyAction | "keymap/get" | - | SetKeyAction | SetKeyRequest | RmkResult | "keymap/set" | - | GetDefaultLayer | () | u8 | "keymap/default_layer" | - | SetDefaultLayer | u8 | RmkResult | "keymap/set_default_layer" | -} - -#[cfg(feature = "bulk")] -endpoints! { - list = KEYMAP_BULK_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetKeymapBulk | GetKeymapBulkRequest | GetKeymapBulkResponse | "keymap/bulk_get" | - | SetKeymapBulk | SetKeymapBulkRequest | RmkResult | "keymap/bulk_set" | -} - -#[cfg(not(feature = "bulk"))] -pub const KEYMAP_BULK_ENDPOINT_LIST: postcard_rpc::EndpointMap = postcard_rpc::EndpointMap { - types: &[], - endpoints: &[], -}; - -endpoints! { - list = ENCODER_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetEncoderAction | GetEncoderRequest | EncoderAction | "encoder/get" | - | SetEncoderAction | SetEncoderRequest | RmkResult | "encoder/set" | -} - -endpoints! { - list = MACRO_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetMacro | GetMacroRequest | MacroData | "macro/get" | - | SetMacro | SetMacroRequest | RmkResult | "macro/set" | -} - -endpoints! { - list = COMBO_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetCombo | u8 | Combo | "combo/get" | - | SetCombo | SetComboRequest | RmkResult | "combo/set" | -} - -#[cfg(feature = "bulk")] -endpoints! { - list = COMBO_BULK_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetComboBulk | GetComboBulkRequest | GetComboBulkResponse | "combo/bulk_get" | - | SetComboBulk | SetComboBulkRequest | RmkResult | "combo/bulk_set" | -} - -#[cfg(not(feature = "bulk"))] -pub const COMBO_BULK_ENDPOINT_LIST: postcard_rpc::EndpointMap = postcard_rpc::EndpointMap { - types: &[], - endpoints: &[], -}; - -endpoints! { - list = MORSE_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetMorse | u8 | Morse | "morse/get" | - | SetMorse | SetMorseRequest | RmkResult | "morse/set" | -} - -#[cfg(feature = "bulk")] -endpoints! { - list = MORSE_BULK_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetMorseBulk | GetMorseBulkRequest | GetMorseBulkResponse | "morse/bulk_get" | - | SetMorseBulk | SetMorseBulkRequest | RmkResult | "morse/bulk_set" | -} - -#[cfg(not(feature = "bulk"))] -pub const MORSE_BULK_ENDPOINT_LIST: postcard_rpc::EndpointMap = postcard_rpc::EndpointMap { - types: &[], - endpoints: &[], -}; - -endpoints! { - list = FORK_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetFork | u8 | Fork | "fork/get" | - | SetFork | SetForkRequest | RmkResult | "fork/set" | -} - -endpoints! { - list = BEHAVIOR_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetBehaviorConfig | () | BehaviorConfig | "behavior/get" | - | SetBehaviorConfig | BehaviorConfig | RmkResult | "behavior/set" | -} - -endpoints! { - list = CONNECTION_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetConnectionType | () | ConnectionType | "conn/type" | - | SetConnectionType | ConnectionType | RmkResult | "conn/set_type" | -} - -endpoints! { - list = STATUS_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetCurrentLayer | () | u8 | "status/layer/get" | - | GetMatrixState | () | MatrixState | "status/matrix/get" | -} - -#[cfg(feature = "_ble")] -endpoints! { - list = BLE_CONNECTION_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetBleStatus | () | BleStatus | "conn/ble" | - | SwitchBleProfile | u8 | RmkResult | "conn/switch_ble" | - | ClearBleProfile | u8 | RmkResult | "conn/clear_ble" | -} - -/// Empty endpoint list for when BLE is not available. -#[cfg(not(feature = "_ble"))] -pub const BLE_CONNECTION_ENDPOINT_LIST: postcard_rpc::EndpointMap = postcard_rpc::EndpointMap { - types: &[], - endpoints: &[], -}; - -#[cfg(feature = "_ble")] -endpoints! { - list = BLE_STATUS_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetBatteryStatus | () | BatteryStatus | "status/battery/get" | -} - -#[cfg(not(feature = "_ble"))] -pub const BLE_STATUS_ENDPOINT_LIST: postcard_rpc::EndpointMap = postcard_rpc::EndpointMap { - types: &[], - endpoints: &[], -}; - -#[cfg(all(feature = "_ble", feature = "split"))] -endpoints! { - list = SPLIT_STATUS_ENDPOINT_LIST; - omit_std = true; - | EndpointTy | RequestTy | ResponseTy | Path | - | ---------- | --------- | ---------- | ---- | - | GetPeripheralStatus | u8 | PeripheralStatus | "status/peripheral/get" | -} - -#[cfg(not(all(feature = "_ble", feature = "split")))] -pub const SPLIT_STATUS_ENDPOINT_LIST: postcard_rpc::EndpointMap = postcard_rpc::EndpointMap { - types: &[], - endpoints: &[], -}; - -/// Build an `EndpointMap` from a list of endpoint group constants. -/// -/// Each argument must be a `postcard_rpc::EndpointList` (as produced by `endpoints!`). -/// The standard ICD endpoints are always included automatically. -macro_rules! build_endpoint_map { - ($($list:expr),* $(,)?) => { - const { - use postcard_rpc::postcard_schema::schema::{DataModelType, NamedType}; - use postcard_rpc::{EndpointMap, Key}; - - const NULL_KEY: Key = unsafe { Key::from_bytes([0u8; 8]) }; - const NULL_TY: &NamedType = &NamedType { - name: "", - ty: &DataModelType::Unit, - }; - - const TYPE_SLICES: &[&[&NamedType]] = &[ - postcard_rpc::standard_icd::STANDARD_ICD_ENDPOINTS.types, - $($list.types,)* - ]; - const TYPE_LEN: usize = postcard_rpc::uniques::total_len(TYPE_SLICES); - const TYPES: [&NamedType; TYPE_LEN] = - postcard_rpc::uniques::combine_with_copy(TYPE_SLICES, NULL_TY); - - const EP_SLICES: &[&[(&str, Key, Key)]] = &[ - postcard_rpc::standard_icd::STANDARD_ICD_ENDPOINTS.endpoints, - $($list.endpoints,)* - ]; - const EP_LEN: usize = postcard_rpc::uniques::total_len(EP_SLICES); - const EPS: [(&str, Key, Key); EP_LEN] = - postcard_rpc::uniques::combine_with_copy(EP_SLICES, ("", NULL_KEY, NULL_KEY)); - - EndpointMap { - types: TYPES.as_slice(), - endpoints: EPS.as_slice(), - } - } - }; -} - -/// Full endpoint map for the RMK protocol. -/// -/// Assembled from smaller endpoint groups to avoid very large const-eval -/// workloads in a single `endpoints!` invocation. -/// Feature-gated groups (bulk, BLE, split) use empty fallback constants -/// when the corresponding feature is disabled. -pub const ENDPOINT_LIST: postcard_rpc::EndpointMap = build_endpoint_map!( - SYSTEM_ENDPOINT_LIST, - KEYMAP_ENDPOINT_LIST, - KEYMAP_BULK_ENDPOINT_LIST, - ENCODER_ENDPOINT_LIST, - MACRO_ENDPOINT_LIST, - COMBO_ENDPOINT_LIST, - COMBO_BULK_ENDPOINT_LIST, - MORSE_ENDPOINT_LIST, - MORSE_BULK_ENDPOINT_LIST, - FORK_ENDPOINT_LIST, - BEHAVIOR_ENDPOINT_LIST, - CONNECTION_ENDPOINT_LIST, - BLE_CONNECTION_ENDPOINT_LIST, - STATUS_ENDPOINT_LIST, - BLE_STATUS_ENDPOINT_LIST, - SPLIT_STATUS_ENDPOINT_LIST, -); - -#[cfg(test)] -mod tests { - extern crate alloc; - - use postcard_rpc::{Endpoint, Key}; - - use super::*; - use crate::protocol::rmk::snapshot; - - /// Collect (path, req_bytes, resp_bytes) entries for a set of endpoint groups. - fn collect<'a>(groups: &[&'a [(&'a str, Key, Key)]]) -> alloc::vec::Vec<(&'a str, [u8; 8], [u8; 8])> { - groups - .iter() - .flat_map(|g| g.iter()) - .map(|(path, req, resp)| (*path, req.to_bytes(), resp.to_bytes())) - .collect() - } - - /// `sys/version` is the protocol's gate: its REQ/RESP keys must be immortal. - /// Every host — regardless of age — relies on this endpoint being callable - /// and returning a shape it can decode. Changing these bytes strands every - /// older host, which is why this test does NOT honor `UPDATE_SNAPSHOTS`: - /// the constants below may only be edited as part of an explicit protocol - /// break with design review and a documented major bump. - /// - /// See the "Protocol Handshake" section in `protocol/rmk/mod.rs` for the - /// full versioning rule. - #[test] - fn sys_version_frozen() { - assert_eq!( - GetVersion::REQ_KEY.to_bytes(), - [0xd6, 0xee, 0xc1, 0xcd, 0xd6, 0x16, 0xfc, 0x29], - "sys/version REQ_KEY changed — forbidden. This key is the protocol's \ - compatibility gate and must never change; see protocol/rmk/mod.rs.", - ); - assert_eq!( - GetVersion::RESP_KEY.to_bytes(), - [0x82, 0xc8, 0xf1, 0x04, 0xc9, 0x4e, 0x7e, 0x8a], - "sys/version RESP_KEY changed — forbidden. Changing ProtocolVersion's \ - shape breaks every older host; see protocol/rmk/mod.rs.", - ); - } - - /// Lock down endpoint schema fingerprints for the always-on groups. - /// Each Key is an 8-byte hash of (path, postcard schema of req/resp). - /// Any change to a request/response type — including transitively-referenced - /// types — flips the corresponding Key, and this snapshot fails. - /// Update the snapshot intentionally with `UPDATE_SNAPSHOTS=1`. - #[test] - fn endpoint_keys_base_locked() { - let entries = collect(&[ - SYSTEM_ENDPOINT_LIST.endpoints, - KEYMAP_ENDPOINT_LIST.endpoints, - ENCODER_ENDPOINT_LIST.endpoints, - MACRO_ENDPOINT_LIST.endpoints, - COMBO_ENDPOINT_LIST.endpoints, - MORSE_ENDPOINT_LIST.endpoints, - FORK_ENDPOINT_LIST.endpoints, - BEHAVIOR_ENDPOINT_LIST.endpoints, - CONNECTION_ENDPOINT_LIST.endpoints, - STATUS_ENDPOINT_LIST.endpoints, - ]); - let actual = snapshot::format_endpoint_keys("snapshots/endpoint_keys_base.snap", &entries); - snapshot::assert_snapshot("snapshots/endpoint_keys_base.snap", actual); - } - - #[cfg(feature = "bulk")] - #[test] - fn endpoint_keys_bulk_locked() { - let entries = collect(&[ - KEYMAP_BULK_ENDPOINT_LIST.endpoints, - COMBO_BULK_ENDPOINT_LIST.endpoints, - MORSE_BULK_ENDPOINT_LIST.endpoints, - ]); - let actual = snapshot::format_endpoint_keys("snapshots/endpoint_keys_bulk.snap", &entries); - snapshot::assert_snapshot("snapshots/endpoint_keys_bulk.snap", actual); - } - - #[cfg(feature = "_ble")] - #[test] - fn endpoint_keys_ble_locked() { - let entries = collect(&[ - BLE_CONNECTION_ENDPOINT_LIST.endpoints, - BLE_STATUS_ENDPOINT_LIST.endpoints, - ]); - let actual = snapshot::format_endpoint_keys("snapshots/endpoint_keys_ble.snap", &entries); - snapshot::assert_snapshot("snapshots/endpoint_keys_ble.snap", actual); - } - - #[cfg(all(feature = "_ble", feature = "split"))] - #[test] - fn endpoint_keys_ble_split_locked() { - let entries = collect(&[SPLIT_STATUS_ENDPOINT_LIST.endpoints]); - let actual = snapshot::format_endpoint_keys("snapshots/endpoint_keys_ble_split.snap", &entries); - snapshot::assert_snapshot("snapshots/endpoint_keys_ble_split.snap", actual); - } - - /// Verify that every non-bulk endpoint is present in the combined ENDPOINT_LIST. - /// This catches divergence when new endpoint groups are added to one cfg block - /// but not the other. - #[test] - fn endpoint_list_contains_all_non_bulk_endpoints() { - let endpoint_paths: alloc::collections::BTreeSet<&str> = - ENDPOINT_LIST.endpoints.iter().map(|(path, _, _)| *path).collect(); - - let non_bulk_groups: &[&[(&str, Key, Key)]] = &[ - postcard_rpc::standard_icd::STANDARD_ICD_ENDPOINTS.endpoints, - SYSTEM_ENDPOINT_LIST.endpoints, - KEYMAP_ENDPOINT_LIST.endpoints, - ENCODER_ENDPOINT_LIST.endpoints, - MACRO_ENDPOINT_LIST.endpoints, - COMBO_ENDPOINT_LIST.endpoints, - MORSE_ENDPOINT_LIST.endpoints, - FORK_ENDPOINT_LIST.endpoints, - BEHAVIOR_ENDPOINT_LIST.endpoints, - CONNECTION_ENDPOINT_LIST.endpoints, - BLE_CONNECTION_ENDPOINT_LIST.endpoints, - STATUS_ENDPOINT_LIST.endpoints, - BLE_STATUS_ENDPOINT_LIST.endpoints, - SPLIT_STATUS_ENDPOINT_LIST.endpoints, - ]; - - for group in non_bulk_groups { - for (path, _, _) in *group { - assert!( - endpoint_paths.contains(path), - "Endpoint '{}' missing from ENDPOINT_LIST — update both cfg blocks in endpoints.rs", - path - ); - } - } - } - - /// Verify that every bulk endpoint is present in the combined ENDPOINT_LIST. - #[cfg(feature = "bulk")] - #[test] - fn endpoint_list_contains_all_bulk_endpoints() { - let endpoint_paths: alloc::collections::BTreeSet<&str> = - ENDPOINT_LIST.endpoints.iter().map(|(path, _, _)| *path).collect(); - - let bulk_groups: &[&[(&str, Key, Key)]] = &[ - KEYMAP_BULK_ENDPOINT_LIST.endpoints, - COMBO_BULK_ENDPOINT_LIST.endpoints, - MORSE_BULK_ENDPOINT_LIST.endpoints, - ]; - - for group in bulk_groups { - for (path, _, _) in *group { - assert!( - endpoint_paths.contains(path), - "Bulk endpoint '{}' missing from ENDPOINT_LIST", - path - ); - } - } - } -} diff --git a/rmk-types/src/protocol/rmk/mod.rs b/rmk-types/src/protocol/rmk/mod.rs deleted file mode 100644 index 75f850e08..000000000 --- a/rmk-types/src/protocol/rmk/mod.rs +++ /dev/null @@ -1,521 +0,0 @@ -//! RMK protocol ICD (Interface Control Document). -//! -//! This module defines the shared type contract between firmware and host for the -//! RMK communication protocol. It contains all endpoint and topic declarations, -//! request/response types, and protocol constants. -//! -//! The protocol uses postcard-rpc's type-level endpoint definitions over COBS-framed -//! byte streams (USB bulk transfer and BLE serial). -//! -//! ## Module layout -//! -//! - [`endpoints`] — `endpoints!` macro invocations + assembled `ENDPOINT_LIST` -//! - [`topics`] — `topics!` macro invocations -//! - [`system`] — handshake, lock/unlock, storage reset, behavior config -//! - [`keymap`], [`encoder`], [`macro_data`], [`combo`], [`morse`], [`fork`] — per-domain request/response types -//! - [`status`] — runtime status types (matrix state, peripheral status) -//! -//! ## Protocol Handshake -//! -//! **Versioning rule.** `sys/version`'s path and `ProtocolVersion`'s shape are -//! immortal. Hosts call `GetVersion` first, compare `major` / `minor`, and -//! bail cleanly on mismatch — it is the only endpoint guaranteed to be -//! callable across every firmware version, and it is the gate that makes -//! the rest of the protocol forward-compatible. -//! -//! Connection flow: -//! 1. Host connects over USB bulk or BLE serial (COBS-framed). -//! 2. Host sends `GetVersion`. If `major` differs from the host's supported -//! major, or `minor` exceeds the host's known maximum, the host aborts -//! with an "update host" diagnostic. It does not proceed to any other -//! endpoint. -//! 3. Host sends `GetCapabilities` — learns layout, feature set, and limits. -//! 4. Host gates every subsequent call on the capability flags -//! (e.g. `bulk_transfer_supported`, `ble_enabled`). -//! 5. If the device is locked, host sends `UnlockRequest` and completes the -//! physical key challenge before issuing write operations. -//! -//! ### Version bump policy -//! -//! - `minor`: new endpoint; new field appended to a wire struct; new variant -//! in a wire enum (including `RmkError`). -//! - `major`: endpoint removed or retyped; struct field reshaped; enum -//! variant renamed or renumbered. `sys/version` itself is exempt — -//! changing its shape is forbidden even across major bumps. -//! - Neither: no wire change. - -mod combo; -mod encoder; -mod endpoints; -mod fork; -mod keymap; -mod macro_data; -mod morse; -mod status; -mod system; -mod topics; - -use postcard::experimental::max_size::MaxSize; -use postcard_schema::Schema; -use serde::{Deserialize, Serialize}; - -// Re-export every submodule's public items into `protocol::rmk::*` for -// convenient endpoint registration. Domain types (Combo, Morse, Fork, etc.) -// are NOT re-exported here — import them from their canonical crate-root -// modules instead. -pub use self::combo::*; -pub use self::encoder::*; -pub use self::endpoints::*; -pub use self::fork::*; -pub use self::keymap::*; -pub use self::macro_data::*; -pub use self::morse::*; -pub use self::status::*; -pub use self::system::*; -pub use self::topics::*; - -// --------------------------------------------------------------------------- -// Protocol-wide error primitives -// --------------------------------------------------------------------------- - -/// Protocol-level error type returned by write operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize, Schema)] -#[non_exhaustive] -pub enum RmkError { - /// The request parameters are invalid or out of range. - InvalidParameter, - /// Operation not valid in current device state (e.g. device is locked). - BadState, - /// An internal firmware error occurred (storage, contention, etc). - InternalError, -} - -/// Result type for write operations. -/// -/// This is a type alias rather than a newtype. `Schema` and `MaxSize` are -/// provided by postcard's blanket impls for `Result`. The endpoint -/// key is derived from the schema structure (not the Rust path), so the -/// alias is stable. Cross-endpoint collision tests in this module verify -/// key uniqueness. -pub type RmkResult = Result<(), RmkError>; - -// --------------------------------------------------------------------------- -// Test utilities (shared across submodule test mods) -// --------------------------------------------------------------------------- - -#[cfg(test)] -pub(crate) mod test_utils { - extern crate alloc; - - use alloc::vec; - - use postcard::experimental::max_size::MaxSize; - use serde::{Deserialize, Serialize}; - - /// Buffer size used by round-trip / max-size helpers. - /// - /// Sized at twice the type's declared `POSTCARD_MAX_SIZE` plus a small - /// fixed slack so that: - /// - under feature configurations with a large `BULK_SIZE` (notably - /// `host`, where `BULK_SIZE = MAX_BULK_SIZE = 16` and - /// `MORSE_SIZE = MAX_MORSE_SIZE = 32`), max-capacity bulk payloads - /// still fit comfortably; - /// - an under-counted manual `MaxSize` impl produces a clear assertion - /// failure in `assert_max_size_bound` instead of a `SerializeBufferFull` - /// panic. - fn buffer_capacity() -> usize { - T::POSTCARD_MAX_SIZE.saturating_mul(2).saturating_add(64) - } - - /// Postcard round-trip helper used by every submodule's tests. - pub fn round_trip(val: &T) -> T - where - T: Serialize + for<'de> Deserialize<'de> + PartialEq + core::fmt::Debug + MaxSize, - { - let mut buf = vec![0u8; buffer_capacity::()]; - let bytes = postcard::to_slice(val, &mut buf).expect("serialize"); - let decoded: T = postcard::from_bytes(bytes).expect("deserialize"); - assert_eq!(&decoded, val); - decoded - } - - /// Assert that `val` serializes within its declared `POSTCARD_MAX_SIZE`. - /// Use alongside `round_trip` in max-capacity tests to catch under-counted - /// manual `MaxSize` impls (the dangerous bug — buffer overflows downstream). - pub fn assert_max_size_bound(val: &T) - where - T: Serialize + MaxSize, - { - let mut buf = vec![0u8; buffer_capacity::()]; - let bytes = postcard::to_slice(val, &mut buf).expect("serialize"); - assert!( - bytes.len() <= T::POSTCARD_MAX_SIZE, - "{} encoded to {} bytes but POSTCARD_MAX_SIZE = {}", - core::any::type_name::(), - bytes.len(), - T::POSTCARD_MAX_SIZE, - ); - } -} - -// --------------------------------------------------------------------------- -// Wire-format snapshot harness (golden-file tests) -// --------------------------------------------------------------------------- - -#[cfg(test)] -pub(crate) mod snapshot { - extern crate alloc; - extern crate std; - - use alloc::format; - use alloc::string::String; - use alloc::vec::Vec; - use std::path::PathBuf; - use std::{env, fs}; - - /// Format a byte slice as lowercase, space-separated hex (e.g. `01 0a ff`). - pub fn hex(bytes: &[u8]) -> String { - let mut s = String::with_capacity(bytes.len() * 3); - for (i, b) in bytes.iter().enumerate() { - if i > 0 { - s.push(' '); - } - s.push_str(&format!("{:02x}", b)); - } - s - } - - /// Build the snapshot text for an endpoint key list. - /// Output format: ` REQ RESP `, sorted by path. - pub fn format_endpoint_keys(rel_path: &str, entries: &[(&str, [u8; 8], [u8; 8])]) -> String { - let mut sorted: Vec<&(&str, [u8; 8], [u8; 8])> = entries.iter().collect(); - sorted.sort_by_key(|(path, _, _)| *path); - - // Pad the path column to the longest entry for readable diffs. - let path_width = sorted.iter().map(|(p, _, _)| p.len()).max().unwrap_or(0); - - let mut out = String::new(); - out.push_str(&format!( - "# Endpoint Key snapshot — DO NOT edit by hand.\n\ - # File: {}\n\ - # Each Key is an 8-byte hash of (path, postcard schema of req/resp).\n\ - # Any change to a request/response type — including transitively-referenced\n\ - # types — flips the corresponding Key. If the change is intentional, regenerate:\n\ - # UPDATE_SNAPSHOTS=1 cargo test -p rmk-types --features rmk_protocol\n\ - # Format: REQ <8-byte hex> RESP <8-byte hex>\n\ - \n", - rel_path, - )); - for (path, req, resp) in sorted { - out.push_str(&format!( - "{:width$} REQ {} RESP {}\n", - path, - hex(req), - hex(resp), - width = path_width, - )); - } - out - } - - /// Build the snapshot text for a topic key list. - /// Output format: ` KEY `, sorted by path. - pub fn format_topic_keys(rel_path: &str, entries: &[(&str, [u8; 8])]) -> String { - let mut sorted: Vec<&(&str, [u8; 8])> = entries.iter().collect(); - sorted.sort_by_key(|(path, _)| *path); - - let path_width = sorted.iter().map(|(p, _)| p.len()).max().unwrap_or(0); - - let mut out = String::new(); - out.push_str(&format!( - "# Topic Key snapshot — DO NOT edit by hand.\n\ - # File: {}\n\ - # Regenerate intentionally with:\n\ - # UPDATE_SNAPSHOTS=1 cargo test -p rmk-types --features rmk_protocol\n\ - # Format: KEY <8-byte hex>\n\ - \n", - rel_path, - )); - for (path, key) in sorted { - out.push_str(&format!("{:width$} KEY {}\n", path, hex(key), width = path_width,)); - } - out - } - - /// Build the snapshot text for a list of (label, encoded bytes) pairs. - pub fn format_value_snapshot(rel_path: &str, entries: &[(&str, &[u8])]) -> String { - let mut sorted: Vec<&(&str, &[u8])> = entries.iter().collect(); - sorted.sort_by_key(|(label, _)| *label); - - let label_width = sorted.iter().map(|(l, _)| l.len()).max().unwrap_or(0); - - let mut out = String::new(); - out.push_str(&format!( - "# Wire-format value snapshot — DO NOT edit by hand.\n\ - # File: {}\n\ - # Each entry is the postcard byte encoding of a fixed value. A diff here means\n\ - # the wire format changed (either intentionally or by accident). Regenerate with:\n\ - # UPDATE_SNAPSHOTS=1 cargo test -p rmk-types --features rmk_protocol wire_values\n\ - # Format: