Processor
A processor reacts to keyboard events or timers. Use one to update an LED, refresh a display, or handle an inactivity timeout. Define its behavior with #[processor], then add an instance to your keyboard's tasks.
Before you begin
Start from a working RMK firmware project. The examples below show the modules and task-list changes to add to that project.
- For each new event subscriber, increase
[event.<name>].subsfrom its default by one. RMK reserves slots for feature-gated built-in tasks separately. See event configuration. - The timer examples need
embassy-time. The GPIO example also needsembedded-hal. Add the dependencies your chosen example uses:
Choose the trigger that fits your task:
You can combine the options. A processor with no event subscriptions must have a timer option or a custom run loop.
Create an event processor
Add #[processor] to a struct and list the events it should receive. For each event, provide an async handler named on_<event_name>_event.
This processor keeps track of the active layer:
The macro implements the processor traits and Runnable. Defining the type does not start it; add an instance to your keyboard's tasks as described below.
Run the processor
Choose the setup that matches your project.
With keyboard.toml
- Save the processor above as
src/layer_tracker.rs. - Declare the module in
src/main.rs. - Add its constructor to your existing
#[rmk_keyboard]module:
For a split keyboard, register it in the central's #[rmk_central] module instead.
The constructor runs after chip initialization and can use the peripherals in p. Choose pins that keyboard.toml does not already use. To enable a processor conditionally, add #[cfg(...)] to its registration function; the condition applies to both construction and execution.
With the Rust API
Save the processor above as src/layer_tracker.rs, then declare the module and import the type in src/main.rs:
After initializing your keyboard and transport, add the instance to your existing task list. In this example, keyboard and transport are the instances your firmware already runs:
Both approaches run the type's Runnable implementation. Select event and timer behavior on the type's #[processor] attribute.
Run periodic work
Use poll_interval for work that repeats at a fixed interval. The value is in milliseconds and must be greater than zero. Provide an async poll() method.
A processor that only polls does not need a subscribe list. This example toggles a GPIO every 500 ms:
Construct the instance with Blinker::new(output_pin), then use the same registration or task-list steps. Initialize output_pin with your board's HAL and choose a pin unused by the matrix or other devices.
To also react to events, add subscribe = [...] and the corresponding event handlers. Events do not restart the polling interval.
Handle inactivity
Use deadline for a timeout that can be reset or cancelled. Implement DeadlineProcessor with these methods:
fn deadline(&self) -> Option<Instant>returns the next timeout, orNonewhen no timeout is armed.async fn on_deadline(&mut self)handles the timeout and clears or advances it.
This example marks pointing activity as inactive 500 ms after the last PointingEvent:
Save this module as src/motion_activity.rs. Construct it with MotionActivity::default() and run it using the same registration or task-list steps.
An event handler can reset the timeout by storing a later deadline, or cancel it by setting the deadline to None. The task checks the deadline again after each callback. A deadline-only processor can omit subscribe; its initial state must arm the first timeout.
Combine polling and deadlines
Add poll_interval alongside deadline when the processor also needs periodic work:
Provide poll() in the processor's inherent implementation. Keep deadline() and on_deadline() in impl DeadlineProcessor. Either polling or an event handler can arm a timeout.
Callbacks run one at a time and finish before another callback starts. When several sources are ready, RMK handles deadlines first, polling ticks second, and events third. Keep callbacks short when timer responsiveness matters. Clear or advance a deadline in on_deadline() to avoid firing repeatedly for the same expired timeout.
Handle multiple event types
List each event in subscribe and provide a handler for each one:
Event names use snake case: LayerChangeEvent maps to on_layer_change_event, and LedIndicatorEvent maps to on_led_indicator_event. Ensure each event has enough subscriber slots; see event configuration.
Attribute reference
You can combine these options. Omit subscribe or set it to [] when no events are needed. At least one event or timer source is required unless you provide a custom Runnable.
Custom Runnable
To provide your own run loop, implement rmk::core_traits::Runnable and place #[rmk::macros::runnable_generated] below #[processor]. The marker keeps your run loop while generating the event-handling and polling traits selected by #[processor]. A bare #[processor] is valid with this marker. Run the instance using the same registration or task-list steps.
Combining deadline with #[input_device] requires a custom run loop. Choose how that loop schedules input reads, event handling, and timeouts.
Related documentation
- Events: built-in events and custom event types.
- Input devices: publish events from hardware.
- Event configuration: reserve channel capacity and subscriber slots.
- Upgrade from v0.9 to v0.10: migrate existing processor registrations and trait implementations.