CLIP
Preload input and let the box play it back, frame by frameCLIP_APPEND preloads a sequence of per-frame entries into a ring on the box, then CLIP_CTRL drives the playback engine and the box drains one entry per native frame into the same injection state that INJECT and MOVE feed. Playback is box-clocked, so it carries no host scheduling jitter and no per-command send floor. Like INJECT it is field-generic and additive: one clip mixes mouse motion, buttons, keyboard, and media, each routed to its own interface, and follows movement riding and the emit rate. A clip needs a cloned mouse, whose native report tick is the box's frame clock; read the ring depth, playback state, and settings back with QUERY(CLIP).
A clip runs in one of two shapes, set by the retain flag on CLIP_SET.
- Streaming (default): the box frees each entry as it plays, so a real-time host keeps appending to the tail while the head drains. Good for open-ended or generated input; an emptied ring underruns.
- Retained: the box keeps entries after playing them, so once you've appended the whole clip and marked it
FINALIZEd you canSTART,RESTART, orloopit as many times as you like without re-appending. Good for a fixed macro you replay on a trigger.
control PC box (drains one entry per native frame)
| +-----------------------------------------+
| CLIP_APPEND [e0][e1] | ring [e0][e1][e2][e3][e4] ... |
| --------------------> | | |
| CLIP_SET loop/retain | | one entry / frame |
| CLIP_TRIGGER bind | v |
| CLIP_CTRL START | injection state --> mouse report |
| --------------------> | --> keyboard report |
| | --> media report |
| QUERY(CLIP) | |
| --------------------> | ring depth + state + settings |
| <-------------------- | |
| +-----------------------------------------+
box-clocked: host does no per-frame timing| Opcode | Command | Direction | Does |
|---|---|---|---|
0x12 | CLIP_APPEND | PC→box | append a batch of entries to the ring |
0x13 | CLIP_CTRL | PC→box | drive the playback engine (start, stop, pause, ...) |
0x14 | CLIP_SET | PC→box | set a clip setting (auto-lock, loop, retain) |
0x15 | CLIP_TRIGGER | PC→box | bind a physical edge to an engine verb |
Entry format
The bytes CLIP_APPEND carriesA clip is a byte stream of variable-length entries, little-endian. The first byte of each entry is a tag: 0x00 is a gap run, any other value is a content tick's flags. One entry is one native frame.
Emit nothing for count frames. The endpoint NAKs, byte-identical to an idle mouse, so a gap is the faithful way to hold still or pace between actions.
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | tag | u8 | 0x00 |
| 1 | count | u16 | frames to NAK, little-endian |
A motion delta and/or a list of edges applied on one frame. The flags byte (nonzero, so it can't be mistaken for a gap tag) selects which fields follow.
| Offset | Field | Type | Present when |
|---|---|---|---|
| 0 | flags | u8 | always; OR of the bits below |
| + | dx, dy | i16 × 2 | flags & XY (0x01), cursor delta |
| + | wheel | i16 | flags & WHEEL (0x02) |
| + | n | u8 | flags & EDGES (0x04), edge count (max 8) |
| + | edges | n × 4 bytes | each edge is [class u8][id u16][action u8] |
An edge reuses INJECT's tuple, so one clip drives every input class.
class | Value | id is |
|---|---|---|
| button | 0 | a button id (0=Left .. 4=Side2) |
| key | 1 | a HID keycode (0xE0-0xE7 = modifier) |
| media | 2 | a 16-bit Consumer usage |
| Action | Value | Effect |
|---|---|---|
| soft-release | 0 | Drop the override; a physical hold stays active. |
| press | 1 | Force the usage active. |
| force-release | 2 | Force it inactive, masking a physical hold too. |
An edge is a level: it sticks until a later tick changes it, and the box NAKs while it is held still. Motion (dx/dy/wheel) is a per-frame delta.
Set several flag bits and the fields stack in a single tick, so a move and a press land on the same frame and the PC sees one report. That is what keeps "aim and hold fire" faithful: motion every frame, the fire button pressed on the frame it goes down.
05 0A 00 FC FF 01 00 00 00 01 flags=XY|EDGES dx=+10 dy=-4 n=1 edge[class=0 button, id=0 Left, action=1 press]
Entries play out one per frame, left to right.
| Frame | Entry | The PC sees |
|---|---|---|
0 | motion | cursor moves |
1 | Left press | left button down |
2-4 | gap 3 | nothing sent (NAK); left stays down |
5 | Left release | left button up |
6 | motion | cursor moves |
7 | key A press | A down |
The bytes for three example entries: move up-right, press A, idle 5 frames.
01 0A 00 F6 FF flags=XY, dx=+10 dy=-10 04 01 01 04 00 01 flags=EDGES n=1 edge[class=1 key, id=0x04 'A', action=1 press] 00 05 00 tag=gap, count=5 (NAK 5 frames)
CLIP_APPEND
Fill the ringAppend a batch of whole entries to the tail of the ring. Send it while stopped to preload, or while playing (streaming mode) to keep topping up in real time. Opcode 0x12.
CLIP_APPEND 0x12 · payload = one or more whole entries
Fire-and-forget
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | entries | bytes | a whole number of entries, back to back (up to the 512-byte frame limit) |
The frame SEQ doubles as an append sequence number: the box expects each CLIP_APPEND to be the previous SEQ plus one. Because the link is fire-and-forget, a lost frame shows up as a SEQ gap, and the box marks the clip faulted in QUERY(CLIP) so the host re-syncs (CLEAR, then rebuild) instead of playing a stream with a hole in it. Pack whole entries per frame; never split one entry across two appends.
An append that doesn't fit the ring is dropped whole and faults the clip, never written as a partial entry that would desync the stream. Keep an append under QUERY(CLIP)'s free bytes to avoid it: in streaming mode the box drains from the head while you append to the tail, so a real-time host tops up as free opens back up.
the ring, read by QUERY(CLIP):
free buffered (total)
+----------------+----------------------------------------+
| (append | [e5][e6][e7][e8][e9] ... | --> drained
| here, <= | buffered, not yet played | 1 / frame
| free) | |
+----------------+----------------------------------------+
append > free --> dropped whole + clip faultedLibrary binding: ClipBuilder + append, which splits a large clip into whole-entry frames for you.
Append one content tick, cursor dx = 10 (a 5-byte entry, so LEN = 5):
+--------+--------+--------+--------+--------+--------+--------+--------+
| A5 | 12 | 00 | 05 00 | 01 | 0A 00 | 00 00 | lo hi |
+--------+--------+--------+--------+--------+--------+--------+--------+
| SOF | TYPE | SEQ | LEN | flags | dx | dy | CRC16 |
+--------+--------+--------+--------+--------+--------+--------+--------+
^ append seq \- one XY content tick --/CLIP_CTRL
Drive the playback engineOne byte of op selects an engine verb. Opcode 0x13. There are no args: settings live on CLIP_SET, so a control frame is just the verb.
CLIP_CTRL 0x13 · payload [op u8]
Fire-and-forget
| op | Name | Effect |
|---|---|---|
0 | START | play from the ring head, applying the autolock setting |
1 | STOP | halt playback and release the clip's auto-lock; buffered entries survive in retained mode |
2 | PAUSE | freeze the playhead where it is; held levels stay down, motion stops |
3 | RESUME | continue a paused clip from where it stopped |
4 | RESTART | jump back to the head and play from the top (retained clip) |
5 | TOGGLE | start if stopped, stop if playing |
6 | CLEAR | stop and empty the ring, dropping every buffered entry and clearing a fault |
7 | FINALIZE | mark the buffered clip complete; the box stops treating an emptied ring as an underrun |
Ops 0-5 (START through TOGGLE) double as theaction byte a CLIP_TRIGGER fires on a physical edge; CLEAR and FINALIZE are host-only.
QUERY(CLIP) reports one of four states.
| Value | State | Means |
|---|---|---|
0 | idle | not playing; ring may hold a retained clip |
1 | playing | draining one entry per frame |
2 | paused | frozen mid-clip by PAUSE, holding its levels |
3 | faulted | a SEQ gap or overflow desynced the stream; CLEAR and rebuild |
In streaming mode, if the ring drains with no FINALIZE, the box idles (NAKs, holding its levels) and stays playing until you refill it; a topping-up host or any keepalive holds the clip alive. A finalized clip ends when the ring empties, or replays from the head if loop is set.
STOP an explicit STOP or CLEAR op silence a full 1 s of control-PC silence RESET a RESET command detach the cloned mouse unplugs link loss the inter-chip link drops
Each halts playback and releases the clip's lock; a hard stop (silence, RESET, detach, link loss) also flushes the ring. The 1 s safety net and a RESET reach a clip like any other injection. With movement riding on, clip motion rides native reports and is additive to the user's own movement, so the frame-exact use case runs riding off.
Library binding: Device::clip().
Start playback (op = 0, a single-byte payload so LEN = 1):
+--------+--------+--------+--------+--------+--------+ | A5 | 13 | 00 | 01 00 | 00 | lo hi | +--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | op | CRC16 | +--------+--------+--------+--------+--------+--------+
Every op has the same shape; only the op byte changes.
CLIP_SET
Set a clip settingSet one of the clip's settings, OPTION-shaped: an id byte picks the setting, a value byte carries it. A setting sticks until you change it or the clip is torn down; read them all back with QUERY(CLIP). Opcode 0x14.
CLIP_SET 0x14 · payload [id u8][value u8]
Fire-and-forget
| id | Setting | value | Effect |
|---|---|---|---|
0 | autolock | class bitmask | the physical-input classes START locks while playing (below) |
1 | loop | 0 / 1 | a finalized clip replays from the head instead of ending |
2 | retain | 0 / 1 | keep entries after playing so START / RESTART can replay them |
The autolock value is a bitmask of the physical-input classes START locks while the clip plays (clip-owned, released on STOP), leaving the ones you don't name free. A host LOCK is untouched. 0 = no auto-lock; 0x1F = every class.
| Bit | Mask | Locks |
|---|---|---|
b0 | 0x01 | the X and Y aim axes |
b1 | 0x02 | the wheel |
b2 | 0x04 | every mouse button |
b3 | 0x08 | every keyboard key |
b4 | 0x10 | every media usage |
Library binding: set_autolock, set_loop, set_retain.
Turn looping on (id = 1, value = 1, so LEN = 2):
+--------+--------+--------+--------+--------+--------+--------+ | A5 | 14 | 00 | 02 00 | 01 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | id | value | CRC16 | +--------+--------+--------+--------+--------+--------+--------+
CLIP_TRIGGER
Bind a physical edge to an engine verbFire a CLIP_CTRL verb the instant the user physically moves an input (the same physical edge CATCH reports), with no host round-trip, so even the first emitted frame is box-timed. Triggers are a LOCK-shaped managed set of up to eight bindings, keyed by (class, id, edge). Opcode 0x15.
CLIP_TRIGGER 0x15 · payload [class u8][id u16][edge u8][action u8][flags u8]
Fire-and-forget
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | class | u8 | input class (below) |
| 1 | id | u16 | usage within the class, little-endian; 0xFFFF = any |
| 3 | edge | u8 | which edge fires (below) |
| 4 | action | u8 | the CLIP_CTRL op to fire, 0-5 |
| 5 | flags | u8 | bit0 present, bit1 consume (below) |
class | Value | id is |
|---|---|---|
| button | 0 | a button id |
| key | 1 | a HID keycode |
| media | 2 | a Consumer usage |
| any | 0xFF | ignored (any input fires) |
| Edge | Value | Fires on |
|---|---|---|
| both | 0 | press and release |
| press | 1 | the physical press |
| release | 2 | the physical release |
| Bit | Mask | Effect |
|---|---|---|
b0 | 0x01 | present: set to add or replace the binding, clear to remove it |
b1 | 0x02 | consume: swallow the physical edge so the app never sees it |
A binding is keyed by (class, id, edge), so re-sending the same key replaces it and clearing present removes it. To wipe the whole set in one frame send the clear-all sentinel: class = 0xFF, id = 0xFFFF, edge = 0 (both), flags = 0. Preload the ring (and, for a replayable macro, mark it FINALIZEd) before you bind.
Library binding: bind, unbind, clear_triggers.
Bind the A key's press to START (class = 1, id = 0x04, edge = 1 press, action = 0 start, flags = 0x01 present):
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 15 | 00 | 06 00 | 01 | 04 00 | 01 | 00 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | class | id | edge | action | flags | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+