A keyboard simulation framework.
The scope of Papilio is to provide an API that allows to simulate a keyboard firmware. The actual firmware is represented by a simulator core that is registered with the simulator. A simulator core typically is a firmware binary that is cross compiled for e.g. the x86 architecture.
Most parts of the physical keyboard's internal states are also represented in the simulated keyboard. Among those the current keymaps and LED states.
Apart from integration testing, the supported API is also
meant to be used as a development tool.
When e.g. being used together with a debugger like gdb
, Papilio's
unique features can help to deal with complex error scenarios that are otherwise
hard to debug with the firmware traditionally running on the device.
Additionally, Papilio comes with an ASCII visualization of the keyboard covering keyboard keycodes and LED colors.
The following code snipped is an example that demonstrates how Papilio is used by Kaleidoscope-Simulator within an Arduino firmware sketch.
// ... at end of the firmware file
#ifdef KALEIDOSCOPE_VIRTUAL_BUILD
#include "Kaleidoscope-Simulator.h"
KALEIDOSCOPE_SIMULATOR_INIT
namespace kaleidoscope {
namespace simulator {
void runSimulator(Simulator &simulator) {
using namespace kaleidoscope::simulator::actions;
using namespace papilio::actions;
using namespace papilio;
auto test = simulator.newTest("A simple test");
// Have some action in order to trigger a keyboard report.
//
simulator.tapKey(2, 1); // (row = 2, col = 1) -> A
// Run a single scan cycle (during which the key will be detected as
// tapped and a keyboard report will be generated). Assert that the
// letter 'a' is part of this keyboard report
//
simulator.cycleExpectReports(AssertKeycodesActive{Key_A});
}
} // namespace simulator
} // namespace kaleidoscope
#endif
This very simple test checks if a keycode for the letter 'a' (defined as Key_A
by the Kaleidoscope firmware) is present in a keyboard
report that is generated as a reaction on a key at matrix position (row = 2, col = 1) being tapped
(pressed and immediately released).
This test implies the standard QWERTY keyboard layout to active for the letter 'a' being associated with the key at the respective matrix position on layer 0.
Although a very simple test, it could already catch multiple types of firmware programming issues, among those keymapping problems or reports accidentally not being generated.
See the documentation of Kaleidoscope-Simulator for more information about this example and about how Papilio can be used to test a firmware.
Actions are probably the most important aspect of Papilio's API. They are boolean conditions that are evaluated at specific points during the firmware simulation. If an action fails, an error message is produced and the test is considered as unsuccessfull.
Actions can check literaly everything what can also be checked programatically. For some common conditions, however, predefined action classes are provided for the sake of convenience. By supplying callback functions to custom actions (described in detail later on in this document) it is possible to define arbitrarily complex actions.
See the doxygen API documentation for more information about available action classes.
As the name tells, those actions analyze keyboard reports ensure specific properties of those.
As a sidenote: Keyboard reports are those sets of information that a keyboard sends to the host system. They can contain information about keycodes or modifiers being active.
This type of actions is evaluated at the end of firmware scan cycles.
In the above example we added a keyboard report action to a queue of actions. The head of this queue is evaluated whenever a HID report is generated by the firmware. After its evaluation the action is removed from the queue and discarded.
While this is very useful to assert properties of individual keyboard reports, under other circumstances it might be useful to define actions that are evaluated for every report, without being discarded. Those actions are referred to as permanent actions.
Actions can be queued or permanent both for both keyboard reports and cycles. Please, check out the methods
Simulator::reportActionsQueue()
Simulator::cycleActionsQueue()
Simulator::permanentBootKeyboardReportActions()
Simulator::permanentKeyboardReportActions()
Simulator::permanentMouseReportActions()
Simulator::permanentAbsoluteMouseReportActions()
Simulator::permanentReportActions()
Simulator::permanentCycleActions()
In most cases, the order and content of keyboard reports is known. In such cases a set of actions can be added to the queue via a single command, e.g.
simulator.reportActionsQueue().queue(
AssertKeycodesActive{Key_A}, // 1. keyboard report
AssertReportEmpty{}, // 2. keyboard report
AssertKeycodesActive{Key_B}, // 3. keyboard report
AssertReportEmpty{} // 4. keyboard report
);
Every individual action will be applied to an individual report.
Permanent actions can be added and removed at any time during simulator execution. They are evaluated whenever a keyboard report arrives or at the end of every cycle, respectively.
When multiple actions are supposed to be evaluated with respect to the same
keyboard report,
they can be supplied via the addGrouped(...)
method of the container returned by the premanent actions retreival functions like e.g. Simulator::permanentReportActions()
or the queueGrouped(...)
method of the queue container returned e.g. by Simulator::reportActionsQueue()
. All grouped actions must pass (logical and) for the test to
be considered as successful.
For instance
simulator.reportActionsQueue().queueGrouped(
AssertKeycodesActive{Key_A},
AssertKeycodesActive{Key_B}
);
would expect both keys 'a' and 'b' to be part of the next upcoming keyboard report.
Actions are boolean conditions. Sometimes you will want to invert their logic.
For instance
simulator.reportActionsQueue().queueGrouped(
AssertKeycodesActive{Key_A}.negate()
);
would allow any key and modifier keycodes to be part of a keyboard report except for key 'a'.
If the logic of an entire group of actions is to be inverted, they can be grouped explicitly and negated.
simulator.reportActionsQueue().queue(
Grouped{
AssertKeycodesActive{Key_A},
AssertKeycodesActive{Key_B}
}.negate()
);
Despite the numerous predefined action classes that come
with Kaleidosope-Simulator's API, under
certain cicumstances it might still be desirable to execute custom code that expresses
an action. This is easily possible by passing a C++ lambda function to the
constructor of a special Custom...Action
class, e.g.
simulator.reportActionsQueue().queue(
CustomReportAction<KeyboardReport_>{
[&](const KeyboardReport_ &kr) -> bool {
simulator.log() << "Custom keyboard report action triggered";
return true;
}
}
);
Note: The template parameter <KeyboardReport_>
informs the action about what type of report to expect. The same would work with <BootKeyboardReport_>
, <MouseReport_>
, <AbsoluteMouseReport_>
and <Report_>
. The latter for accessing any type of report.
The lambda function in this example actually doesn't evaluate
any specific condition (the action returns the constant true
).
But you are free to return the result of any complex test instead.
In general the lambda function
passed to the CustomReportAction
has a predefined signature
bool(const KeyboardReport &kr)
. In the above examples it uses [&]
to capture
all context variables by reference. This also includes the simulator object from
the surrounding context that
is used to generate additional log output.
Use the action class CustomAction
for custom cycle actions as e.g.
simulator.cycleActionsQueue().queue(
CustomAction{
[&]() -> bool {
simulator.log() << "Custom cycle action triggered";
return true;
}
}
);
Instead of when a keyboard report occurs or at the end of a cycle, actions may also be evaluated immediately as e.g.
simulator.evaluateActions(AssertLayerIsActive{1});
This example checks whether a specific layer is active. In most cases this is not necessary as the same condition can be checked programatically through Kaleidoscope's proper firmware API.
The only difference to doing so is that predefined actions might require less user code to do the same and your code might be less sensitive against API changes to the Kaleidoscope core.
When simulating and testing, key action (press/release/tap) is the most important input that causes the firmware to react.
In contrast to a real experiment where the user hits a key on the keyboard,
in a virtual keyboard run, keys are hit virtually through key action methods
of the Simulator
class.
While pressKey(...)
activates
a key at a given key matrix position, release(...)
will deactivate it.
The method tapKey(...)
simulates a key being tapped instantaneously
(pressed and released within a single cycle).
There are scenarios where a key must be tapped multiple times during a test run.
This might e.g. be necessary if we want to simulate cycling through the LED effects of
the keyboard. On the real keyboard we would hit the 'next LED effect' key multiple times.
To simplify this, the Simulator
class supports a dedicated method multiTapKey(...)
.
The following example demonstrates cycling through LED modes via multiple taps (stock firmware assumed).
// Cycle through the color effects and visualize the keyboard's LED state
//
simulator.multiTapKey(
15 /*num. taps*/,
0 /*row*/, 6/*col*/,
50 /* num. cycles after each tap */,
CustomAction{
[&]() -> bool {
renderKeyboard(simulator, keyboardio::model01::ascii_keyboard);
return true;
}
}
);
This example does the following. It simulates the LED effect cycle key (row = 0,
col = 6) being tapped 15 times. After each tap 50 cycles elapse and
a CustomAction
is executed every time.
This exaple uses the function renderKeyboard
. Please read the section about visualization for
more information on rendering the keyboard.
The virtual hardware comes with simulated timing. As it is impossible to estimate the actual runtime of a loop scan cycle on the target hardware, a fixed time increment must be associated with each loop cycle (default = 5 ms).
Use the Simulator
class' methods setCycleDuration(...)
and getCycleDuration()
to
consider cycle duration of a specific keyboard hardware.
There are different methods to progress time.
This method runs a single firmware cycle and advances time accordingly.
Same as cycle()
but it accepts a number of actions that are
evaluated for the keyboard reports that are generated during the cycle.
This method runs a number of firmware cycles and advances time accordingly.
The simulator processes cycles until it reaches a specific point in time.
Runs a number of cycles with a given total duration.
The simulator API supports several logging methods. All log output is written
to a common std::ostream
object. This stream object can be queried and registered
through the Simulator
class' methods getOStream()
and setOStream(...)
.
Every log line starts with information about the current time and firmware cycle ID.
FWIW, there's no need to pass std::endl
or "\n"
to any of the logging functions.
Line breaks are inserted automatically.
Standard log text can be generated for instance as
simulator.log() << "Text ... " << 12 << ...;
This will e.g. generate
t=5750, c=1150: Text ... 12
in the log output.
Log stream output works exactly as with std::ostream
of C++'s standard library.
For error logging use simulator.error()
instead, e.g. as
simulator.error() << "Something bad happened...";
This will e.g. generate
t=5770, c=1154: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Error !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
t=5770, c=1154: !!! Something bad happened...
t=5770, c=1154: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
in the log output.
Use simulator.header()
to generate text headers.
simulator.header() << "And now for something completely different...";
This will e.g. generate
t=5770, c=1154: ########################################################
t=5770, c=1154: ### And now for something completely different...
t=5770, c=1154: ########################################################
in the log output.
Papilio comes with functions that help integration testing of LED modes.
During a reference run the function dumpKeyLEDState()
may be used
to generate the C++ code of a pre-initialized array that represents the current
state of the per-key LEDs.
The generated code of the array can then be used to defined a LED state
verification test using assertKeyLEDState(...)
. This enables to assert
that the per-key LEDs are at a given color-state at a specific
time during future test-runs.
During development and when debugging it may be of great help to visualize what the actual keyboard would do, especially when it comes to LED effects.
Papilio allows to display an ASCII-text representation of the
keyboard via the renderKeyboard(...)
function.
This function is passed a string that is a template of the actual keyboard.
At the time this document was written, only for Keyboardio's model Model01,
the first Kaleidoscope-supporting keyboard, a predefined template string
was shipped with Kaleidoscope-Simulator (see vendors/keyboardio/model01.cpp
).
// In the firmware sketch file
#ifdef KALEIDOSCOPE_VIRTUAL_BUILD
#include "Kaleidoscope-Simulator.h"
#include "vendors/keyboardio/model01.h"
...
renderKeyboard(simulator, keyboardio::model01::ascii_keyboard);
Papilio can simulate the keyboard in realtime. Realtime means that the simulator runs approximately at the same speed as the real keyboard hardware would run.
There are two methods of the simulator class that enable this.
Runs the simulator in pseudo-realtime for a given amount of time. A function that is passed as a parameter is executed after every scan cycle.
We refer to pseudo-realtime here as the duration of a cycle is defined
as a constant via the configuration method setCycleDuration(...)
.
On the real keyboard, in contrast, the duration of a cycle depends on the actual computational task of each cycle which is unknown to the simulator.
The host hardware is typically much faster than the device hardware. Because of this the host inserts idle time after executing a cycle's computational task to ensure that the resulting cycle times are as expected.
In this mode of operation, the simulator reads keyswitch information from stdin at the beginning of every cycle. Keyswitch activation information is typically generated by a suitable plugin running on the device, e.g. Kaleidoscope-Simulator-Control for a Kaleidoscope driven firmware. That way it is possible to use the physical keyboard to generate realtime keyswitch input for the simulator.
The simulator waits for new input to arrive at the beginning of each cycle. On the other hand, the physical keyboard also sends the state of its keyswitches once per cycle. This allows the timing of the simulator to be close to that of the real hardware.
Remote controlled simulation is useful to prototype new LED modes that react on user input. In contrast to running traditional compile-flash-test-modifiy cycles, there is no flashing and all the nice debugging features of the keyboard simulator are available.
It is quite easy to define a custom template string for any keyboard.
Appart from any ASCII art that represents the keyboard hardware,
the template may contain tokens {xxxx}
where xxxx
is an integer number
that equals the expression row*matrix_colums + col
being evaluated
for a key's (row, col)
tuple.
Any such tokens, when encountered in the template are automatically replaced by
information about the keycode that the respective key would currently generate. The key
is colored in the same way as its key LED (if there are key LEDs present).
Please note that any {xxxxx}
token is replaced by exactly four visible characters
no matter how wide in terms of characters its appearance in the template string.
In the initial example we just defined a single test function runSimulator(...)
.
To structure individual tests, it can be beneficial to define an individual function
for each test.
...
void test1(Simulator &simulator) {
...
}
void test2(Simulator &simulator) {
...
}
void runSimulator(Simulator &simulator) {
test1(simulator);
test2(simulator);
}
With this approach it is more convenient to temporarily enable/disable tests by uncommenting/commenting the individual test function invokations.
To generate Papilio's API documentation with doxygen make sure doxygen is installed on you unixoid system (GNU/Linux & macOS) and run
make doc
in the root directory of Papilio.