The firmware
Configure everything in a JSON file, flash it once, and the box behaves exactly how you set it up. No code changes, no drivers.
The firmware is CircuitPython for an RP2040 with a TCA9555 I/O expander. The device presents as a standard USB joystick with 128 buttons and 8 axes. A single JSON config file drives all the behaviour, so the same firmware fits any layout you build.
The rule engine
Rules run every cycle, 200 times a second, in the order you list them. Any pin not claimed by a rule automatically passes through, so D1 lands on button 1 and you only write rules for the things that need to be cleverer than that.
There are seven rule types:
- MAP sends an input straight to an output, with an optional invert.
- NOR turns on only when all its inputs are off, which is how a 3-way switch reports its middle position.
- TOGGLE flips its output on each rising edge of the input.
- PULSE fires its output for a set time, after an optional delay.
- ENCODER reads a quadrature pair and produces clockwise and counter-clockwise outputs.
- AXIS_INC and AXIS_DEC move an axis by a step on each rising edge.
Rules can read the outputs of earlier rules in the same cycle, which is what makes the useful combinations possible. An encoder feeding an axis is three rules stacked:
{ "type": "ENCODER", "inputs": ["D17", "D18"], "cw": "B17", "ccw": "B18" },
{ "type": "AXIS_INC", "input": "B17", "axis": "AX1", "step": 2048 },
{ "type": "AXIS_DEC", "input": "B18", "axis": "AX1", "step": 2048 }A 3-way switch, where D3 is up, D4 is down, and the NOR covers the middle:
{ "type": "NOR", "inputs": ["D3", "D4"], "output": "B30" }Bools and axes
Bools are named booleans for toggle states, and they are what the desktop app calls Variables. Axes are the eight HID outputs (X, Y, Z, Rx, Ry, Rz, Slider, Dial), 16 bit and centred at 32767. Both can be marked to persist, in which case their value is stored in NVM and survives a power cycle. A toggle that comes back on in the same state after you unplug the box is a store flag away.
Set an axis output to BACKLIGHT and it drives the backlight PWM without reporting anything to the sim.
Device settings
The device block carries the USB product name, the product ID, the debounce filter in milliseconds (10 by default), and how often the box sends a keep-alive HID report when nothing is happening.
The serial protocol
Alongside the joystick, the box exposes a USB CDC serial port. Commands are line delimited JSON, which is how the desktop app reads and writes config, streams live state, and pushes firmware. The commands cover connection tests, reading and validating config, live streaming, file transfer, reboot, and entering the UF2 bootloader.
Firmware updates are staged and transactional, with power-loss-safe installs and verified transfers. Update packages deliberately leave out config.json, so installing an update can never overwrite your own configuration.
boot.py, code.py, config.json, and the lib/ folder to the CIRCUITPY drive. After the first boot that drive is disabled, so to get it back you reflash CircuitPython over BOOTSEL or use the REPL on the console serial port.The full reference, including every rule field and the serial command list, is in the firmware repository. It is MIT licensed.