What is the advantage of bundling key events and sending USB keyboard reports once per loop cycle, thereby clearing the report in every cycle, against an alternative approach that issues a key report whenever a key state changes, without ever clearing the keyboard report data structure?
Lengthy explanation why this is relevant
Finally I got around to port my QMK firmware-mod and keymap to Kaleidoscope. When I started (months ago), I expected it to be much easier than it had been using QMK. Kaleidoscopes’s design, with its plugin interface and hooks appeared to be pretty well suited. That was one of the major advantages that attracted me coming from the QMK world.
Eventually, it turned out that the port was almost as hard as it was to implement all the stuff for QMK in the first place. Why was my primary assumption that wrong?
One of the issues that cost me the greatest amount of time to solve where the various sorts of rollover issues that were mostly related to the way USB keyboard HID reports are dealt with by the firmware.
I’d like to start with a brief explanation of how USB keyboard reports are currently generated. This is for all those who have not delved into the firmware core yet.
The USB HID keyboard report is essentially a data array that is passed to the host system once in a while (usually once per loop cycle after keys have been pressed and released) to inform the OS about key events. The key report thereby contains information about keys and modifiers that are currently pressed. It is the host OS’s task to compare consecutive reports to find out which key states actually changed. The key report array can be modified programmatically, e.g. from plugins’ hook methods or macros, but it is mostly changed by the firmware core when changes of key states are are detected during key matrix scans.
In every cycle of the firmware loop, first the keyboard matrix is scanned for changes. Event handler hooks enable firmware plugins to react on newly pressed/released keys and modify the keyboard report accordingly. In the next stage, another set of hook methods, the pre-clear loop hooks, may again modify the keyboard report before it is finally send to the host. After the report was send, the report data structure is cleared. Then, at the end of the loop cycle, another set of hook methods, the post-report loop hooks are allowed to pre-populate the cleared keyboard report before the next loop cycle commences.
For most applications this approach of bundling key events and sending and clearing the report data structure is sufficient. But for more complex applications like the plugin I recently developed, this is a quite problematic approach. The plugin I am speaking of - working title Kaleidoscope-Papageno - does some complex types of key event pattern recognition (clusters, chords, arbitrary key sequences, and tap dances) with the minimum possible memory (RAM and PROGMEM) footprint. This is quite a complex thing (ok - frankly, metaphorically it’s close to Frankensteins’ Monster ). But it is very powerful.
However, there were many corner cases I had to eliminate, which I finally managed with the help of a firmware simulator that I developed, mostly for this purpose. Without its ability to simulate the exact timing of the firmware loop, and a huge amount of regression tests I would have been lost. But even with this nice tool I spend hours banging my head against whatever hard objects I could find.
Apparently, I am not alone with this sort of problems. In his posts here in the forum @merlin mentioned he also had to deal with similar problems to make his popular Qukeys plugin work. And maybe there are others who experience the same.
Most of our problems come from the fact that key events are bundled into keyboard reports, which are send in each loop (only if the report data structure changed compared to the previous send attempt). The report data structure is thereby cleared before the key matrix processing of the next cycle happens. This has several drawbacks
- When keys are pressed and released in the same cycle, the report does not reflect it.
- Most hooks see incompletely populated reports.
- A loop hook cannot see if keys changed unless it stores the completed report (which it cannot as the report is only complete in the moment when it is send).
These are only some of the aspects that might cause problems.
I heard rumours about serious attempts to change the current key event handling towards an event driven approach. This is a complex task. It will take some time until it will be production ready and, of course, only if the gods of Kaleidoscope are well meaning towards such changes.
For the meantime, maybe it would be possible to change the current implementation a little bit to make it easier to work with by no more clearing the key report in every cycle and by sending a report after every key event (key press and release), thereby staying compatible with the existing plugins?
I am not sure if I am missing important details here that would explain why things are done the way they are done right now. Also, the approach I proposed might have some severe drawbacks that I overlook (although my firmware mod works nice and fast based on the proposed changes).
In any case I would be happy about explanations, comments and discussion.