Merge pull request #1050 from gedankenexperimenter/plugin-authors-guide
commit
66b16e3657
@ -0,0 +1,261 @@
|
|||||||
|
# How to write a Kaleidoscope plugin
|
||||||
|
|
||||||
|
This is a brief guide intended for those who want to write custom Kaleidoscope plugins. It covers basic things you'll need to know about how Kaleidoscope calls plugin event handlers, and how it will respond to actions taken by those plugins.
|
||||||
|
|
||||||
|
## What can a plugin do?
|
||||||
|
|
||||||
|
There are many things that Kaleidoscope plugins are capable of, from LED effects, serial communication with the host, altering HID reports, and interacting with other plugins. It's useful to break these capabilities down into some broad categories, based on the types of input a plugin can respond to.
|
||||||
|
|
||||||
|
- Key events (key switches toggling on and off)
|
||||||
|
- Focus commands (sent to the keyboard from software on the host via the serial port)
|
||||||
|
- LED updates
|
||||||
|
- Keymap layer changes
|
||||||
|
- Timers
|
||||||
|
|
||||||
|
## An example plugin
|
||||||
|
|
||||||
|
To make a Kaleidoscope plugin, we create a subclass of the `kaleidoscope::Plugin` class, usually in the `kaleidoscope::plugin` namespace:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
namespace kaleidoscope {
|
||||||
|
namespace plugin {
|
||||||
|
|
||||||
|
class MyPlugin : public Plugin {};
|
||||||
|
|
||||||
|
} // namespace kaleidoscope
|
||||||
|
} // namespace plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
This code can be placed in a separate C++ source file, but it's simplest to just define it right in the sketch's \*.ino file for now.
|
||||||
|
|
||||||
|
By convention, we create a singleton object named like the plugin's class in the global namespace. This is typical of Arduino code.
|
||||||
|
|
||||||
|
```c++
|
||||||
|
kaleidoscope::plugin::MyPlugin MyPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, in order to connect that plugin to the Kaleidoscope event handler system, we need to register it in the call to the preprocessor macro `KALEIDOSCOPE_INIT_PLUGINS()` in the sketch:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
KALEIDOSCOPE_INIT_PLUGINS(MyPlugin, OtherPlugin);
|
||||||
|
```
|
||||||
|
|
||||||
|
To make our plugin do anything useful, we need to add [[event-handler-hooks]] to it. This is how Kaleidoscope delivers input events to its registered plugins. Here's an example:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
class MyPlugin : public Plugin {
|
||||||
|
public:
|
||||||
|
EventHanderResult onKeyEvent(KeyEvent &event);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This will result in `MyPlugin.onKeyEvent()` being called (along with other plugins' `onKeyEvent()` methods) when Kaleidoscope detects a key state change. This function returns one of three `EventHandlerResult` values:
|
||||||
|
|
||||||
|
- `EventHandlerResult::OK` indicates that Kaleidoscope should proceed on to the event handler for the next plugin in the chain.
|
||||||
|
- `EventHandlerResult::ABORT` indicates that Kaleidoscope should stop processing immediately, and treat the event as if it didn't happen.
|
||||||
|
- `EventHandlerResult::EVENT_CONSUMED` stops event processing like `ABORT`, but records that the key is being held.
|
||||||
|
|
||||||
|
The `onKeyEvent()` method takes one argument: a reference to a `KeyEvent` object, which is a simple container for these essential bits of information:
|
||||||
|
|
||||||
|
- `event.addr` — the physical location of the keyswitch, if any
|
||||||
|
- `event.state` — a bitfield containing information on the current and previous state of the keyswitch (from which we can find out if it just toggled on or toggled off)
|
||||||
|
- `event.key` — a 16-bit `Key` value containing the contents looked up from the sketch's current keymap (if the key just toggled on) or the current live value of the key (if the key just toggled off)
|
||||||
|
|
||||||
|
Because the `KeyEvent` parameter is passed by (mutable) reference, our plugin's `onKeyEvent()` method can alter the components of the event, causing subsequent plugins (and, eventually, Kaleidoscope itself) to treat it as if it was a different event. In practice, except in very rare cases, the only member of a `KeyEvent` that a plugin should alter is `event.key`. Here's a very simple `onKeyEvent()` handler that changes all `X` keys into `Y` keys:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
EventHandlerResult onKeyEvent(KeyEvent &event) {
|
||||||
|
if (event.key == Key_X)
|
||||||
|
event.key = Key_Y;
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The difference between `ABORT` & `EVENT_CONSUMED`
|
||||||
|
|
||||||
|
Here's a plugin that will suppress all `X` key events:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
EventHandlerResult onKeyEvent(KeyEvent &event) {
|
||||||
|
if (event.key == Key_X)
|
||||||
|
return EventHandlerResult::ABORT;
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's an almost identical plugin that has an odd failure mode:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
EventHandlerResult onKeyEvent(KeyEvent &event) {
|
||||||
|
if (event.key == Key_X)
|
||||||
|
return EventHandlerResult::EVENT_CONSUMED;
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, when an `X` key is pressed, no Keyboard HID report will be generated and sent to the host, but the key will still be recorded by Kaleidoscope as "live". If we hold that key down and press a `Y` key, we will suddenly see both `x` _and_ `y` in the output on the host. This is because returning `ABORT` suppresses the key event entirely, as if it never happened, whereas `EVENT_CONSUMED` signals to Kaleidoscope that the key should still become "live", but that no further processing is necessary. In this case, since we want to suppress all `X` keys entirely, we should return `ABORT`.
|
||||||
|
|
||||||
|
### A complete in-sketch plugin
|
||||||
|
|
||||||
|
Here's an example of a very simple plugin, defined as it would be in a firmware sketch (e.g. a `*.ino` file):
|
||||||
|
|
||||||
|
```c++
|
||||||
|
namespace kaleidoscope {
|
||||||
|
namespace plugin {
|
||||||
|
|
||||||
|
class KillX : public Plugin {
|
||||||
|
public:
|
||||||
|
EventHandlerResult onKeyEvent(KeyEvent &event) {
|
||||||
|
if (event.key == Key_X)
|
||||||
|
return EventHandlerResult::ABORT;
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kaleidoscope
|
||||||
|
} // namespace plugin
|
||||||
|
|
||||||
|
kaleidoscope::plugin::KillX;
|
||||||
|
```
|
||||||
|
|
||||||
|
On its own, this plugin won't have any effect unless we register it later in the sketch like this:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
KALEIDOSCOPE_INIT_PLUGINS(KillX);
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `KALEIDOSCOPE_INIT_PLUGINS()` should only appear once in a sketch, with a list of all the plugins to be registered.
|
||||||
|
|
||||||
|
## Plugin registration order
|
||||||
|
|
||||||
|
Obviously, the `KillX` plugin isn't very useful. But more important, it's got a potential problem. Suppose we had another plugin defined, like so:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
namespace kaleidoscope {
|
||||||
|
namespace plugin {
|
||||||
|
|
||||||
|
class YtoX : public Plugin {
|
||||||
|
public:
|
||||||
|
EventHandlerResult onKeyEvent(KeyEvent &event) {
|
||||||
|
if (event.key == Key_Y)
|
||||||
|
event.key = Key_X;
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kaleidoscope
|
||||||
|
} // namespace plugin
|
||||||
|
|
||||||
|
kaleidoscope::plugin::YtoX;
|
||||||
|
```
|
||||||
|
|
||||||
|
`YtoX` changes any `Y` key to an `X` key. These two plugins both work fine on their own, but when we put them together, we get some undesirable behavior. Let's try it this way first:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
KALEIDOSCOPE_INIT_PLUGINS(YtoX, KillX);
|
||||||
|
```
|
||||||
|
|
||||||
|
This registers both plugins' event handlers with Kaleidoscope, in order, so for each `KeyEvent` generated in response to a keyswitch toggling on or off, `YtoX.onKeyEvent(event)` will get called first, then `KillX.onKeyEvent(event)` will get called.
|
||||||
|
|
||||||
|
If we press `X`, the `YtoX` plugin will effectively ignore the event, allowing it to pass through to `KillX`, which will abort the event.
|
||||||
|
|
||||||
|
If we press `Y`, `YtoX.onKeyEvent()` will change `event.key` from `Key_Y` to `Key_X`. Then, `KillX.onKeyEvent()` will abort the event. As a result, both `X` and `Y` keys will be suppressed by the combination of the two plugins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Now, let's try the same two plugins in the other order:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
KALEIDOSCOPE_INIT_PLUGINS(KillX, YtoX);
|
||||||
|
```
|
||||||
|
|
||||||
|
If we press `X`, its keypress event will get aborted by `KillX.onKeyEvent()`, and that key will not become live, so when it gets released, the event generated won't have the value `Key_X`, but will instead by `Key_Inactive`, which will not result in anything happening, either from the plugins or from Kaleidoscope itself.
|
||||||
|
|
||||||
|
Things get interesting if we press and release `Y`, though. First, `KillX.onKeyEvent()` will simply return `OK`, allowing `YtoX.onKeyEvent()` to change `event.key` from `Key_Y` to `Key_X`, causing that `Key_X` to become live, and sending its keycode to the host in the Keyboard USB HID report. That's all as expected, but then we release the key, and that's were it goes wrong.
|
||||||
|
|
||||||
|
`KillX.onKeyEvent()` doesn't distinguish between presses and releases. When a key toggles off, rather than looking up that key's value in the keymap, Kaleidoscope takes it from the live keys array. That means that `event.key` will be `Key_X` when `KillX.onKeyEvent()` is called, which will result in that event being aborted. And when an event is aborted, the key's entry in the live keys array doesn't get updated, so Kaleidoscope will treat it as if the key is still held after release. Thus, far from preventing the keycode for `X` getting to the host, it keeps that key pressed forever! The `X` key becomes "stuck on" because the plugin suppresses both key _presses_ and key _releases_.
|
||||||
|
|
||||||
|
### Differentiating between press and release events
|
||||||
|
|
||||||
|
There is a solution to this problem, which is to have `KillX` suppress `Key_X` toggle-on events, but not toggle-off events:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
EventHandlerResult KillX::onKeyEvent(KeyEvent &event) {
|
||||||
|
if (event.key == Key_X && keyToggledOn(event.state))
|
||||||
|
return EventHandlerResult::ABORT;
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Kaleidoscope provides `keyToggledOn()` and `keyToggledOff()` functions that operate on the `event.state` bitfield, allowing plugins to differentiate between the two different event states. With this new version of the `KillX` plugin, it won't keep an `X` key live, but it will stop one from _becoming_ live.
|
||||||
|
|
||||||
|
Our two plugins still yield results that depend on registration order in `KALEIDOSCOPE_INIT_PLUGINS()`, but the bug where the `X` key becomes "stuck on" is gone.
|
||||||
|
|
||||||
|
It is very common for plugins to only act on key toggle-on events, or to respond differently to toggle-on and toggle-off events.
|
||||||
|
|
||||||
|
## Timers
|
||||||
|
|
||||||
|
Another thing that many plugins need to do is handle timeouts. For example, the OneShot plugin keeps certain keys live for a period of time after those keys are released. Kaleidoscope provides some infrastructure to help us keep track of time, starting with the `afterEachCycle()` "event" handler function.
|
||||||
|
|
||||||
|
The `onKeyEvent()` handlers only get called in response to keyswitches toggling on and off (or as a result of plugins calling `Runtime.handleKeyEvent()`). If the user isn't actively typing for a period, its `onKeyEvent()` handler won't get called at all, so it's not very useful to check timers in that function. Instead, if we need to know if a timer has expired, we need to do it in a function that gets called regularly, regardless of input. The `afterEachCycle()` handler gets called once per cycle, guaranteed.
|
||||||
|
|
||||||
|
This is what an `afterEachCycle()` handler looks like:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
EventHandlerResult afterEachCycle() {
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns an `EventHandlerResult`, like other event handlers, but this one's return value is ignored by Kaleidoscope; returning `ABORT` or `EVENT_CONSUMED` has no effect on other plugins.
|
||||||
|
|
||||||
|
In addition to this, we need a way to keep track of time. For this, Kaleidoscope provides the function `Runtime.millisAtCycleStart()`, which returns an unsigned integer representing the number of milliseconds that have elapsed since the keyboard started. It's a 32-bit integer, so it won't overflow until about one month has elapsed, but we usually want to use as few bytes of RAM as possible on our MCU, so most timers store only as many bytes as needed, usually a `uint16_t`, which overflows after about one minute, or even a `uint8_t`, which is good for up to a quarter of a second.
|
||||||
|
|
||||||
|
We need to use an integer type that's at least as big as the longest timeout we expect to be used, but integer overflow can still give us the wrong answer if we check it by naïvely comparing the current time to the time at expiration, so Kaleidoscope provides a timeout-checking service that's handles the integer overflow properly: `Runtime.hasTimeExpired(start_time, timeout)`. To use it, your plugin should store a timestamp when the timer begins, using `Runtime.millisAtCycleStart()` (usually set in response to an event in `onKeyEvent()`). Then, in its `afterEachCycle()` call `hasTimeExpired()`:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
namespace kaleidoscope {
|
||||||
|
namespace plugin {
|
||||||
|
|
||||||
|
class MyPlugin : public Plugin {
|
||||||
|
public:
|
||||||
|
constexpr uint16_t timeout = 500;
|
||||||
|
|
||||||
|
EventHandlerResult onKeyEvent(KeyEvent &event) {
|
||||||
|
if (event.key == Key_X && keyToggledOn(event.state)) {
|
||||||
|
start_time_ = Runtime.millisAtCycleStart();
|
||||||
|
timer_running_ = true;
|
||||||
|
}
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventHandlerResult afterEachCycle() {
|
||||||
|
if (Runtime.hasTimeExpired(start_time_, timeout)) {
|
||||||
|
timer_running_ = false;
|
||||||
|
// do something...
|
||||||
|
}
|
||||||
|
return EventHandlerResult::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool timer_running_ = false;
|
||||||
|
uint16_t start_time_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kaleidoscope
|
||||||
|
} // namespace plugin
|
||||||
|
|
||||||
|
kaleidoscope::plugin::MyPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, the private member variable `start_time_` and the constant `timeout` are the same type of unsigned integer (`uint16_t`), and we've used the additional boolean `timer_running_` to keep from checking for timeouts when `start_time_` isn't valid. This plugin does something (unspecified) 500 milliseconds after a `Key_X` toggles on.
|
||||||
|
|
||||||
|
## Creating artificial events
|
||||||
|
|
||||||
|
## Controlling LEDs
|
||||||
|
|
||||||
|
## HID reports
|
||||||
|
|
||||||
|
## Physical keyswitch events
|
||||||
|
|
||||||
|
## Layer changes
|
Loading…
Reference in new issue