Input Device Support
RMK's input device system provides a unified interface for managing input devices like rotary encoders, mouse sensors, joysticks, and other peripherals that generate input events.
Overview
Input devices in RMK are hardware peripherals that generate input events. They provide:
- Event-driven architecture for efficient input handling
- Processor chain for flexible event transformation
- Custom device/processor support through attributes
- Automatic split keyboard support
Architecture
Event Flow
Input Devices → EVENT_CHANNEL → Processor Chain → HID Reports
Key Point: Devices and processors are completely decoupled through EVENT_CHANNEL. Devices generate events, processors transform them into HID reports.
Core Traits
InputDevice - Generates events:
pub trait InputDevice {
async fn read_event(&mut self) -> Event;
}
InputProcessor - Transforms events:
pub trait InputProcessor {
async fn process(&mut self, event: Event) -> ProcessResult;
fn get_keymap(&self) -> &RefCell<KeyMap>;
}
ProcessResult - Controls processor chain:
Continue(Event) - Pass to next processor
Stop - Event fully handled
Built-in Devices
Rotary Encoder
[[input_device.encoder]]
pin_a = "P0_29"
pin_b = "P0_28"
resolution = 4
PMW3610 Mouse Sensor
[[input_device.pmw3610]]
name = "trackpoint"
spi.sck = "P0_12"
spi.mosi = "P0_13"
spi.miso = "P0_14"
cpi = 800
Joystick
[[input_device.joystick]]
name = "thumbstick"
pin_x = "P0_04"
pin_y = "P0_05"
transform = [[1, 0], [0, 1]]
bias = [0, 0]
resolution = 16
See configuration reference for all options.
Custom Input Devices
Use #[device] to create custom devices. They have access to peripheral resources via p:
#[rmk_keyboard]
mod keyboard {
#[device]
fn trackball() -> Trackball {
// Access I2C bus for trackball sensor via `p`
Trackball::new(p.I2C0, p.P0_26, p.P0_27)
}
}
// Pimoroni Trackball device
// I2C-based trackball with RGBW LEDs
pub struct Trackball {
i2c: /* I2C type */,
interrupt_pin: /* GPIO type */,
}
impl InputDevice for Trackball {
async fn read_event(&mut self) -> Event {
// Read X/Y movement and button state from I2C
let (x, y, button) = self.read_i2c_registers().await;
let mut data = [0u8; 16];
data[0] = x;
data[1] = y;
data[2] = button;
Event::Custom(data)
}
}
Key Points:
- Access
p for peripherals (GPIO, SPI, I2C, ADC, etc.)
- No TOML config needed - auto-discovered
- Implement
InputDevice trait
Custom Processors
Use #[processor] to create custom processors. They have access to keymap:
#[rmk_keyboard]
mod keyboard {
#[processor]
fn scroll_processor() -> ScrollWheelProcessor {
ScrollWheelProcessor::new(&keymap)
}
}
// Scroll wheel processor for trackball
// Converts Y-axis movement to scroll events when modifier key is held
pub struct ScrollWheelProcessor<'a, ...> {
keymap: &'a RefCell<KeyMap<...>>,
scroll_mode: bool,
}
impl InputProcessor for ScrollWheelProcessor {
async fn process(&mut self, event: Event) -> ProcessResult {
match event {
Event::Custom(data) => {
let y_movement = data[1] as i8;
// Check if scroll modifier (Fn key) is held
let keymap = self.keymap.borrow();
self.scroll_mode = keymap.is_key_pressed(SCROLL_MODIFIER_KEY);
if self.scroll_mode && y_movement != 0 {
// Convert Y movement to scroll wheel
self.send_scroll_report(y_movement).await;
ProcessResult::Stop
} else {
// Pass through as normal mouse movement
ProcessResult::Continue(event)
}
}
_ => ProcessResult::Continue(event),
}
}
fn get_keymap(&self) -> &RefCell<KeyMap> {
self.keymap
}
}
Key Points:
- Access
keymap for layer/key state
- Return
Stop when fully handled
- Return
Continue(event) to pass to next
Processor Chain
Control processor execution order in TOML:
[input_device]
processor_chain = [
"pmw3610_trackpoint_processor",
"scroll_processor",
"battery_processor",
]
Processor names:
- Built-in:
{type}_{name}_processor (e.g., pmw3610_trackpoint_processor)
- Custom: Function name from
#[processor]
Default order (if not specified):
- Built-in processors (TOML order)
- Custom processors (code order)
Split Keyboard Support
Configure devices per board:
[[split.central.input_device.encoder]]
pin_a = "P0_29"
pin_b = "P0_28"
[[split.peripheral.input_device.pmw3610]]
name = "peripheral_mouse"
# ... config ...
Automatic behavior:
- Device runs where configured
- Processor runs on central
- Events auto-routed from peripheral to central
Best Practices
Devices
- Use async operations (await events, don't busy-loop)
- Use
Event::Custom for non-standard events
- Handle errors gracefully
Processors
- Return
Stop when fully handled
- Return
Continue(event) otherwise
- Avoid blocking operations
- Use
send_report() for HID reports
Processor Order
- Put specialized processors first (filter early)
- Put fallback processors last
- Test your processor order
Complete Example
[[input_device.encoder]]
pin_a = "P0_29"
pin_b = "P0_28"
[[input_device.pmw3610]]
name = "trackpoint"
spi.sck = "P0_12"
spi.mosi = "P0_13"
spi.miso = "P0_14"
[input_device]
processor_chain = [
"pmw3610_trackpoint_processor",
"scroll_processor",
]
#[rmk_keyboard]
mod keyboard {
#[device]
fn trackball() -> Trackball {
Trackball::new(p.I2C0, p.P0_26, p.P0_27)
}
#[processor]
fn scroll_processor() -> ScrollWheelProcessor {
ScrollWheelProcessor::new(&keymap)
}
}
See Also