From v0.8 to v0.9

v0.9 introduces Rynk, RMK's optional native host protocol, while keeping Vial as the default. v0.9 also updates the embassy and BLE dependency stacks, unifies the event/processor Rust API, restructures keyboard.toml to separate the electrical matrix, physical layout, and logical keymap, and includes a few smaller breaking changes.

Each change below is labeled with who it affects. Use the table to find what applies to you, then follow the steps in that section. Changes are grouped as Everyone and Rust API developers (examples/use_rust); keyboard.toml users (examples/use_config) are affected only by the Everyone changes.


Everyone

Rynk is a new optional host protocol

Rynk is RMK's native host protocol and a new opt-in alternative to Vial. Vial remains the default host protocol. The rmk crate's default features changed only to replace the removed vial_lock alias with its underlying host_lock feature:

v0.8 default featuresv0.9 default features
defmt, storage, vial, vial_lock, watchdogdefmt, storage, vial, host_lock, watchdog

Rynk and Vial are mutually exclusive — enable exactly one. If you relied on the defaults, your firmware continues to use Vial with its physical-presence lock.

If you use keyboard.toml

The [host] section must match the rmk Cargo features — RMK checks this when it builds and stops with an error if they disagree.

To keep Vial, no host-protocol changes are needed. The default [host] values continue to match the rmk default features.

To opt into Rynk, update [host]:

keyboard.toml
[host]
vial_enabled = false
rynk_enabled = true

Then turn off default features and explicitly enable rynk along with the other features you need:

Cargo.toml
rmk = { version = "0.9", default-features = false, features = [
    "defmt",
    "storage",
    "rynk",
    "watchdog",
    # ...your chip and other features
] }

See Host Configuration for the full set of options.

If you use the Rust API

Step 1 — pick the protocol in Cargo.toml. Rust projects that list vial explicitly do not need to change it. To move to Rynk, disable default features and replace vial (and vial_lock) with rynk in your explicit feature list.

Step 2 — update how you wire up the host service. It no longer takes a separate KeyboardContext, and it now attaches to a transport instead of running as its own task.

Before (v0.8):

let host_ctx = rmk::host::KeyboardContext::new(&keymap);
let mut host_service = HostService::new(&host_ctx, &rmk_config);

let mut usb_transport = UsbTransport::new(driver, rmk_config.device_config);

run_all!(matrix, storage, usb_transport, keyboard, host_service, watchdog_runner).await;

After (v0.9):

let host_service = HostService::new(&keymap, &rmk_config);

// Attach the host service to each transport that should serve it.
let mut usb_transport =
    UsbTransport::new(driver, rmk_config.device_config).with_host_service(&host_service);

// `host_service` is no longer passed to `run_all!` — the transport drives it.
run_all!(matrix, storage, usb_transport, keyboard, watchdog_runner).await;

On wireless boards, attach it to the BLE transport the same way:

let mut ble_transport = BleTransport::new(&stack, rmk_config).with_host_service(&host_service);

HostService is protocol-agnostic — it resolves to whichever of rynk or vial you enabled, so the wiring above is the same for both.

keyboard.toml layout restructured

Who this affects: everyone using keyboard.toml.

The keyboard description is now split into three sections: [matrix] (electrical wiring, unchanged), [layout] (physical arrangement), and the new [keymap] (layer count and key actions). The layer-related fields move out of [layout] into [keymap]:

Old (v0.8)New (v0.9)
[layout].matrix_map[layout].map
[layout].layers[keymap].layers
[[layer]][[keymap.layer]]
[layout].keymap (removed)[[keymap.layer]]
[layout].encoder_map (removed)[[keymap.layer]].encoders

[layout].rows and [layout].cols stay in [layout].

Before (v0.8):

[layout]
rows = 2
cols = 3
layers = 2
matrix_map = """
(0,0) (0,1) (0,2)
(1,0) (1,1) (1,2)
"""

[[layer]]
name = "base"
keys = """
A B C
D E F
"""

After (v0.9):

[layout]
rows = 2
cols = 3
map = """
(0,0) (0,1) (0,2)
(1,0) (1,1) (1,2)
"""

[keymap]
layers = 2

[[keymap.layer]]
name = "base"
keys = """
A B C
D E F
"""

Experimental protocol features renamed

Who this affects: the rare early adopters who enabled the experimental protocol features by name.

If your Cargo.toml used the experimental feature rmk_protocol, rename it to rynk. The separate bulk_transfer feature is gone — bulk transfers are now always on with rynk and sized by the [rmk].rynk_buffer_size knob, so just drop bulk_transfer from your feature list.

Old (v0.8)New (v0.9)
rmk_protocolrynk
bulk_transferremoved — always on, use rynk_buffer_size

The [rmk].protocol_max_bulk_size key was likewise removed; [rmk].rynk_buffer_size is now the single knob for bulk-transfer throughput.

Dependency updates

Who this affects: everyone — these versions live in your own Cargo.toml.

v0.9 moves to newer embassy and BLE-stack releases. Update your dependency versions to match the example for your chip. The main bumps are:

Dependencyv0.8v0.9
embassy-executor0.90.10
embassy-rp0.80.10
embassy-nrf0.80.11
trouble-host (BLE)0.50.7
bt-hci (BLE)0.60.9

embassy-time is unchanged (0.5). For nRF BLE boards, the nrf-sdc / nrf-mpsl git rev also moves forward — copy the exact rev (and any other pins) from the matching example under examples/ for your chip, since some boards track specific git revisions.


