Controller Support
RMK's controller system provides a unified interface for managing output devices like displays, LEDs, and other peripherals that respond to keyboard events.
Overview
RMK uses an event-driven architecture where event producers (keyboard, BLE stack, etc.) are decoupled from event consumers (controllers). This allows controllers to independently react to specific events they care about.
┌──────┐ ┌────────────┐
│ │ ┌──────▶│controller a│
│ │ │ └────────────┘
┌───────────────┐ │ │ │ ┌────────────┐
│event publisher│──publish──▶│events│──subscribe───▶│controller b│
└───────────────┘ │ │ │ └────────────┘
│ │ │ ┌────────────┐
│ │ └──────▶│controller c│
└──────┘ └────────────┘
Key concepts:
- Events - Carry state changes and keyboard events through type-safe channels
- Controllers - Subscribe to events and react accordingly
Controllers can operate in two modes:
- Event-driven - React to controller events as they arrive
- Polling - Subscribe to controller events and perform periodic updates at specified intervals
Built-in Features
RMK provides built-in events and controllers that you can use directly without writing custom code.
Built-in Events
RMK provides a type-safe event system where each event type has its own dedicated channel. The following built-in event types are available for controllers to subscribe:
Keyboard State Events:
LayerChangeEvent - Active layer changed
LedIndicatorEvent - LED indicator state changed (NumLock, CapsLock, ScrollLock)
WpmUpdateEvent - Words per minute updated
SleepStateEvent - Sleep state changed
Input Events:
KeyEvent - Key press/release event with processed key action
ModifierEvent - Modifier keys combination changed
Connection Events:
ConnectionChangeEvent - Connection type changed (USB/BLE)
BLE Events (when BLE is enabled):
BleStateChangeEvent - BLE connection state changed
BleProfileChangeEvent - BLE profile switched
Power Events (when BLE is enabled):
BatteryLevelEvent - Battery level changed
ChargingStateEvent - Charging state changed
Split Keyboard Events (when split is enabled):
PeripheralConnectedEvent - Peripheral connection state changed
CentralConnectedEvent - Connected to central state changed
PeripheralBatteryEvent - Peripheral battery level changed
ClearPeerEvent - Clear BLE peer information (BLE split only)
Built-in Controllers
RMK provides built-in LED indicator controllers for NumLock, CapsLock, and ScrollLock. These can be easily configured in keyboard.toml without writing any code:
[light]
# NumLock LED
numslock.pin = "PIN_1"
numslock.low_active = false
# CapsLock LED
capslock.pin = "PIN_2"
capslock.low_active = true
# ScrollLock LED
scrolllock.pin = "PIN_3"
scrolllock.low_active = false
The LED indicators automatically subscribe to LedIndicatorEvent and update based on host keyboard state.
Custom Controllers
RMK's controller system is designed for easy extension without modifying core code. You can define custom controllers using the #[controller] macro to extend keyboard functionality for displays, sensors, LEDs, and any other peripherals.
Defining Controllers
Controllers are defined using the #[controller] attribute macro on structs:
use rmk_macro::controller;
#[controller(subscribe = [LayerChangeEvent])]
pub struct MyController {
// Your controller fields
}
impl MyController {
async fn on_layer_change_event(&mut self, event: LayerChangeEvent) {
// Handle layer changes
}
}
Parameters:
subscribe = [Event1, Event2, ...] (required): Event types to subscribe to
poll_interval = <ms> (optional): Enable polling with fixed interval, requires poll() method
How it works:
#[controller] implements Controller trait automatically
- Routes events to
on_<event_name>_event() handler methods, where <event_name> is a snake case name converted from the subscribed event. For example, if your controller subscribes to BatteryLevelEvent, then async fn on_battery_level_event(&mut self, event: BatteryLevelEvent) should be implemented
- If
poll_interval is set, the controller operates in polling mode, a poll() method is required. poll() will be called at every poll_interval
Registering Controllers
Register controllers in your keyboard module with #[register_controller]:
#[rmk_keyboard]
mod keyboard {
#[register_controller(event)]
fn battery_led() -> BatteryLedController {
let pin = Output::new(p.PIN_4, Level::Low, OutputDrive::Standard);
BatteryLedController::new(pin, false)
}
}
Execution modes:
#[register_controller(event)]: Event-driven only, responds to events as they arrive
#[register_controller(poll)]: Event-driven + periodic polling, requires poll_interval parameter in #[controller] macro
Inside the registration function:
p variable provides access to chip peripherals
- Use
bind_interrupts! macro if additional interrupts are needed
Examples
Event-based Controller
Controllers can subscribe to one or multiple event types. This example monitors layer changes and battery level:
use rmk_macro::controller;
use rmk::event::{LayerChangeEvent, BatteryLevelEvent};
// Subscribe to multiple events
#[controller(subscribe = [LayerChangeEvent, BatteryLevelEvent])]
pub struct StatusController {
current_layer: u8,
battery_level: u8,
}
impl StatusController {
pub fn new() -> Self {
Self {
current_layer: 0,
battery_level: 100,
}
}
// Handler for LayerChangeEvent
async fn on_layer_change_event(&mut self, event: LayerChangeEvent) {
self.current_layer = event.layer;
info!("Layer: {}", event.layer);
}
// Handler for BatteryLevelEvent
async fn on_battery_level_event(&mut self, event: BatteryLevelEvent) {
self.battery_level = event.level;
info!("Battery: {}%", event.level);
}
}
Register with #[register_controller(event)]:
#[rmk_keyboard]
mod keyboard {
#[register_controller(event)]
fn status_controller() -> StatusController {
StatusController::new()
}
}
Polling Controller
Blinking LED when layer 0 is activated, using poll_interval parameter:
use rmk_macro::controller;
use rmk::event::LayerChangeEvent;
use embedded_hal::digital::StatefulOutputPin;
#[controller(subscribe = [LayerChangeEvent], poll_interval = 500)]
pub struct BlinkingController<P: StatefulOutputPin> {
pin: P,
active: bool,
}
impl<P: StatefulOutputPin> BlinkingController<P> {
pub fn new(pin: P) -> Self {
Self { pin, active: true }
}
async fn on_layer_change_event(&mut self, event: LayerChangeEvent) {
self.active = event.layer == 0;
if !self.active {
let _ = self.pin.set_low();
}
}
// Called every 500ms automatically
async fn poll(&mut self) {
if self.active {
let _ = self.pin.toggle();
}
}
}
Register with #[register_controller(poll)]:
#[rmk_keyboard]
mod keyboard {
#[register_controller(poll)]
fn blinking_led() -> BlinkingController {
let pin = Output::new(p.PIN_5, Level::Low, OutputDrive::Standard);
BlinkingController::new(pin)
}
}
Split Keyboard Controller
CapsLock LED on peripheral (events auto-sync from central):
use rmk_macro::controller;
use rmk::event::LedIndicatorEvent;
use embassy_nrf::gpio::Output;
#[controller(subscribe = [LedIndicatorEvent])]
pub struct CapsLockController {
led: Output<'static>,
caps_lock_on: bool,
}
impl CapsLockController {
pub fn new(led: Output<'static>) -> Self {
Self { led, caps_lock_on: false }
}
async fn on_led_indicator_event(&mut self, event: LedIndicatorEvent) {
let new_state = event.indicator.caps_lock();
if new_state != self.caps_lock_on {
self.caps_lock_on = new_state;
if new_state {
self.led.set_high();
} else {
self.led.set_low();
}
}
}
}
#[rmk_peripheral(id = 0)]
mod keyboard_peripheral {
#[register_controller(event)]
fn capslock_led() -> CapsLockController {
let led = Output::new(p.PIN_4, Level::Low, OutputDrive::Standard);
CapsLockController::new(led)
}
}
Custom Events
In addition to built-in controller events, you can define custom event types using the #[controller_event] macro. Custom events work seamlessly alongside built-in events and follow the same usage patterns.
Defining Custom Events
Use the #[controller_event] macro to define custom events:
use rmk_macro::controller_event;
#[controller_event(channel_size = 8, subs = 2)]
#[derive(Clone, Copy, Debug)]
pub struct BacklightEvent {
pub brightness: u8,
}
Macro parameters:
channel_size (optional): Buffer size for PubSubChannel. Default is 1
subs (optional): Maximum number of subscribers. Default is 4
pubs (optional): Maximum number of async publishers. Default is 1
The #[controller_event] macro uses PubSubChannel for all events, which buffers events with configurable capacity and supports both immediate (non-blocking) and async (awaitable) publishing.
Choosing buffer size:
- Use
channel_size = 1(which is the default value) for state-like events (layer, battery level, connection state) where only the latest value matters
- Use larger buffer sizes (e.g.,
channel_size = 8) for event streams that need history (key events, input events)
Publishing Custom Events
Events can be published from anywhere in your code:
Non-blocking publish:
use rmk::event::publish_controller_event;
publish_controller_event(BacklightEvent { brightness: 50 });
This publishes immediately and drops the event if the buffer is full.
Async publish:
use rmk::event::publish_controller_event_async;
publish_controller_event_async(BacklightEvent { brightness: 75 }).await;
This waits if the buffer is full (backpressure).
WARNING
When using publish_controller_event_async(), ensure at least one subscriber exists to avoid infinite blocking.
Complete Example
Here's a complete example showing how to define a custom event, create a controller for it, and publish it:
use rmk_macro::{controller_event, controller};
use rmk::event::publish_controller_event;
// 1. Define custom event
#[controller_event(channel_size = 8, subs = 2)]
#[derive(Clone, Copy, Debug)]
pub struct DisplayUpdateEvent {
pub line: u8,
pub text: [u8; 16],
}
// 2. Create controller that subscribes to it
#[controller(subscribe = [DisplayUpdateEvent])]
pub struct DisplayController {
// ... display hardware fields
}
impl DisplayController {
pub fn new() -> Self {
Self { /* ... */ }
}
async fn on_display_update_event(&mut self, event: DisplayUpdateEvent) {
// Update display with event.line and event.text
}
}
// 3. Register the controller
#[rmk_keyboard]
mod keyboard {
#[register_controller(event)]
fn display_controller() -> DisplayController {
DisplayController::new()
}
}
// 4. Publish from anywhere in your code
publish_controller_event(DisplayUpdateEvent {
line: 0,
text: *b"Hello RMK! ",
});