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. Controllers are software modules that implement the Controller trait and receive events from the keyboard through RMK's event system.

Overview

What are Controllers?

Controllers in RMK are software modules that manage external hardware components and respond to keyboard events. They provide:

  • Event-driven architecture that reacts to keyboard state changes
  • Two execution modes: event-driven and polling-based
  • Built-in TOML configuration for LED indicators (NumLock, CapsLock, ScrollLock)
  • Custom controller support through the #[controller] attribute

Built-in Controllers

RMK includes several built-in controllers:

LED Indicator Controllers:

  • NumLock, CapsLock, ScrollLock LED indicators
  • Automatic configuration through keyboard.toml
  • Support for active-high and active-low pins

Battery LED Controller:

  • Battery level indication with different states
  • Charging state visualization
  • Configurable blinking patterns

Controller Architecture

Controller Trait

All controllers must implement the Controller trait:

pub trait Controller {
    /// Type of the received events
    type Event;

    /// Process the received event
    async fn process_event(&mut self, event: Self::Event);

    /// Block waiting for next message
    async fn next_message(&mut self) -> Self::Event;
}

Execution Modes

Controllers can operate in two modes:

Event-Driven Controllers: Controllers that react only to events implement EventController (auto-implemented for all Controllers):

impl Controller for MyEventController {
    type Event = ControllerEvent;

    async fn process_event(&mut self, event: Self::Event) {
        match event {
            ControllerEvent::KeyboardIndicator(state) => {
                // Handle LED indicator changes
            },
            ControllerEvent::Battery(level) => {
                // Handle battery level changes
            },
            _ => {}
        }
    }

    async fn next_message(&mut self) -> Self::Event {
        self.sub.next_message_pure().await
    }
}

Polling Controllers: Controllers that need periodic updates implement PollingController:

impl PollingController for MyPollingController {
    const INTERVAL: embassy_time::Duration = embassy_time::Duration::from_millis(100);

    async fn update(&mut self) {
        // Periodic update logic (e.g., LED animations, sensor readings)
    }
}

Using Controllers

TOML Configuration (Built-in Controllers)

Built-in LED indicators can be configured in keyboard.toml:

[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

Custom Controllers

Custom controllers are declared using the #[controller(event)] or #[controller(poll)] attribute within your keyboard module. If #[controller(event)] is used the controller must implement EventController (or just Controller) and the EventController::event_loop method will be called. If #[controller(poll)] is used the controller must implement PollingController and the PollingController::polling_loop method will be called.

A p variable containing the chip peripherals is in scope inside the function. It's also possible to define extra interrupts using the bind_interrupts! macro.

#[rmk_keyboard]
mod keyboard {
    // ... keyboard configuration ...

    #[controller(event)]
    fn my_custom_controller() -> MyCustomController {
        // Initialize your controller
        let pin = Output::new(p.PIN_4, Level::Low, OutputDrive::Standard);
        MyCustomController::new(pin)
    }
}

Controller Events

Controllers receive events from RMK through the ControllerEvent enum, which includes:

Available Events

pub enum ControllerEvent {
    /// Key event with the associated action
    Key(KeyboardEvent, KeyAction),
    /// Battery percentage (0-100)
    Battery(u8),
    /// Charging state (true = charging, false = not charging)
    ChargingState(bool),
    /// Active layer changed
    Layer(u8),
    /// Modifier combination changed
    Modifier(ModifierCombination),
    /// Words per minute typing speed
    Wpm(u16),
    /// Connection type (USB = 0, BLE = 1)
    ConnectionType(u8),
    /// LED indicator states (NumLock, CapsLock, ScrollLock, etc.)
    KeyboardIndicator(LedIndicator),
    /// Sleep state changed
    Sleep(bool),
    // ... and more
}

Event Subscription

Controllers automatically receive events through the CONTROLLER_CHANNEL:

use crate::channel::{CONTROLLER_CHANNEL, ControllerSub};

pub struct MyController {
    sub: ControllerSub,
    // ... other fields
}

impl MyController {
    pub fn new() -> Self {
        Self {
            sub: unwrap!(CONTROLLER_CHANNEL.subscriber()),
            // ... initialize other fields
        }
    }
}

Creating Custom Controllers

Basic Controller Implementation

Here's a complete example of a custom LED controller:

use embedded_hal::digital::StatefulOutputPin;
use crate::channel::{CONTROLLER_CHANNEL, ControllerSub};
use crate::controller::Controller;
use crate::event::ControllerEvent;

pub struct CustomLedController<P: StatefulOutputPin> {
    pin: P,
    sub: ControllerSub,
    state: bool,
}

impl<P: StatefulOutputPin> CustomLedController<P> {
    pub fn new(pin: P) -> Self {
        Self {
            pin,
            sub: unwrap!(CONTROLLER_CHANNEL.subscriber()),
            state: false,
        }
    }
}

impl<P: StatefulOutputPin> Controller for CustomLedController<P> {
    type Event = ControllerEvent;

    async fn process_event(&mut self, event: Self::Event) {
        match event {
            ControllerEvent::Layer(layer) => {
                // Toggle LED based on layer
                if layer > 0 && !self.state {
                    let _ = self.pin.set_high();
                    self.state = true;
                } else if layer == 0 && self.state {
                    let _ = self.pin.set_low();
                    self.state = false;
                }
            }
            _ => {}
        }
    }

    async fn next_message(&mut self) -> Self::Event {
        self.sub.next_message_pure().await
    }
}

Polling Controller Example

For controllers that need periodic updates (like animations):

use crate::controller::PollingController;

impl<P: StatefulOutputPin> PollingController for BlinkingController<P> {
    const INTERVAL: embassy_time::Duration = embassy_time::Duration::from_millis(500);

    async fn update(&mut self) {
        // Toggle LED every 500ms when active
        if self.active {
            self.state = !self.state;
            if self.state {
                let _ = self.pin.set_high();
            } else {
                let _ = self.pin.set_low();
            }
        }
    }
}

Advanced Usage

Battery State Controller

RMK includes a built-in BatteryLedController that demonstrates both event handling and polling:

// Events set the state
ControllerEvent::Battery(level) => {
    if level < 10 {
        self.state = BatteryState::Low;
    } else {
        self.state = BatteryState::Normal;
    }
}

// Polling updates the LED based on state
async fn update(&mut self) {
    match self.state {
        BatteryState::Low => self.pin.toggle(),      // Blink for low battery
        BatteryState::Normal => self.pin.deactivate(), // Off for normal
        BatteryState::Charging => self.pin.activate(),  // On when charging
    }
}

Multiple Controllers

You can define multiple controllers in your keyboard module:

#[rmk_keyboard]
mod keyboard {
    #[controller(event)]
    fn status_led() -> StatusLedController {
        StatusLedController::new(p.PIN_1)
    }

    #[controller(event)]
    fn layer_indicator() -> LayerLedController {
        LayerLedController::new(p.PIN_2)
    }

    #[controller(poll)]
    fn battery_monitor() -> BatteryController {
        BatteryController::new(p.PIN_3)
    }
}

Split Keyboard

For split keyboard controller usage, see the Split Keyboard Controllers section.