Rust API developers

Event and processor API is unified

Who this affects: anyone who wrote custom controllers, input processors, or events.

v0.9 consolidates the previously separate controller and input_processor APIs into a single, cohesive event/processor model. Rename the macros and functions as follows.

Macro changes

OldNew
#[controller_event]#[event]
#[input_event]#[event]
#[controller]#[processor]
#[input_processor]#[processor]
#[derive(InputEvent)]#[derive(Event)]
#[register_controller]#[register_processor]

Function changes

OldNew
publish_controller_event()publish_event()
publish_controller_event_async()publish_event_async()
publish_input_event()publish_event()
publish_input_event_async()publish_event_async()

Examples

Migrating a controller to a processor:

// Before (v0.8)
use rmk_macro::controller;

#[controller(subscribe = [LayerChangeEvent])]
pub struct DisplayController {
    display: Display,
}

// After (v0.9)
use rmk_macro::processor;

#[processor(subscribe = [LayerChangeEvent])]
pub struct DisplayProcessor {
    display: Display,
}

Migrating registration:

// Before (v0.8)
#[register_controller(event)]
fn display_controller() -> DisplayController {
    DisplayController::new()
}

// After (v0.9)
#[register_processor(event)]
fn display_processor() -> DisplayProcessor {
    DisplayProcessor::new()
}

Migrating custom events:

// Before (v0.8)
use rmk_macro::controller_event;

#[controller_event(channel_size = 8, subs = 2, pubs = 1)]
#[derive(Clone, Copy, Debug)]
pub struct MyCustomEvent {
    pub data: u8,
}

// After (v0.9)
use rmk_macro::event;

#[event(channel_size = 8, subs = 2, pubs = 1)]
#[derive(Clone, Copy, Debug)]
pub struct MyCustomEvent {
    pub data: u8,
}

Migrating multi-event enums:

// Before (v0.8)
use rmk_macro::InputEvent;

#[derive(InputEvent, Clone, Debug)]
pub enum MyEvents {
    Battery(BatteryEvent),
    Pointing(PointingEvent),
}

// After (v0.9)
use rmk_macro::Event;

#[derive(Event, Clone, Debug)]
pub enum MyEvents {
    Battery(BatteryEvent),
    Pointing(PointingEvent),
}

Pointing device API renamed

Who this affects: anyone constructing a pointing device in Rust.

The generic PointingDevice / PointingProcessor replace the PMW3610-specific Pmw3610Device / Pmw3610Processor. For the PMW3610, only the type name changes — the ::new() arguments are the same:

// Before (v0.8)
let device = Pmw3610Device::new(/* ...same args... */);

// After (v0.9)
let device = PointingDevice::new(/* ...same args... */);

keyboard.toml users don't need to do anything — the TOML config is unchanged.

PollingController interval is now a method

Who this affects: anyone who implemented PollingController for a custom input device.

The INTERVAL associated constant became an interval() method, so the interval can be chosen at runtime:

// Before (v0.8)
impl PollingController for MyDevice {
    const INTERVAL: Duration = Duration::from_millis(10);
}

// After (v0.9)
impl PollingController for MyDevice {
    fn interval(&self) -> Duration {
        Duration::from_millis(10)
    }
}

MouseKeyConfig fields renamed

Who this affects: anyone building a MouseKeyConfig in Rust. (keyboard.toml users are unaffected — the [rmk] mouse_key_interval / mouse_wheel_interval settings are unchanged.)

Old (v0.8)New (v0.9)
time_to_maxticks_to_max
wheel_time_to_maxwheel_ticks_to_max
wheel_max_speed_multiplierwheel_max_speed

The renames reflect that these values now count acceleration ticks, not milliseconds.

Tap-hold macros take a profile index

Who this affects: Rust keymaps that set custom tap-hold timing profiles, or that construct KeyAction::TapHold directly. (keyboard.toml users are unaffected — the macro interns profiles by name automatically.)

KeyAction::TapHold now carries a u8 index into the morse-profile table instead of an inline MorseProfile. The custom-profile macros thp! / mtp! / ltp! / ttp! take that index as their last argument: populate behavior.morse.profiles and pass the 0-based index of the profile you want. An index with no matching entry falls back to the default profile. The default-profile macros th! / mt! / lt! / tt! are unchanged.


Automatic changes (usually no action)

  • Event channel subscriber counts. Several events had their default subs count increased to make room for Rynk's host sessions. This is applied automatically. You only need to act if you pinned custom [event.<name>] subs values — recheck them against the new defaults. When enabling Rynk, also make sure there are enough subscriber slots, or the keyboard will panic on connect.
  • keyboard.toml rejects unknown keys. A leftover or misspelled key that v0.8 silently ignored is now an error at build time. No action unless the build complains — then remove or fix the flagged key.

New in v0.9 (no migration needed)

These are new capabilities, listed so you know they exist:

  • rmk::boot is now public — call rmk::boot::jump_to_bootloader() from your own code.
  • bootmagic — hold a designated key during boot to drop into the chip bootloader (works per-half on splits).
  • New input-device drivers — Azoteq IQS5xx trackpad and PMW3360 / PMW3389 mouse sensors.

See the changelog for the complete list.

Why these changes?

  • Rynk gives RMK a native host protocol that understands every RMK feature, instead of being limited to what the Vial standard supports.
  • The unified event/processor API collapses two parallel systems into one: a single #[event] macro, a single #[processor] macro, and one publish_event() family — simpler to learn and maintain, with no loss of functionality.