Skip to content

Commit

Permalink
Merge pull request #192 from HaoboGu/feat/input_device
Browse files Browse the repository at this point in the history
Add basic POC input device support and rotary encoder implementation as an example
  • Loading branch information
HaoboGu authored Jan 3, 2025
2 parents aacdf02 + e01373e commit acfd3db
Show file tree
Hide file tree
Showing 28 changed files with 993 additions and 93 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
- **Real-time keymap editing**: RMK has built-in [Vial](https://get.vial.today) support, the keymap can be changed on-the-fly. You can even use Vial to edit keymap over BLE directly
- **Advanced keyboard features**: Many advanced keyboard features are available by default in RMK, such as layer switch, media control, system control, mouse control, etc
- **Wireless**: BLE wireless support with auto-reconnection/multiple devices feature for nRF52 and esp32 microcontrollers, tested on nRF52840, esp32c3 and esp32s3
- **Easy configuration**: RMK provides a simple way to build your keyboard: a `keyboard.toml` is all you need! For experienced Rust user, you can still customize your firmware easily using RMK
- **Easy configuration**: RMK provides a simple way to build your keyboard: a `keyboard.toml` is all you need! For experienced Rust user, you can still customize your firmware easily using Rust code
- **Low latency and low-power ready**: RMK has a typical 2 ms latency in wired mode and 10 ms latency in wireless mode. By enabling `async_matrix` feature, RMK has very low power consumption, with a 2000mah battery, RMK can provide several months battery life

## [User Documentation](https://haobogu.github.io/rmk/user_guide/1_guide_overview.html) | [API Reference](https://docs.rs/rmk/latest/rmk/) | [FAQs](https://haobogu.github.io/rmk/faq.html) | [Changelog](https://github.com/HaoboGu/rmk/blob/main/rmk/CHANGELOG.md)
Expand Down
4 changes: 2 additions & 2 deletions docs/po/zh_CN.po
Original file line number Diff line number Diff line change
Expand Up @@ -2531,15 +2531,15 @@ msgstr ""

#: src\how_to_contribute.md:41
msgid ""
"`key_event_channel`: a multi-sender, single-receiver channel. The sender can "
"`KEY_EVENT_CHANNEL`: a multi-sender, single-receiver channel. The sender can "
"be a matrix task which scans the key matrix or a split peripheral monitor "
"which receives key event from split peripheral. The receiver, i.e. keyboard "
"task, receives the key event and processes the key"
msgstr ""

#: src\how_to_contribute.md:42
msgid ""
"`keyboard_report_channel`: a single-sender, single-receiver channel, "
"`KEYBOARD_REPORT_CHANNEL`: a single-sender, single-receiver channel, "
"keyboard task sends keyboard report to channel after the key event is "
"processed, and USB/BLE task receives the keyboard report and sends the key "
"to the host."
Expand Down
129 changes: 129 additions & 0 deletions docs/src/design_doc/input_device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Input devices

The definition of input devices varies, but for RMK, we focus on two categories: keys and sensors.

- Keys are straightforward—they are essentially switches with two states.
- Sensors, on the other hand, are more complex. Devices such as joysticks, mice, trackpads, and trackballs are all sensors. They produce different data and speak different protocols.

RMK’s input device framework is designed to provide a simple yet extensible way to handle both keys and sensors. Below is an overview of the framework:

![input_device_framework](../images/input_device_framework.svg)

At first glance, the framework looks simple. However, as we delve into the implementation, intricacies emerge. In this documentation, we will focus on the left half of the diagram: `InputDevices` and `InputProcessors`. Key areas of complexity include:

This design document outlines the current implementation, which aims to make RMK’s input device system clear, easy to use, and extensible.

## Input device trait

The input devices can be key matrix or sensors, which read the physical devices, send raw events to the input processors. All input devices in RMK should implement the `InputDevice` trait:

```rust
pub trait InputDevice {
/// Event type that input device will send
type EventType;

/// Starts the input device task.
///
/// This asynchronous method should contain the main logic for the input device.
/// It will be executed concurrently with other input devices using the `run_devices` macro.
fn run(&mut self) -> impl Future<Output = ()>;

/// Get the event channel for the input device. All events should be send by this channel.
fn get_channel(&self) -> &Channel<CriticalSectionRawMutex, Self::EventType, EVENT_CHANNEL_SIZE>;
}
```

This trait should be used with the `run_devices!` macro:

```rust
// Suppose that the d1 & d2 both implement `InputDevice`. `run()` will be called in `run_devices!`
run_devices!(d1, d2).await;
```

> Why `run_devices!`?
>
> Currently, embassy-rs does not support generic tasks. So the only option is to join all tasks(aka `run` function in `InputDevice`) together. That's what `run_devices!` does.

Another thing that need to be mentioned is that every input device defines its own `EventType`. RMK provides a default Event enum, which is compatible with built-in InputProcessors. This design balances convenience and flexibility:

- For common devices, developers can use the built-in `Event` and `InputProcessor` implementations to when implementing new `InputDevice`
- For advanced use cases, developers can define custom events and processors to fully control the input logic.

## Event

The event is the output of input devices. It's defined as a non-exhaustive enum:

```rust
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum Event {
/// Keyboard event
Key(KeyEvent),
/// Rotary encoder, ec11 compatible models
RotaryEncoder(RotaryEncoderEvent),
/// Multi-touch touchpad
Touchpad(TouchpadEvent),
/// Joystick, suppose we have x,y,z axes for this joystick
Joystick([AxisEvent; 3]),
/// An AxisEvent in a stream of events. The receiver should keep receiving events until it receives [`Eos`] event.
AxisEventStream(AxisEvent),
/// End of the event sequence
///
/// This is used with [`AxisEventStream`] to indicate the end of the event sequence.
Eos,
}
```

The `Event` aims to include raw outputs of all commonly used input devices, such as mice, trackpads, joysticks, etc. It also provides a stream-like axis event representation `AxisEventStream`, which can be used for a multiple-axis device where the number of axes is not known. Note that when using this, the `Eos` should be sent the indicate the end of the event sequence, otherwise the `InputProcessor` would wait for the next event forever.

## Input processor trait

The input processors receive the event from input devices, process them and convert results to HID reports for USB/BLE transmission. All input processors should implement the `InputProcessor` trait:

```rust
pub trait InputProcessor {
/// Event type that the input processor receives.
type EventType;

/// Process the incoming events, convert them to HID report [`KeyboardReportMessage`],
/// then send the report to the USB/BLE.
///
/// Note there might be mulitple HID reports are generated for one event,
/// so the "sending report" operation should be done in the `process` method.
/// The input processor implementor should be aware of this.
fn process(&mut self, event: Self::EventType) -> impl Future<Output = ()>;

/// Get the input event channel for the input processor.
///
/// The input processor receives events from this channel, processes the event,
/// then sends to the report channel.
fn get_event_channel(
&self,
) -> &Channel<CriticalSectionRawMutex, Self::EventType, EVENT_CHANNEL_SIZE>;

/// Get the output report channel for the input processor.
///
/// The input processor sends keyboard reports to this channel.
fn get_report_channel(
&self,
) -> &Channel<CriticalSectionRawMutex, KeyboardReportMessage, REPORT_CHANNEL_SIZE> {
&KEYBOARD_REPORT_CHANNEL
}

/// Default implementation of the input processor. It wait for a new event from the event channel,
/// then process the event.
///
/// The report is sent to the USB/BLE in the `process` method.
fn run(&mut self) -> impl Future<Output = ()> {
async {
loop {
let event = self.get_event_channel().receive().await;
self.process(event).await;
}
}
}
}
```

The `process` method is responsible for processing input events and sending HID report to the report channel, which is available by `get_report_channel`.
4 changes: 2 additions & 2 deletions docs/src/how_to_contribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ So, if you want to contribute new features of RMK, just look into `rmk` core cra
Generally, there are 4-5 running tasks in the meanwhile, according to the user's config. Communication between tasks is done by channels.There are several built-in channels:

- `FLASH_CHANNEL`: a multi-sender, single-receiver channel. There are many tasks send the `FlashOperationMessage`, such as BLE task(which saves bond info), vial task(which saves key), etc.
- `key_event_channel`: a multi-sender, single-receiver channel. The sender can be a matrix task which scans the key matrix or a split peripheral monitor which receives key event from split peripheral. The receiver, i.e. keyboard task, receives the key event and processes the key
- `keyboard_report_channel`: a single-sender, single-receiver channel, keyboard task sends keyboard report to channel after the key event is processed, and USB/BLE task receives the keyboard report and sends the key to the host.
- `KEY_EVENT_CHANNEL`: a multi-sender, single-receiver channel. The sender can be a matrix task which scans the key matrix or a split peripheral monitor which receives key event from split peripheral. The receiver, i.e. keyboard task, receives the key event and processes the key
- `KEYBOARD_REPORT_CHANNEL`: a single-sender, single-receiver channel, keyboard task sends keyboard report to channel after the key event is processed, and USB/BLE task receives the keyboard report and sends the key to the host.

### Matrix scanning & key processing

Expand Down
88 changes: 88 additions & 0 deletions docs/src/images/input_device_framework.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<mxfile host="65bd71144e">
<diagram id="KHJxcRW501o7JvBhlY4s" name="第 1 页">
<mxGraphModel dx="1137" dy="862" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="6" value="" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="80" y="220" width="180" height="450" as="geometry"/>
</mxCell>
<mxCell id="2" value="key matrix" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="110" y="260" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="encoders" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="110" y="360" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="4" value="mice" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="110" y="460" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="5" value="trackpads" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="110" y="560" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="7" value="Input devices" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" vertex="1">
<mxGeometry x="105" y="180" width="130" height="30" as="geometry"/>
</mxCell>
<mxCell id="9" value="" style="shape=flexArrow;endArrow=classic;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="6" target="10" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="580" y="510" as="sourcePoint"/>
<mxPoint x="440" y="445" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="10" value="" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="440" y="220" width="180" height="450" as="geometry"/>
</mxCell>
<mxCell id="11" value="key processor" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="470" y="260" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="12" value="encoder processor" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="470" y="360" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="13" value="mice processor" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="470" y="460" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="14" value="axis processor" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="470" y="560" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="15" value="Input processor" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" vertex="1">
<mxGeometry x="465" y="180" width="130" height="30" as="geometry"/>
</mxCell>
<mxCell id="16" value="Event" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" vertex="1">
<mxGeometry x="320" y="410" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="17" value="" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="770" y="345" width="180" height="200" as="geometry"/>
</mxCell>
<mxCell id="20" value="USB" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="800" y="370" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="21" value="BLE" style="rounded=1;whiteSpace=wrap;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;shadow=0;" parent="1" vertex="1">
<mxGeometry x="800" y="460" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="22" value="Reporter" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" vertex="1">
<mxGeometry x="795" y="300" width="130" height="30" as="geometry"/>
</mxCell>
<mxCell id="23" value="" style="shape=flexArrow;endArrow=classic;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="620" y="444.5" as="sourcePoint"/>
<mxPoint x="770" y="445" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="" style="shape=flexArrow;endArrow=classic;html=1;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" target="28" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="960" y="445" as="sourcePoint"/>
<mxPoint x="1060" y="445" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="27" value="host" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" vertex="1">
<mxGeometry x="1065" y="370" width="130" height="30" as="geometry"/>
</mxCell>
<mxCell id="28" value="" style="fontColor=#0066CC;verticalAlign=top;verticalLabelPosition=bottom;labelPosition=center;align=center;html=1;outlineConnect=0;fillColor=#CCCCCC;strokeColor=#6881B3;gradientColor=none;gradientDirection=north;strokeWidth=2;shape=mxgraph.networks.laptop;rounded=1;shadow=0;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" vertex="1">
<mxGeometry x="1080" y="417.5" width="100" height="55" as="geometry"/>
</mxCell>
<mxCell id="29" value="Report" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;sketch=1;hachureGap=4;jiggle=2;curveFitting=1;fontFamily=Verdana;fontSize=16;" parent="1" vertex="1">
<mxGeometry x="660" y="410" width="60" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
1 change: 1 addition & 0 deletions docs/src/images/input_device_framework.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions examples/use_rust/nrf52840_ble/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ embassy-executor = { version = "0.6", features = [
"executor-thread",
"integrated-timers",
] }
embassy-futures = { version = "0.1", features = ["defmt"] }

defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
Expand Down
Loading

0 comments on commit acfd3db

Please sign in to comment.