From v0.8 to v0.9

v0.9's headline change is Rynk, RMK's native host protocol, which replaces 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 the new default host protocol

Rynk is RMK's native host protocol — an alternative to Vial — and it is now enabled by default. The rmk crate's default features changed:

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

Rynk and Vial are mutually exclusive — enable exactly one. Note that vial_lock is also no longer a default feature; add it back explicitly if you use Vial's unlock keys.

If you relied on the defaults, your firmware now uses Rynk instead of Vial. Pick one of the paths below.

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.

Option A — switch to Rynk (recommended). The default features already include rynk, so you only need to update [host]:

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

Option B — keep Vial. Turn off default features and re-enable vial (plus vial_lock if you use unlock keys), and keep vial_enabled = true (the default) in keyboard.toml:

Cargo.toml
rmk = { version = "0.9", default-features = false, features = [
    "defmt",
    "storage",
    "vial",
    "vial_lock",
    "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 list features explicitly, so if you already listed vial, nothing forces a change. To move to Rynk, replace vial (and vial_lock) with rynk in your 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 features rmk_protocol or bulk_transfer, rename them:

Old (v0.8)New (v0.9)
rmk_protocolrynk
bulk_transferbulk

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.


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.

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.