Medius - Native APIOption

Option

Set a persistent box option by id

One 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.

OptionidDoesFactory default
IMPERFECT0Clone an over-capacity device anywayoff
MOVE_RIDE1Inject motion only on a real moveoff
EMIT2Pick what paces injected motionlearnt
NAME3Give the box a human-readable nameMedius-XXXX
BEARING4What with and against are measured against20 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 option

OPTION 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

PAYLOAD
OffsetFieldTypeNotes
0idu8which option
1..valuevariesid-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 anyway
id 0 · [allow u8]
ALLOW
ValueEffect
0Faithful-only: refuse a device the box can't clone exactly (default)
1Clone 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.

EXAMPLE

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 move
id 1 · [timeout u16 LE] ms
TIMEOUT
ValueEffect
0Off: injection emits via the frame clock (default)
N msInjected 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.

EXAMPLE

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 against
id 4 · [window u16 LE] ms [mode u8]
WINDOW
ValueEffect
0No bearing is ever held, so with and against are inert whatever their scale
N msAn axis keeps the direction of its last injected delta for N ms (default 20)
MODE
ValueGeometry
0Per axis (default)
1Vector
2 or aboveUnknown: 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.

EXAMPLE

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 at
id 2 · [mode u8][rate_hz u16 LE][force_hz u16 LE]
MODE
modeNamerate_hzEmit paced to
0Learnt (default)n/aThe rate the real mouse actually reports at
1Intervaln/aThe cloned mouse's declared poll rate (its bInterval)
2Fixedtarget Hzrate_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
force_hzWhat the box does
0 (default)Serves the captured descriptor and polls the device at the interval it declared
target HzWrites 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.

EXAMPLE

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 name
id 3 · [name ascii 1..32] (0 bytes = clear)
VALUE
BytesEffect
1..32 printable ASCIISets 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.

EXAMPLE

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  |
+--------+--------+--------+--------+--------+--------------+--------+