Option
Set a persistent box option by idOne command (opcode 0x11) sets every box-level toggle: an id byte picks the option, the rest is its value. All persist in NVS, restore at boot, and are fire-and-forget. An unknown id is ignored.
| Option | id | Does | Factory default |
|---|---|---|---|
IMPERFECT | 0 | Clone an over-capacity device anyway | off |
MOVE_RIDE | 1 | Inject motion only on a real move | off |
EMIT | 2 | Pick what paces injected motion | learnt |
NAME | 3 | Give the box a human-readable name | Medius-XXXX |
BEARING | 4 | What with and against are measured against | 20 ms, per axis |
Each card carries its own value layout on its signature line. A box that has been set boots at its own stored value, not the factory one.
OPTION
One generic, persistent optionOPTION carries an id byte then an id-specific value, and the box persists the setting across a reboot. Opcode 0x11.
OPTION 0x11 · payload 1 + value bytes
Fire-and-forget
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | id | u8 | which option |
| 1.. | value | varies | id-specific; the frame LEN delimits it, so a new option needs no new opcode |
No reply. Read any value back with QUERY(OPTIONS, id).
IMPERFECT
Clone an over-capacity device anywayid 0 · [allow u8]
| Value | Effect |
|---|---|
0 | Faithful-only: refuse a device the box can't clone exactly (default) |
1 | Clone it anyway: every other interface byte-faithful, the over-capacity one dead |
Some devices need more interrupt-IN endpoints than the box serves (the Wooting Two HE's analog stream needs a sixth, past the ESP32-S3's five). Changing this for an attached over-capacity device reboots the box to re-clone; a normal device is unaffected.
Read QUERY(OPTIONS, 0) (opt-in plus the over-capacity and imperfect-clone flags) · Library allow_imperfect_clones.
Opt in (allow = 1):
+--------+--------+--------+--------+--------+--------+--------+ | A5 | 11 | 00 | 02 00 | 00 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | id | allow | CRC16 | +--------+--------+--------+--------+--------+--------+--------+
MOVE_RIDE
Inject motion only on a real moveid 1 · [timeout u16 LE] ms
| Value | Effect |
|---|---|
0 | Off: injection emits via the frame clock (default) |
N ms | Injected cursor and wheel motion only rides a native move seen within N ms; no synthetic motion frame, and motion left unridden is dropped (never dumped on the next move) |
This keeps injected motion's report density identical to the real mouse's, erasing the density tell (a human aims at ~270-360 Hz, idle 60-70% of the time; gap-filling injection runs gapless near 990 Hz).
While on, pure idle injection (moving the cursor while the real device is still) stops working: motion waits for a native move and is dropped if none comes. Button, key, and media injection are unaffected, and a move can opt out per command with the MOVE flags.
Changing the value drops whatever motion was held for a ride, and clears the standing bearing with it, so every with / against scale stops applying at that instant.
Read QUERY(OPTIONS, 1) · Library set_movement_riding.
Turn it on with a 20 ms window (timeout = 0x0014):
+--------+--------+--------+--------+--------+--------+--------+ | A5 | 11 | 00 | 03 00 | 01 | 14 00 | lo hi | +--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | id | timeout| CRC16 | +--------+--------+--------+--------+--------+--------+--------+
BEARING
What with and against are measured againstid 4 · [window u16 LE] ms [mode u8]
| Value | Effect |
|---|---|
0 | No bearing is ever held, so with and against are inert whatever their scale |
N ms | An axis keeps the direction of its last injected delta for N ms (default 20) |
| Value | Geometry |
|---|---|
0 | Per axis (default) |
1 | Vector |
2 or above | Unknown: the whole command is dropped, window included, with no reply to say so |
A write that changes either field drops the standing bearing and the banked carry on every mouse interface. With a with / against scale live that is a visible step in what reaches the game PC, so set the geometry before the scales, not between reports.
Read QUERY(OPTIONS, 4) · Library set_bearing.
A 20 ms window in vector mode (window = 0x0014, mode = 1):
+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 11 | 00 | 04 00 | 04 | 14 00 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | id | window | mode | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+
EMIT
Pick what paces injected motion, and what rate the clone runs atid 2 · [mode u8][rate_hz u16 LE][force_hz u16 LE]
mode | Name | rate_hz | Emit paced to |
|---|---|---|---|
0 | Learnt (default) | n/a | The rate the real mouse actually reports at |
1 | Interval | n/a | The cloned mouse's declared poll rate (its bInterval) |
2 | Fixed | target Hz | rate_hz, snapped to 1000/n |
Fixed snaps to 1000/n Hz on the 1 ms frame clock and caps at 1 kHz, so 1000, 500, 333, 250… are exact and 750 lands on 1000 (0 means 1000).
Every mode raises the ceiling only: the box still emits a frame solely when injection is pending, so idle stays idle.
force_hz | What the box does |
|---|---|
0 (default) | Serves the captured descriptor and polls the device at the interval it declared |
| target Hz | Writes the bInterval nearest that rate onto every HID interrupt-IN endpoint of the served descriptor, and polls the device at that same interval |
A forced rate applies only with IMPERFECT on, because the descriptor stops matching the real device. Changing the resolved interval re-clones the box, which drops this port for a few seconds.
Vendor interfaces, interrupt-OUT and isochronous endpoints keep the captured value. A low-speed clone cannot express an interval below 10 ms, so a request above 100 Hz there resolves to 100.
force_hz is independent of mode: a clone can advertise 1 kHz while injection still paces to the learnt native rate. Both ride one command, so every OPTION(EMIT) writes both.
Read QUERY(OPTIONS, 2) (mode, the rate in effect, and what the clone advertises) · Library set_emit_pace.
Fixed 1 kHz with the wire forced to 1 kHz (mode = 2, rate_hz = 0x03E8, force_hz = 0x03E8):
+--------+--------+--------+--------+--------+--------+---------+----------+--------+ | A5 | 11 | 00 | 06 00 | 02 | 02 | E8 03 | E8 03 | lo hi | +--------+--------+--------+--------+--------+--------+---------+----------+--------+ | SOF | TYPE | SEQ | LEN | id | mode | rate_hz | force_hz | CRC16 | +--------+--------+--------+--------+--------+--------+---------+----------+--------+
NAME
Give the box a human-readable nameid 3 · [name ascii 1..32] (0 bytes = clear)
| Bytes | Effect |
|---|---|
1..32 printable ASCII | Sets the box's name to those bytes. |
0 (the id alone) | Clears the name, reverting to the synthesized Medius-XXXX default derived from the MAC. |
The name is the readable partner to the box MAC, persisted in NVS with no reboot. It rides on RESP(VERSION) as the ASCII tail after the MAC, so it's read there, not through QUERY(OPTIONS).
Read it back on RESP(VERSION) · Library set_name.
Name the box "Loki" (id = 3, ascii 4C 6F 6B 69):
+--------+--------+--------+--------+--------+--------------+--------+ | A5 | 11 | 00 | 05 00 | 03 | 4C 6F 6B 69 | lo hi | +--------+--------+--------+--------+--------+--------------+--------+ | SOF | TYPE | SEQ | LEN | id | name ascii | CRC16 | +--------+--------+--------+--------+--------+--------------+--------+