Requests
QUERYAsk the box for state
QUERY asks for one piece of state, named by its what byte. Opcode 0x05.
QUERY 0x05 · payload 1 byte (2 for OPTIONS, REWRITE_ENTRY and PATCH_ENTRY)
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | state to read (below) |
| 1 | id | u8 | option (what = 9), rule (13) or patch (15); omitted otherwise |
what | Reads | Reply |
|---|---|---|
0 | Firmware version. | VERSION |
1 | Box health. | HEALTH |
2 | Cloned device's USB identity, kind, and product. | DEVICE_INFO |
3 | Whole-device capabilities (mouse + keyboard). | CAPS |
4 | Native report rate. | RATE |
5 | Delivery and telemetry counters. | STATS |
6 | Active input locks. | LOCKS |
7 | Active catch subscription table, its drop counts, and the cross-chip clock estimate. | CATCH |
8 | retired; folded into CAPS | - |
9 | Persistent box option, by id. | OPTIONS |
10 | Buffered-clip ring depth, playback state, and config. | CLIP |
11 | Both chips' firmware versions, the slot each runs, and what is staged. | FIRMWARE |
12 | Rewrite-rule table summary. | REWRITE |
13 | One rewrite rule in full, in the REWRITE command's shape. | REWRITE_ENTRY |
14 | Descriptor-patch set summary. | PATCHES |
15 | One descriptor patch in full, in the PATCH command's shape. | PATCH_ENTRY |
16 | Active field-transform table. | TRANSFORMS |
The box replies with a RESP carrying the same what and the requested data. Besides QUERY, only UPDATE and TRANSFER reply. Every other command is fire-and-forget.
Library bindings: query_version, query_health, device_info, caps, query_rate, query_stats, query_locks, query_catch, query_imperfect, query_movement_riding, query_bearing, query_emit_pace, the clip status query, firmware_info, query_rewrite, query_rewrite_entry, query_patches, query_patch_entry, and query_transforms.
what = 0 (read the version):
RESPReply from the box
RESP is the box's reply to a QUERY. Opcode 0x06.
RESP 0x06 · box → PC
Reply
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | echoes the request's selector |
| 1.. | data | varies | layout for the requested what (see selectors) |
At most one RESP per QUERY, its SEQ matching the request's.
VERSIONRESP payload, what = 0
The RESP payload when what = 0: protocol version, firmware version, base mac, then a length-delimited ASCII name tail. The tail is additive. An older box sends it empty.
QUERY what = 0 · RESP 11-byte header + name
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x00 |
| 1 | proto_ver | u8 | protocol version, expected 9 |
| 2 | fw_major | u8 | firmware major |
| 3 | fw_minor | u8 | firmware minor |
| 4 | fw_patch | u8 | firmware patch |
| 5 | mac | u8[6] | the device chip's base MAC; the stable per-box id, rendered as 12 lowercase hex digits |
| 11.. | name | ascii | human-readable box name, filling the rest of the payload (delimited by the frame LEN); a synthesised Medius-XXXX default when unset, set via OPTION(NAME) |
The box also sends this unprompted at boot and on the first frame it receives, as a ready signal. The mac identifies the same box across replugs and port renumbering. Library binding: query_version.
Firmware 3.4.6, protocol 9, MAC 123456789abc, name "Loki":
HEALTHRESP payload, what = 1
The RESP payload when what = 1: a two-byte flags word (u16, little-endian), each bit an independent status.
QUERY what = 1 · RESP 3 bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x01 |
| 1 | flags | u16 | status bits below, little-endian |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | the host-chip link is up |
| b1 | 0x02 | a real mouse is attached |
| b2 | 0x04 | the PC has set up the cloned mouse |
| b3 | 0x08 | injection is active |
| b4 | 0x10 | RATE_CONFIDENT: a mouse is bound and the native-rate estimator window is full, so the RATE value is trustworthy |
| b5 | 0x20 | LOCK_ON: a LOCK scale on ≥1 input differs from a full pass (blocked or merely weighed) |
| b6 | 0x40 | CATCH_ON: the CATCH subscription table is non-empty, so events are streaming. QUERY(CATCH) reads what is subscribed |
| b7 | 0x80 | KBD_ATT: a keyboard is attached on the host chip, cloned and injectable |
| b8 | 0x0100 | REWRITE_ON: the rewrite rule table is non-empty |
| b9 | 0x0200 | PATCH_ON: the clone is serving a descriptor-patch set, as PATCHES b0 |
| b10 | 0x0400 | TRANSFORM_ON: a field transform is active (the table is non-empty) |
With the first three bits set, input reaches the PC. Library binding: query_health.
Ready, with link, mouse, and clone all up (flags = 0x0007):
DEVICE_INFO RESP payload, what = 2
The RESP payload when what = 2: the USB identity, kind, and product string read from the real device, otherwise invisible to the control PC. Every field is zero with nothing cloned. A clone with no HID interface reads primary_kind 0, with its identity filled in.
QUERY what = 2 · RESP 11-byte header + product
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x02 |
| 1 | vid | u16 | idVendor, little-endian |
| 3 | pid | u16 | idProduct, little-endian |
| 5 | bcd_device | u16 | bcdDevice, the device release |
| 7 | bcd_usb | u16 | bcdUSB, e.g. 0x0200 or 0x0201 |
| 9 | flags | u8 | the bits below |
| 10 | primary_kind | u8 | cloned device kind (below), from its HID report descriptors and Boot declarations. A device carrying both kinds reads keyboard if it has a full key bitmap (unless it declares Boot for both) or declares Boot for the keyboard alone. Otherwise it reads mouse |
| 11.. | product | ASCII | product string, filling the rest of the payload, with ? for each character outside ASCII. In English if the real device lists English, else in its first language. May be empty |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | HAS_SERIAL: the clone serves a serial string |
| b1 | 0x02 | HAS_BOS: the clone serves a BOS descriptor |
| Value | Kind |
|---|---|
0 | unknown: neither a mouse nor a keyboard (a clone with no HID interface reads this), or nothing cloned |
1 | keyboard |
2 | mouse |
Library binding: device_info.
A Logitech G502 (046D:C08B), USB 2.01, serial and BOS served, kind mouse, product "G502":
CAPSRESP payload, what = 3
The RESP payload when what = 3: one summary of the whole cloned device, mouse and keyboard, read from its HID report descriptors. Counts and yes/no flags only, never raw HID field offsets.
An INJECT for a usage the real device lacks reaches no report field. An absent class reads all-zero.
QUERY what = 3 · RESP 7 bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x03 |
| 1 | n_buttons | u8 | buttons the mouse report carries; the cap on an injectable or lockable button id |
| 2 | axis_flags | u8 | mouse axes, the bits below |
| 3 | n_hid | u8 | cloned HID interfaces. >1 = composite. 0 with nothing cloned, or for a clone with no HID interface (a vendor-class pad); DEVICE_INFO's identity tells the two apart |
| 4 | n_keys | u8 | keycode-array slots, or 0xFF for NKRO; 0 = no keyboard |
| 5 | kbd_flags | u8 | keyboard, the bits below |
| 6 | change_driven | u8 | per class: b0 mouse (continuous, 0), b1 keyboard/media (change-driven, 1 when bound) |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | X: the report carries an X axis |
| b1 | 0x02 | Y: the report carries a Y axis |
| b2 | 0x04 | WHEEL: the report carries a wheel |
| b3 | 0x08 | REPORT_ID: the mouse report sits behind a HID report ID |
| b4 | 0x10 | PAN: the report carries an AC Pan (horizontal scroll) axis |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | NKRO: the keyboard reports an NKRO bitmap |
| b1 | 0x02 | CONSUMER: a Consumer collection is present, so media keys are injectable |
| b2 | 0x04 | SYSTEM: a system-control collection is present (relayed only) |
| b3 | 0x08 | REPORT_ID: the keyboard report sits behind a HID report ID |
Library binding: caps.
A 5-button mouse (X/Y/wheel, one interface) plus a 6-key Consumer keyboard (axis_flags = 0x07, kbd_flags = 0x02, keyboard change-driven):
RATERESP payload, what = 4
The RESP payload when what = 4: how fast the active input reports, plus the poll period the clone advertises. Which field to read depends on the input kind.
| Input kind | CHANGE_DRIVEN | Read | Gives |
|---|---|---|---|
| continuous (moving mouse) | 0 | native_period_us | Hz = 1e6 / period; reads 0 until learned |
| change-driven (keyboard, media) | 1 | poll_period_us | no steady cadence, so native_period_us is 0 |
QUERY what = 4 · RESP 6 bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x04 |
| 1 | native_period_us | u16 | realised native period in µs; 0 = not learned, or change-driven (see flags), Hz = 1e6/period |
| 3 | poll_period_us | u16 | cloned inject-endpoint bInterval poll period in µs; the figure to read for a change-driven input |
| 5 | flags | u8 | the bits below |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | CONFIDENT: the estimator window is full, same source as HEALTH RATE_CONFIDENT |
| b1 | 0x02 | CHANGE_DRIVEN: the active input is event-driven (keyboard / media), so there is no continuous cadence and native_period_us is 0 |
A 1 kHz mouse reads ~1000 µs once learned. Library binding: query_rate.
A 1 kHz mouse, 1000 µs poll, estimator confident (flags = 0x01):
STATSRESP payload, what = 5
The RESP payload when what = 5: delivery counters, the only delivery feedback for fire-and-forget commands.
A nonzero tx_drops or tx_wedges means native input was dropped or stalled on the way to the PC. A nonzero link_rx_drops or host_rx_drops means a native report or injected delta was lost between the two chips.
relay_drops is counted apart from the native-input drop counters, so lost relayed traffic doesn't read as lost native input. The eight narrowed counters clamp at their max instead of wrapping. The three drop counts are full width.
session counts releases of host-set state. A change since the last read means some or all of it is gone and needs setting again.
Most releases drop all of it, and two drop part. Turning OPTION(IMPERFECT) off drops only rewrite rules, consuming packet triggers and queued clip transfers, unless the toggle also presents the clone again. A configuration switch by the game PC that unbinds a role (the mouse, the keyboard, the consumer control) drops only the locks, injected usages and owed motion held on that role.
QUERY what = 5 · RESP 31 bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x05 |
| 1 | inject_emits | u32 | pure-injection reports emitted, little-endian |
| 5 | tx_drops | u16 | reports the clone's IN queue couldn't hold, never reaching the game PC; should stay 0 |
| 7 | tx_merges | u16 | backed-up reports merged instead of queued |
| 9 | tx_maxdepth | u8 | TX queue high-water mark |
| 10 | tx_wedges | u8 | wedged-endpoint recoveries |
| 11 | wakeups | u16 | remote wakeups the clone signalled to the game PC, for the real device or an injection |
| 13 | reset_count | u16 | USB bus resets seen |
| 15 | config_count | u16 | SET_CONFIGURATION events (re-enumerations) |
| 17 | link_rx_drops | u32 | host-chip input frames the device chip couldn't take off the link; should stay 0 |
| 21 | host_rx_drops | u32 | the same count on the host chip, relayed over the link; should stay 0 |
| 25 | relay_drops | u32 | relayed traffic and commands that went no further, none of it native input. IN: a vendor IN packet that found its endpoint's queue full of the control PC's RAW packets, or a native zero-length packet answering a poll the PC did not make (the one poll the box runs ahead of a suspended PC). OUT: a packet the box had no room to queue, that failed three times on the real device's bus, or that a bus reset or SET_INTERFACE overtook; a RAW OUT packet with no room to wait. Other: a relayed request the box could not queue; a flagged MOVE the motion batch could not take; any frame other than native input that either chip's link receive ring dropped; a frame from the control PC its receive ring had no room for (one per frame), or an overrun of the control port's UART (one per overrun, however many frames it cost) |
| 29 | session | u16 | releases of host-set state; wraps at 0xFFFF, so compare for inequality |
Library binding: query_stats.
Host-set state: injection, locks, CATCH subscriptions, rewrite rules, transforms, the clip with its settings and triggers, and an LED override. The count is 0 at boot, announced by the ready hello. One release counts once, however many of these it clears. A release of everything counts only if a command other than QUERY or RESET arrived since the last count.
| Event | Counts |
|---|---|
RESET | once |
| the inter-chip link drops | once per outage |
| the real device detaches | once. The clone's teardown after the detach grace counts again only if a command arrived during the grace |
| another device attaches | once |
a PATCH APPLY or CLEAR, or an OPTION(IMPERFECT) toggle, that presents the clone again | once |
| an update takes the clone down | once |
| control-PC silence | once per silence, if it released anything |
OPTION(IMPERFECT) turned off | if it drops rewrite rules, consuming packet triggers or queued clip transfers. A re-clone it causes counts again |
| a configuration switch by the game PC that unbinds a role | if a host held a lock, an injected usage or owed motion on that role |
| state the box refused on arrival: a rule with the opt-in off, a patch with no device attached | not counted |
a clear from the host: a whole-table REWRITE clear, a CATCH unsubscribe, CLIP_CTRL CLEAR | not counted |
4096 emits, every drop counter at 0, session at 2:
LOCKSRESP payload, what = 6
The RESP payload when what = 6: native inputs currently weighed by LOCK, one entry per non-passing direction. An empty list (n = 0) means everything passes.
QUERY what = 6 · RESP 2 + 6n bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x06 |
| 1 | n | u8 | number of entries that follow, ≤85 |
| + | class | u8 | per entry: 0=button 1=key 2=media 3=axis (as LOCK) |
| + | id | u16 | weighed field id, or 0xFFFF for a whole-class blanket, little-endian |
| + | direction | u8 | direction, as LOCK |
| + | scale | i16 | percent of the native value kept, little-endian, -255 to 255 (as LOCK); 0 = blocked, >100 amplifies, negative reverses |
Entries mirror the LOCK frame field for field, so a readback replays as commands.
| State | Reports as |
|---|---|
| An axis or button weighed on a direction | One entry under its class and id, per direction. |
| A target passing on every direction | Absent. |
| A blanket button or axis lock | One entry per member, under the member's id; never 0xFFFF. |
| A blanket key lock | One entry per blocked edge, id = 0xFFFF, direction 1 and/or 2; never 0. |
| A media lock, blanket or specific | Direction 0, always. Media has no edges. |
| A relative direction in vector mode | The effective scale, the lower of X's and Y's, on both axes. |
| Any momentary usage | scale of 0 or 100 only. The box stores the block or pass it renders, not the number sent, and never a negative. |
| A reversed axis | Its negative scale, sign and all. A reader that takes the field as a u8 sees 156 where the box holds -100, and takes a reversal for an amplification. |
| A relative direction with no bearing live | Its stored scale, unchanged. A lapsed window, or an OPTION(BEARING) window of 0, stops with and against weighing without clearing them, so an entry can report 40 while that axis passes untouched. |
| Order | Source | Max entries |
|---|---|---|
| 1 | Mouse axes and buttons | 48: 4 axes × 4 directions, plus 16 buttons × 2 edges. A button has no relative pair, so its other two slots are out of reach. |
| 2 | The blanket key lock | 2, one per blocked edge |
| 3 | The blanket media lock | 1 |
| 4 | Specific media usages | 8, the whole media-lock table |
| 5 | Specific keys | the rest of the 85, so ≥26, one per blocked edge |
The frame is the cap: 2 + 85 x 6 is exactly the 512-byte payload. The box's tables cap rows 1-4 at 59 combined, so they always fit. Keyboard usages are the one unbounded class: 252 of them, two edges each.
Truncation only hits row 5: n stops short with nothing marking the cut. Compare the key edges requested with those returned.
Library binding: query_locks.
One entry: the wheel's negative (scroll-down) sign blocked (class = 3 axis, id = 2 wheel, direction = 2, scale = 0):
CATCHRESP payload, what = 7
The RESP payload when what = 7: the active CATCH subscription, a fixed scalar header then the table, shaped like RESP(LOCKS). An empty table means nothing is subscribed, matching the CATCH_ON health bit.
QUERY what = 7 · RESP 19 + 7n bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x07 |
| 1 | flags | u8 | b0 = the table is full and an entry was refused |
| 2 | dropped | u32 | box-wide events that could not be queued, little-endian |
| 6 | clk_offset_us | i32 | the host chip's clock minus the device chip's, measured (below), little-endian |
| 10 | clk_rate_ppb | i32 | relative drift between the two chips in parts per billion; INT32_MIN = no fit made, little-endian |
| 14 | clk_delay_us | u16 | best measured round trip; the offset's error bound is half this, little-endian |
| 16 | clk_age_ms | u16 | age of the exchange the offset rests on; 0xFFFF = no estimate, little-endian |
| 18 | n | u8 | number of entries following |
| + | class | u8 | per entry: the address class |
| + | id | u16 | entry id, or 0xFFFF for a class blanket, little-endian |
| + | dir | u8 | entry direction |
| + | snaplen | u8 | bytes captured per event, 0 = whole packet |
| + | dropped | u16 | events this entry could not queue, little-endian |
The box-wide dropped counts every lost event. The per-entry one attributes them to a subscription.
CATCH is fire-and-forget, so this reply is the only confirmation an entry landed. A refused entry is absent from the list. flags b0 marks a full 32-entry table as the reason. Library binding: query_catch.
The two ESP32-S3s boot independently with unrelated timers, and events carry stamps from both (the clk byte).
The box measures the difference with a four-timestamp exchange across the inter-chip link, stamped as each frame reaches the wire rather than when it is queued.
device chip host chip
t1 ------- request -------------------> t2
|
t4 <---------------- reply ------------ t3
offset = ((t2 - t1) + (t3 - t4)) / 2
delay = (t4 - t1) - (t3 - t2) -> the error bound is delay / 2| Field | Use |
|---|---|
clk_delay_us | Round trip of the best exchange in the window. The offset is accurate to about half of it, a hard bound. |
clk_rate_ppb | Extrapolates between exchanges, since two independent crystals drift a stale offset by up to 20 µs/s. INT32_MIN means no fit yet, distinct from a fitted 0 (matched crystals). |
clk_age_ms | Age of the exchange the offset rests on: the least-delayed one in the window, often older than the newest. 0xFFFF marks "no estimate yet", which an offset of 0 alone can't tell from a true zero. |
Applying the offset is optional. The clk byte on each event stays authoritative.
One entry, every mouse button, both edges, whole packet (class = 0, id = 0xFFFF, dir = 0, snaplen = 0), with no drops and no clock estimate taken yet:
OPTIONSRESP payload, what = 9
The RESP payload when what = 9: one persistent box OPTION value, echoing the queried id. Values are id-specific, so each option is read separately. An unknown id gets no reply.
QUERY what = 9, id · RESP varies
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x09 |
| 1 | id | u8 | option id |
| 2.. | value | varies | id-specific, mirroring the matching OPTION value |
The IMPERFECT opt-in (id 0) plus two derived clone-status bytes. Each is 0 or 1. A faithful clone reads all-zero.
| Offset | Field | Notes |
|---|---|---|
| 2 | allowed | the opt-in toggle; 1 = opted in |
| 3 | over_capacity | the attached device has more IN endpoints live at once than the box has transmit FIFOs (four), has >6 HID interfaces, or runs at high speed |
| 4 | clone_imperfect | the live clone is imperfect: an opted-in device the box can't clone faithfully, a forced rate, or a descriptor-patch set it serves |
The value shows why a clone is missing (over_capacity = 1, allowed = 0) or confirms an imperfect clone is live (clone_imperfect = 1). Library binding: query_imperfect.
The current MOVE_RIDE window (id 1).
| Offset | Field | Notes |
|---|---|---|
| 2 | timeout | u16, little-endian; the ride window in ms, 0 = off |
Library binding: query_movement_riding.
The current EMIT pace and wire rate (id 2).
| Offset | Field | Notes |
|---|---|---|
| 2 | mode | the pace: 0 learnt, 1 interval, 2 fixed |
| 3 | fixed_hz | u16, little-endian; the configured fixed rate |
| 5 | resolved_hz | u16, little-endian; the ceiling in effect, 0 = learnt/adaptive or no device yet, 1000 once the renderer has a profile |
| 7 | force_hz | u16, little-endian; the requested wire rate, 0 = off |
| 9 | advertised_hz | u16, little-endian; what the clone's input endpoints advertise now, forced or native, 0 = no clone |
| 11 | force_active | 1 when a forced interval is in the served descriptor |
A force_hz set while IMPERFECT is off reads back with force_active 0 and advertised_hz still the native interval.
Library binding: query_emit_pace.
The current BEARING setting (id 4): window and geometry mode.
| Offset | Field | Notes |
|---|---|---|
| 2 | window | u16, little-endian; the bearing window in ms, 0 = off |
| 4 | mode | 0 per axis, 1 vector |
Library binding: query_bearing.
The current RENDER setting (id 5), and whether a render profile has been learned.
| Offset | Field | Notes |
|---|---|---|
| 2 | mode | the texture: 0 off, 1 stock, 2 de-spiked (default), 3 unsmoothed |
| 3 | full | 1 when native motion is rendered by the model rather than relayed |
| 4 | ready | 1 once a profile has armed for the attached device |
Nothing is rendered while ready reads 0. It also reads 0 while the box's two chips have not yet agreed which holds the motion. Until then, motion is relayed and injection takes the paced fill, whatever mode says.
Library binding: query_render.
The current SPREAD setting (id 6), and the interval injected deltas are released across.
| Offset | Field | Notes |
|---|---|---|
| 2 | percent | u16, little-endian; share of the command interval, 100 = one whole interval (default) |
| 4 | span_us | u32, little-endian; the interval in effect in microseconds, 0 = nothing is being spread |
span_us reads 0 while percent is 0, while no command period has been learned, and while the box's two chips have not yet agreed which holds the motion. In each case the whole delta is emitted on the next report the box emits.
Library binding: query_spread.
Reading id = 0: opted in, an over-capacity device attached and cloned imperfectly:
CLIPRESP payload, what = 10
The RESP payload when what = 10: the buffered-clip ring depth, playback state, and full config. A fixed prefix, then the clip's held-usage snapshot (the same class-tagged list a USAGE_EVENT carries), then the config tail.
QUERY what = 10 · RESP 31-byte prefix + held usages + config
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 10 |
| 1 | state | u8 | 0 idle / 1 playing / 2 paused / 3 faulted (recover with CLEAR) |
| 2 | free | u32 | ring bytes free; pace CLIP_APPEND top-ups off this, little-endian |
| 6 | total | u32 | retained clip size in bytes (streaming: buffered-but-undrained bytes) |
| 10 | played | u32 | bytes played from the clip start (retained progress; ~0 while streaming) |
| 14 | ticks | u32 | content ticks played (diagnostic) |
| 18 | underruns | u16 | empty-ring episodes |
| 20 | overruns | u16 | appends dropped whole because the ring was full |
| 22 | seq_gaps | u16 | dropped CLIP_APPEND frames detected (SEQ gaps) |
| 24 | xfers | u16 | clip transfers the real device completed with status 0 |
| 26 | xfer_errs | u16 | clip transfers that completed with another status, got no answer, found no room in the queue, or were dropped behind a slow 0xFE |
| 28 | gated | u16 | raw reports and transfers discarded because OPTION(IMPERFECT) was off |
| 30 | held_n | u8 | number of held usages that follow |
| + | class | u8 | per held usage: 0=button 1=key 2=media |
| + | id | u16 | held usage id (button id, HID keycode, or Consumer usage), little-endian |
| + | autolock | u8 | config: the CLIP_SET autolock bitmask (CLIP_LOCK_*) |
| + | flags | u8 | config: b0 loop, b1 retain, b2 finalized, b3 ride |
| + | n_trig | u8 | config: number of bound triggers that follow |
| + | class | u8 | per trigger: 0=button 1=key 2=media 0xFF=any |
| + | id | u16 | per trigger: the usage id, 0xFFFF=any, little-endian |
| + | edge | u8 | per trigger: 0 both / 1 press / 2 release |
| + | action | u8 | per trigger: the CLIP_CTRL op 0..5 (start/stop/pause/resume/restart/toggle) |
| + | consume | u8 | per trigger: 1 = lock the trigger usage while it stays active |
| + | n_pkt | u8 | config: number of packet triggers that follow |
| + | class | u8 | per packet trigger: the traffic class, 4-9 |
| + | id | u16 | per packet trigger: the class's address, 0xFFFF=any, little-endian |
| + | dir | u8 | per packet trigger: 0 both / 1 IN / 2 OUT |
| + | action | u8 | per packet trigger: the CLIP_CTRL op 0..5 |
| + | flags | u8 | per packet trigger: b0 clear, b1 consume, b2 RUN |
| + | slen | u8 | per packet trigger: the selector length |
| + | mlen | u8 | per packet trigger: the match length, 0-16 |
| + | hits | u16 | per packet trigger: packets it matched as the top-ranked trigger, saturating, little-endian |
| + | match, mask | u8[mlen] each | per packet trigger: as set |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | LOOP: playback restarts from the top on drain instead of stopping |
| b1 | 0x02 | RETAIN: the buffered content survives a stop instead of clearing |
| b2 | 0x04 | FINALIZED: the clip is sealed, no more CLIP_APPEND accepted |
| b3 | 0x08 | RIDE: the clip's motion waits to ride a native report instead of emitting on the box's clock |
The held snapshot lists the usages the clip is currently forcing down, one class-tagged entry each (3 bytes). ticks and the six counters count since boot and wrap.
A packet trigger reads back as the command that set it with hits spliced in after mlen and bit0 (present) clear. A host replays it by setting bit0. The whole reply is ≤507 bytes, inside one frame.
Library bindings: query_status (ClipStatus) and query_config (ClipSettings).
Idle, empty 64 KB ring, no held usages, no autolock, no triggers of either kind (state = 0, free = 65536):
One packet trigger entry: HID_IN id 0x0102, IN, TOGGLE, consume and RUN, selector 1, hits saturated.
04 02 01 01 05 06 01 02 FF FF 07 20 FF 20 class=4 HID_IN id=0x0102 dir=1 IN action=5 TOGGLE flags=0x06 slen=1 mlen=2 hits=65535 match=07 20 mask=FF 20
FIRMWARERESP payload, what = 11
The RESP payload when what = 11: both chips' versions, the slot each is running, and what is staged.
The only source of the host chip's version. VERSION reports the device chip alone.
Read it before an UPDATE: slot_size is the largest image a chip will take, and a chip whose state is pending-verify refuses to open one.
QUERY what = 11 · RESP 17 bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 11 |
| 1 | dev_major | u8 | |
| 2 | dev_minor | u8 | |
| 3 | dev_patch | u8 | |
| 4 | dev_slot | u8 | 0 = ota_0, 1 = ota_1 |
| 5 | dev_state | u8 | image state, below |
| 6 | host_present | u8 | 1 when the host chip has replied over the link |
| 7 | host_major | u8 | 0 when host_present is 0 |
| 8 | host_minor | u8 | |
| 9 | host_patch | u8 | |
| 10 | host_slot | u8 | 0xFF when host_present is 0 |
| 11 | host_state | u8 | |
| 12 | slot_size | u32 | usable bytes in a spare slot, little-endian; the same on both chips |
| 16 | staged | u8 | bit 0 device staged, bit 1 host staged |
| Value | State | Means |
|---|---|---|
0 | new | selected but not yet booted |
1 | pending-verify | booted, on probation: the rollback window |
2 | valid | confirmed by the image itself |
3 | invalid | the image asked to be rolled back |
4 | aborted | booted once and never confirmed |
0xFF | unknown | no entry for this slot |
Library binding: firmware_info.
Both chips on 3.4.6, device on ota_1, host on ota_0, both images valid, nothing staged:
REWRITERESP payload, what = 12
The RESP payload when what = 12: a REWRITE table summary, a four-byte header then twelve bytes per rule in table order.
QUERY what = 12 · RESP 4 + 12n bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x0C |
| 1 | flags | u8 | state bits below |
| 2 | gen | u8 | moves when the table changes, not on an identical re-send; see lifecycle |
| 3 | n | u8 | number of rules that follow, ≤32 |
| + | cls | u8 | per rule: the surface, as REWRITE |
| + | id | u16 | class address, little-endian |
| + | dir | u8 | 0 both, 1 IN, 2 OUT |
| + | action | u8 | the action, 0-8 |
| + | mlen | u8 | match length; here before off, the reverse of the command |
| + | off | u16 | patch offset, little-endian |
| + | plen | u16 | payload length, little-endian |
| + | hits | u16 | packets this rule matched as the top-ranked rule, a PASS included; saturating at 65535 |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | FULL: the last add was refused for capacity (a 33rd rule, or a payload the 2048-byte pool cannot hold, an overwrite included). The next change to the table clears it: an add, an overwrite, a removal or a clear. An identical re-send is no change, so a keepalive leaves it set |
A line carries no match, mask or payload bytes. Read those with QUERY(REWRITE_ENTRY). HEALTH sets REWRITE_ON (0x0100) while the list is non-empty.
The table is PC-owned session state on the same lifecycle as locks, so an empty list can mean the box released it. session counts that release. Library binding: query_rewrite.
One rule, DROP on EMIT endpoint 1, after 42 hits, with the table not full:
REWRITE_ENTRY RESP payload, what = 13
The RESP payload when what = 13: one rule in full. The QUERY carries [13][index], where index is the rule's line in RESP(REWRITE).
QUERY what = 13 · RESP 11 + 2 x mlen + plen bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x0D |
| 1 | index | u8 | requested index |
| 2 | cls | u8 | from here on, the REWRITE payload |
| 3 | id | u16 | little-endian |
| 5 | dir | u8 | 0 both, 1 IN, 2 OUT |
| 6 | state | u8 | always 1 |
| 7 | action | u8 | 0-8 |
| 8 | off | u16 | little-endian |
| 10 | mlen | u8 | 0-16 |
| 11 | match | u8[] | mlen bytes |
| 11+mlen | mask | u8[] | mlen bytes |
| 11+2 x mlen | payload | u8[] | the rest, plen bytes |
Past the first two bytes it is a REWRITE payload that reinstalls the rule. An index ≥n gets no reply. Library binding: query_rewrite_entry.
Rule 0, the DROP on EMIT endpoint 1:
PATCHESRESP payload, what = 14
The RESP payload when what = 14: a stored PATCH set summary, a three-byte header then seven bytes per patch in set order. An overwrite moves a patch to the end.
QUERY what = 14 · RESP 3 + 7n bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x0E |
| 1 | flags | u8 | state bits below |
| 2 | n | u8 | number of patches that follow, ≤16 |
| + | section | u8 | per patch, as PATCH |
| + | cfg | u8 | configuration index |
| + | index | u8 | interface number (REPORT) or string index (STRING) |
| + | offset | u16 | little-endian |
| + | len | u16 | length, little-endian |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | APPLIED: the clone is serving a non-empty patched set |
| b1 | 0x02 | PENDING: the stored set differs from the one the clone serves (in its patches, their bytes or their order). Causes: not yet applied, changed or emptied since, refused, or stored with the opt-in off |
| b2 | 0x04 | REFUSED: the stored set failed a check at the clone's last presentation and has not changed since, so the clone serves the descriptors unpatched. The LOG line names the check. A change to the set, CLEAR, a presentation that does not refuse it, or a detach clears it |
| b3 | 0x08 | FULL: the last add was refused for capacity (a 17th patch, or bytes the 1024-byte pool cannot hold, an overwrite included). The next change to the set, or CLEAR, clears it |
A line carries no patch bytes. Read those with QUERY(PATCH_ENTRY). HEALTH sets PATCH_ON (0x0200) while b0 is set.
The list is the stored set. The clone serves the same set while b1 is clear, and none while b0 is clear. With the real device unplugged, the list is the last attached device's set. Library binding: query_patches.
One applied patch, bcdDevice in the device descriptor (offset = 12, len = 2):
PATCH_ENTRY RESP payload, what = 15
The RESP payload when what = 15: one patch in full. The QUERY carries [15][entry], where entry is the patch's line in RESP(PATCHES).
QUERY what = 15 · RESP 7 + len bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x0F |
| 1 | entry | u8 | requested entry |
| 2 | section | u8 | from here on, the PATCH payload |
| 3 | cfg | u8 | configuration index |
| 4 | index | u8 | interface number (REPORT) or string index (STRING) |
| 5 | offset | u16 | little-endian |
| 7 | bytes | u8[] | the rest, len bytes |
Past the first two bytes it is a PATCH payload that restores the patch. An entry ≥n gets no reply. Library binding: query_patch_entry.
Entry 0, the bcdDevice patch setting 0x0200:
TRANSFORMSRESP payload, what = 16
The RESP payload when what = 16: the installed TRANSFORM table, a three-byte header then seven bytes per entry in install order.
QUERY what = 16 · RESP 3 + 7n bytes
Returns RESP
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | what | u8 | 0x10 |
| 1 | flags | u8 | b0 = the table is full and an entry was refused |
| 2 | n | u8 | number of entries that follow, ≤32 |
| + | op | u8 | per entry: 0 remap, 1 swap (as TRANSFORM) |
| + | sclass | u8 | per entry: source class, 0=button 1=key 2=media 3=axis |
| + | sid | u16 | source id within the class, little-endian |
| + | dclass | u8 | destination class |
| + | did | u16 | destination id, little-endian |
An entry mirrors the TRANSFORM frame field for field except the state byte, so a readback replays as commands.
| State | Reports as |
|---|---|
| An installed entry | One 7-byte line, under its (source, dest) key. |
| An entry the box refused | Absent. TRANSFORM is fire-and-forget, so this reply is the only confirmation an entry landed. |
| An entry the 32-slot table turned away | Absent, with flags b0 (full) set. |
| An entry whose destination this configuration doesn't declare | Present and unchanged. It is inert, not removed, and works again once the destination binds. |
| Bit | Mask | Set when |
|---|---|---|
| b0 | 0x01 | FULL: an entry was refused because the table already held 32. Nothing is evicted, and removing any entry clears the bit |
HEALTH sets its TRANSFORM_ON bit (0x0400) while the table is non-empty.
The table is PC-owned session state on the same lifecycle as locks and the catch subscription, so an empty list can mean the box released it. session counts that release. Library binding: query_transforms.
One entry, X and Y swapped (op = 1, source (axis 3, id 0), dest (axis 3, id 1)), with the table not full: