# Medius Documentation > Mouse-passthrough firmware for MAKCU-class boxes: an open binary control protocol, byte-exact device behavior, and the medius Rust library. Source: https://medius.k4tech.net --- # Medius Native API _The binary control protocol_ Medius is replacement firmware for MAKCU-class USB input-passthrough boxes plus an open binary control protocol. The box sits inline between a USB device and a PC: the real device (mouse, keyboard, or combo) passes through unchanged while your program injects input of its own (cursor and buttons for a mouse, keys and media for a keyboard) over a separate USB-serial link. Drive it from any language; the Rust [library](/library.md) is the official client. | Property | Value | | --- | --- | | Firmware version | `3.0.0` | | Protocol version | `3` | | Transport | 4 Mbaud, framed-only ([CH343](https://www.wch-ic.com/products/CH343.html)) | | USB ID | VID `0x1A86` / PID `0x55D3` | | Delivery | Fire-and-forget; [`QUERY`](/native/commands/requests.md#requests) → [`RESP`](/native/commands/requests.md#resp) is the only round-trip | Before you talk to the box: | Topic | What to know | | --- | --- | | Protocol version | These pages describe version `3`. Confirm it during the [handshake](/native/connection.md#handshake) from the `proto_ver` field of the [`VERSION`](/native/commands/requests.md#version) reply; a different value means firmware these pages don't cover. | | Wire format | The box speaks [framed binary](/native/frame.md) from the first byte. No startup baud, no text mode. | | Finding the port | Scan for the CH343's VID/PID pair to locate the box's serial port. | | Correlation | [Fire-and-forget](/native/injection.md#fire-and-forget) has no ack or echo. A [`QUERY`](/native/commands/requests.md#requests) is correlated to its [`RESP`](/native/commands/requests.md#resp) by [`SEQ`](/native/frame.md#seq). | ## Overview - [Quickstart](/native/quickstart.md): Open the port and inject - [Architecture](/native/architecture.md): Clone, passthrough, inject - [Hardware](/native/hardware.md): Three USB ports and the chips ## Protocol - [Transport](/native/transport.md): 4 Mbaud, framed-only - [Connection](/native/connection.md): Handshake and hello - [Frame Format](/native/frame.md): SOF, type, CRC16 - [Injection Model](/native/injection.md): Accumulator and emission ## Commands - [Inject](/native/commands/inject.md): Press buttons, keys, media - [Move](/native/commands/move.md): Cursor and wheel - [Lock](/native/commands/lock.md): Block a physical input - [Catch](/native/commands/catch.md): Stream physical input - [Clip](/native/commands/clip.md): Buffered clip playback - [Requests](/native/commands/requests.md): QUERY and its RESP, all ten selectors - [LED](/native/commands/led.md): Override the status LEDs - [Admin](/native/commands/admin.md): RESET, REBOOT, LOG - [Option](/native/commands/option.md): Imperfect clones, movement riding, emit pacing - [Usage IDs](/native/commands/usage.md): Button, key, media numbers ## Reference - [Flashing](/native/flashing.md): Reboot to ROM and flash - [Troubleshooting](/native/troubleshooting.md): Common problems and fixes --- # Quickstart _Plug in and send your first command_ A Medius box sits inline between a USB device and a PC. The real device passes through, and your program sends input of its own (cursor and buttons for a mouse, keys and media for a keyboard) over a USB-serial link. Below: wire it, open the link, send one movement command. Every step is byte-exact firmware behavior. The box talks in [frames](/native/frame.md), small fixed-shape packets that each carry one command. Most are [fire-and-forget](/native/injection.md#fire-and-forget): you send and move on with no reply. The exception is [`QUERY`](/native/commands/requests.md#requests), which gets one answer back. ## Wire it up _The safe 3-port layout_ - `USB1` ([clone](/native/hardware.md)) → game PC - `USB2` (control) → control PC - `USB3` (mouse) → real mouse The clone copies the real mouse's USB identity, so the game PC sees the same device it would if the mouse were plugged in directly. > **Danger** > > Never connect `USB1` and `USB3` to the same machine. The `USB3` 5V rail can't be pulled low in firmware, so wiring both to one machine back-feeds power and can force a shutdown and drain the battery. One game PC, one control PC, never the same machine. Full port map and hazard detail on [Hardware](/native/hardware.md). ## Open the link _Open at the fixed baud, speak binary_ Open `/dev/ttyACM0` (Linux) or `COMx` (Windows) at `4,000,000` baud, `8N1`, and speak binary immediately. There's no `115200` handshake and no baud-switch frame. [`RESP`](/native/commands/requests.md#resp) is the box's reply to a question. To learn the protocol version: 1. On first contact the box sends one [`RESP(VERSION)`](/native/commands/requests.md#version) on its own, with [`SEQ`](/native/frame.md#seq) `0`, your ready signal. Or send [`QUERY(VERSION)`](/native/commands/requests.md#version) yourself. 2. Read `proto_ver` from the `VERSION` payload, the one-byte protocol version the firmware speaks. 3. Check `proto_ver == 3` before trusting the commands here. This documents version `3`. Serial framing detail on [Transport](/native/transport.md#serial); handshake and presence detail on [Connection](/native/connection.md). ## Send a MOVE _Relative cursor movement_ [`MOVE`](/native/commands/move.md#move) nudges the cursor on the PC. Its frame has the standard shape, laid out byte-by-byte below. - [`SEQ`](/native/frame.md#seq) is a number you pick and increment per frame. For a [`QUERY`](/native/commands/requests.md#requests) the box echoes it in the matching [`RESP`](/native/commands/requests.md#resp) so you can pair reply to request; here `0`. - `CRC16` lets the box reject a corrupted frame. It is [CRC16-CCITT](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) (polynomial `0x1021`, initial value `0xFFFF`) over `TYPE | SEQ | LEN | PAYLOAD`, stored little-endian. ```python import struct def crc16_ccitt(data): crc = 0xFFFF for b in data: crc ^= b << 8 for _ in range(8): crc = (crc << 1) ^ 0x1021 if crc & 0x8000 else crc << 1 crc &= 0xFFFF return crc def encode(type, seq, payload): head = bytes([type, seq]) + struct.pack('`: start byte, opcode `0x01`, sequence, length, the `motion`/`dx`/`dy` payload, then the checksum. The byte-by-byte breakdown is on [`MOVE`](/native/commands/move.md#move), the frame format on [Frame Format](/native/frame.md). ## Confirm _Check the chain before relying on injection_ [Injection](/native/injection.md) is the input your program sends on top of the real mouse's passthrough. Confirm the chain (real mouse → box → PC) is live before you rely on it: send [`QUERY(HEALTH)`](/native/commands/requests.md#health), read the `flags` byte from the [`RESP`](/native/commands/requests.md#resp), and check these bits are set. | Flag | Mask | Means | | --- | --- | --- | | `LINK_UP` | `0x01` | The link to the host chip is up. | | `MOUSE_ATTACHED` | `0x02` | A mouse is on `USB3`. | | `CLONE_CONFIGURED` | `0x04` | The game PC has enumerated the clone. | A flag is set when `(flags & mask)` is non-zero. With all three set, your [`MOVE`](/native/commands/move.md#move) reaches the game PC. The full byte is on [HEALTH](/native/commands/requests.md#health). ## Skip the framing For a ready-made client, `cargo add medius`. See the [library](/library.md). --- # How it fits together _Mouse, box, and PC_ The box has two chips joined by an internal link, plus a third port for your program. The clone is a copy of the real mouse's USB identity. | Part | Role | | --- | --- | | Host chip | Reads the real mouse. | | Device chip | Presents the clone to the PC and merges your input into what it reports. | | Link | Passes data between the two chips. | ``` real mouse game PC | ^ | USB3 | USB1 v | +-----------+ +-----------+ | host chip | ---- link ---> |device chip| +-----------+ +-----------+ ^ | USB2 (CH343 serial) | control PC ``` Three connections: | Port | Connects to | | --- | --- | | [`USB3`](/native/hardware.md) | The real mouse. | | [`USB1`](/native/hardware.md) | The PC receiving the mouse. | | [`USB2`](/native/transport.md) | The program driving the box, over a [CH343 serial link](/native/transport.md). | That program speaks the [binary protocol](/native/frame.md), sending [movement](/native/commands/move.md), [button](/native/commands/inject.md), and [scroll](/native/commands/move.md#wheel) commands. ## What the PC sees _A real mouse, plus your input_ To the PC the clone is the same model of mouse, with the same buttons and capabilities. Your input adds to the real mouse's input rather than replacing it ([Injection Model](/native/injection.md)), so the physical mouse keeps working either way. | When your program | The box | | --- | --- | | Sends input | Layers your movement, scroll, and button state onto the real mouse. | | Goes quiet | Returns to plain passthrough, just a wire, per the [safety rule](/native/injection.md#safety). | ## Two computers _A separate driver host_ Your program on [`USB2`](/native/transport.md) runs on a different computer from the one receiving the mouse on [`USB1`](/native/hardware.md). > **Note** > > See [Hardware](/native/hardware.md) for the ports and how to wire them, including the one pairing to avoid. --- # Ports _The three USB ports and how to cable them_ Wire the box once at first plug-in; after that everything is software. Inside are two [ESP32](https://www.espressif.com/en/products/socs)\-S3 microcontrollers and a [`CH343`](https://www.wch-ic.com/products/CH343.html) USB-serial bridge. Your program only ever speaks to the `CH343` serial port; the two chips talk over an internal 5 Mbaud link you never touch, separate from the 4 Mbaud control link. | Port | Connects to | Role | | --- | --- | --- | | `USB1` | Game PC | The clone ([device chip](/native/architecture.md)), a copy of the mouse's USB identity, so the PC sees the same device it would if the mouse were plugged in directly. | | `USB2` | Control PC | [CH343](/native/transport.md) serial control, the `/dev/ttyACM*` port your program opens | | `USB3` | Mouse | Real mouse ([host chip](/native/architecture.md)) | ## USB3 power hazard _The one wiring pairing to avoid_ > **Danger** > > ⚠️ `USB1` and `USB3` must never both connect to the same machine. > > The `USB3` 5V rail can't be pulled low in firmware, so wiring both to one machine back-feeds power and can force a shutdown or drain the battery. Keep them on separate machines per the port table above. ## Disconnecting _No power switch, just unplug_ The box is USB bus-powered: no battery, no power button, nothing to power down. Turning it off means unplugging it, and that is safe at any moment. Injected input never outlives the program that sent it, so a button or move can't get stuck. | You unplug | What happens | | --- | --- | | `USB2` (control), or the program stops | After `1 s` of silence the box clears all [injection](/native/injection.md) and falls back to pure passthrough. The real mouse keeps working. | | `USB1` (clone) | The game PC sees an ordinary mouse unplug; the box drops its injection state. | | `USB3` (mouse) | The box tears down the captured mouse cleanly and reports it detached. | To return to passthrough instantly instead of waiting out the [silence timeout](/native/injection.md#safety), send a [`RESET`](/native/commands/admin.md#reset) before unplugging (the library's [`reset`](/library/admin.md#reset)). Dropping the [`Device`](/library/guides/connection.md#release) stops its threads, after which the same timeout clears the box. Port order otherwise does not matter. > **Warning** > > The one rule: `USB1` and `USB3` must not share a machine at any point, plugging in or unplugging. See the [power hazard](/native/hardware.md#hazard). --- # Transport _USB-serial port_ The box enumerates as a USB-serial device, so it appears as an ordinary serial port. Everything travels over that port as raw bytes. Open the port at the fixed baud, then send [frames](/native/frame.md) and confirm the box with the [handshake](/native/connection.md). ## Serial link _Baud and framing_ Fixed `4,000,000` baud (4 Mbaud), no negotiation. Open the port at that exact baud and start sending bytes; any other baud fails. The box speaks [framed binary](/native/frame.md) from the first byte. There is no legacy startup path: - No ASCII console. - No `115200` startup step. - No baud-switch frame. - Baud is never saved on the box, so a bad setting can't lock you out. ## USB identity _WCH CH343 bridge_ A [WCH](https://www.wch-ic.com) [`CH343`](https://www.wch-ic.com/products/CH343.html) chip does the USB-to-serial conversion. Match on its vendor and product IDs to pick the box out from other serial devices. | Property | Value | | --- | --- | | VID | `0x1A86` | | PID | `0x55D3` | | Baud | `4,000,000` | | Framing | `8N1` | | Linux path | `/dev/ttyACM*` | | Windows path | `COMx` | `8N1` is 8 data bits, no parity, 1 stop bit. > **Note** > > See [Connection](/native/connection.md) for the handshake and [Frame Format](/native/frame.md) for the wire format. --- # Connection & handshake _Open, find, and handshake_ The handshake confirms the device on the serial port is a Medius box and speaks a protocol version you understand: one request, one reply. Open the port and start talking. - No baud negotiation. - No login. - No `115200` startup step or baud-switch command. ## Handshake _One round-trip to confirm the box_ 1. Open the serial port at `4,000,000` baud ([Transport](/native/transport.md)). The box speaks [framed binary](/native/frame.md) from the first byte. 2. Catch the unsolicited hello the box sends on its own ([below](/native/connection.md#hello)), or send a [`QUERY(VERSION)`](/native/commands/requests.md#version) yourself. Both produce the same [`RESP(VERSION)`](/native/commands/requests.md#version) frame. 3. Read `proto_ver` from that reply and check it equals `3`. | Reply | Meaning | | --- | --- | | `proto_ver == 3` | Speaks the protocol these pages describe. | | `proto_ver != 3` | Speaks a protocol these pages don't cover; don't assume the commands behave as described. | | No reply | Not a Medius box, or the port or baud is wrong. | #### THE REPLY: RESP(VERSION) The first byte echoes the `what` selector you asked for (the byte that chose which thing to query), then the protocol and firmware version, the box MAC, and the box name follow. Full detail on the [Requests](/native/commands/requests.md#version) page. | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | the selector byte, echoed back; `0x00` = `VERSION` | | 1 | `proto_ver` | `u8` | protocol version, expected `3` | | 2 | `fw_major` | `u8` | firmware major | | 3 | `fw_minor` | `u8` | firmware minor | | 4 | `fw_patch` | `u8` | firmware patch | | 5 | `mac` | `u8[6]` | the box MAC, a stable per-box id | | 11.. | `name` | `ascii` | the box's human-readable name (may be empty), delimited by the frame `LEN` | ## The ready hello _Unsolicited RESP(VERSION) on link-up_ The box sends one [`RESP(VERSION)`](/native/commands/requests.md#version) on its own as soon as its serial link is up. Treat it as "box is here and ready" and skip your own [`QUERY(VERSION)`](/native/commands/requests.md#version). | Trigger | When it fires | | --- | --- | | Power-on | Once, as the box boots. | | First contact | On the first valid frame after a program opens the port, so a program that connects after the power-on hello still gets one. | The hello carries [`SEQ=0`](/native/frame.md#seq) since no request prompted it, and its payload is identical to a queried reply. > **Note** > > The [medius library](/library/connection.md) does all of this inside [`open`](/library/connection.md#open) and [`find`](/library/connection.md#open): it sends [`QUERY(VERSION)`](/native/commands/requests.md#version), retries a few times, and checks `proto_ver == 3` before handing you a working connection. --- # Frame format _The one packet shape_ Every message, both directions, is a frame with one fixed shape. The whole protocol is frames; each command is the same machinery with a different opcode and payload. ```text [SOF 0xA5][TYPE u8][SEQ u8][LEN u16 LE][PAYLOAD ≤512][CRC16 u16 LE] ``` | Field | Bytes | Notes | | --- | --- | --- | | `SOF` | 1 | start-of-frame marker, always `0xA5` | | `TYPE` | 1 | the opcode | | `SEQ` | 1 | per-frame sequence number | | `LEN` | 2 | payload byte count, little-endian | | `PAYLOAD` | 0-512 | the command's data; empty for argument-free commands | | `CRC16` | 2 | checksum over the frame body | Max payload 512 bytes, so a frame is at most 519. Every multi-byte number is little-endian: the 16-bit value `100` is the bytes `64 00`. ## Sequence numbers _Matching a reply to its request_ `SEQ` is a one-byte counter you set per frame, typically incrementing and wrapping at 255. | Command | Role of `SEQ` | | --- | --- | | Ordinary commands | Only helps you spot a dropped frame. | | [`QUERY`](/native/commands/requests.md#requests) | The box copies your `SEQ` onto the [`RESP`](/native/commands/requests.md#resp), so with several requests outstanding the matching `SEQ` tells you which reply answers which. | ## Opcodes _The TYPE byte_ The opcodes run from `0x01` to `0x15`. Four values are reserved, retired by the unified-input collapse. An unrecognised opcode is ignored harmlessly, which keeps newer and older firmware compatible. | Opcode | Name | Direction | Payload | Reply | | --- | --- | --- | --- | --- | | `0x01` | [`MOVE`](/native/commands/move.md#move) | PC→box | 3 or 5 bytes | none | | `0x02` | reserved | \- | \- | \- | | `0x03` | [`INJECT`](/native/commands/inject.md#inject) | PC→box | 4 bytes | none | | `0x04` | [`RESET`](/native/commands/admin.md#reset) | PC→box | 0 bytes | none | | `0x05` | [`QUERY`](/native/commands/requests.md#requests) | PC→box | 1 byte | [`RESP`](/native/commands/requests.md#resp) | | `0x06` | [`RESP`](/native/commands/requests.md#resp) | box→PC | varies | none | | `0x07` | [`REBOOT`](/native/commands/admin.md#reboot) | PC→box | 1 byte | none | | `0x08` | [`LOG`](/native/commands/admin.md#log) | box→PC | varies | none | | `0x09` | [`LED`](/native/commands/led.md#led) | PC→box | 3 bytes | none | | `0x0A` | [`LOCK`](/native/commands/lock.md#lock) | PC→box | 5 bytes | none | | `0x0B` | [`CATCH`](/native/commands/catch.md#catch) | PC→box | 1 byte | none | | `0x0C` | [`MOTION_EVENT`](/native/commands/catch.md#motion-event) | box→PC | 6 bytes | none | | `0x0D` | reserved | \- | \- | \- | | `0x0E` | reserved | \- | \- | \- | | `0x0F` | [`USAGE_EVENT`](/native/commands/catch.md#usage-event) | box→PC | varies | none | | `0x10` | reserved | \- | \- | \- | | `0x11` | [`OPTION`](/native/commands/option.md#option) | PC→box | varies | none | | `0x12` | [`CLIP_APPEND`](/native/commands/clip.md#append) | PC→box | varies | none | | `0x13` | [`CLIP_CTRL`](/native/commands/clip.md#ctrl) | PC→box | 1 byte | none | | `0x14` | [`CLIP_SET`](/native/commands/clip.md#set) | PC→box | 2 bytes | none | | `0x15` | [`CLIP_TRIGGER`](/native/commands/clip.md#trigger) | PC→box | 6 bytes | none | `0x02`, `0x0D`, and `0x0E` were the old `WHEEL`, `KEY`, and `CONSUMER` commands, folded into [`MOVE`](/native/commands/move.md#move) (motion-tagged) and [`INJECT`](/native/commands/inject.md#inject) (class-tagged). `0x10` was `CONS_EVENT`, folded into the class-tagged [`USAGE_EVENT`](/native/commands/catch.md#usage-event). The old numbers are never reused. ## Checksum & integrity _Rejecting corrupted frames_ The last two bytes are a [CRC16-CCITT](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) checksum over `TYPE | SEQ | LEN | PAYLOAD` (everything but `SOF` and the checksum itself), stored little-endian. On a mismatch the box silently drops the frame and resyncs at the next `0xA5`, so corrupted frames are never acted on. | Parameter | Value | | --- | --- | | Polynomial | `0x1021` | | Initial value | `0xFFFF` | | Bit reflection | None | | Final XOR | None | ```python def crc16_ccitt(data): crc = 0xFFFF for b in data: crc ^= b << 8 for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF return crc def encode_frame(type, seq, payload): body = bytes([type, seq]) + len(payload).to_bytes(2, "little") + payload crc = crc16_ccitt(body) return bytes([0xA5]) + body + crc.to_bytes(2, "little") ``` ## Example: a MOVE frame A cursor [`MOVE`](/native/commands/move.md#move) of `dx = 100`, `dy = 0`. - Opcode `0x01`. - Payload is the `motion` byte (`00` = cursor) then the two 16-bit values `dx` and `dy` (`64 00`, `00 00`). - `LEN` is `05 00`. ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 01 | 00 | 05 00 | 00 | 64 00 | 00 00 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | motion | dx | dy | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` The CRC bytes are the little-endian `crc16_ccitt` of `01 00 05 00 00 64 00 00 00`. Compute them rather than copying a literal. --- # Injection model _A set of fields, two verbs, added on top of the user's input_ A connected device is a set of _fields_: each is an _Axis_ (relative motion: X, Y, wheel) or a _Usage_ (a momentary button, key, or media control). [`MOVE`](/native/commands/move.md#move) drives an Axis, [`INJECT`](/native/commands/inject.md#inject) drives a Usage, and a Usage is one `(class, id)` shape for buttons, keys, and media alike. | Device | Axes ([`MOVE`](/native/commands/move.md#move)) | Momentary ([`INJECT`](/native/commands/inject.md#inject)) | | --- | --- | --- | | mouse | cursor X/Y, wheel | buttons | | keyboard | none | keys, modifiers | | media | none | volume, play/pause, ... | Whatever you send is _added on top of_ the user's own input, never replacing it: ``` physical input (real device) --+ +--> one combined report --> game PC injected input (your program) --+ ``` | You send | The PC sees | | --- | --- | | a `MOVE` while the real mouse moves | The sum of both. | | an `INJECT` press while the user holds nothing | The injected press. | | nothing | Only the real device. | > **Note** > > [`INJECT`](/native/commands/inject.md#inject), [`LOCK`](/native/commands/lock.md#lock), and [`CATCH`](/native/commands/catch.md#catch) all name an input with the same Axis and Usage vocabulary, so one `(class, id)` works across inject, lock, and catch. ## Fire-and-forget _No per-command acknowledgement_ Command frames get no echo and no acknowledgement, so you can stream input fast (up to about one command per millisecond). The exception is [`QUERY`](/native/commands/requests.md#requests), which returns a [`RESP`](/native/commands/requests.md#resp). Correctness comes from three places, not per-command tracking: | Mechanism | What it does | | --- | --- | | frame [checksum](/native/frame.md#crc) | Drops corrupted frames. | | [safety rules](/native/injection.md#safety) | Keep a dropped command from leaving the box stuck. | | [`HEALTH`](/native/commands/requests.md#health) | Reads the box's actual state. | A lost movement frame costs one millisecond of motion; the next frame carries on. ## What the box tracks _Pending motion and held usages_ | State | What it holds | | --- | --- | | accumulator | A running total of sent motion and scroll not yet delivered to the PC. Each [`MOVE`](/native/commands/move.md#move), cursor or wheel, adds in; the box drains it into outgoing reports. | | usage override | Per usage (button, key, or media), whether the box forces it active, forces it inactive, or leaves it to the real device. Set by the [`INJECT`](/native/commands/inject.md#inject) actions: press forces active, force-release forces inactive, soft-release hands it back. | A report can only carry a limited movement size. A large injected move sends what fits and keeps the remainder in the accumulator for the next report. Nothing is clipped (`total seen = total sent`), just spread over as many reports as it takes. ## When the box sends a report _At the mouse's own report rate, only on activity_ | When… | The box sends | | --- | --- | | the real mouse reported | The real movement plus whatever's drained from the accumulator, with buttons combining the physical state and your overrides. | | the real mouse was still, but you have motion pending | A report carrying just the drained accumulator, paced to the mouse's own report rate (not one every millisecond). | | an [`INJECT`](/native/commands/inject.md#inject) or [`RESET`](/native/commands/admin.md#reset) changed a usage | One report reflecting the new state. | Otherwise the box sends nothing, like a real mouse sitting idle. A held usage is a single report (the edge), then silence until it changes. ## Safety _Injected state can't trap the real device_ A [force-release](/native/commands/inject.md#inject) always wins: it clears an injected hold and masks a physical press, so any held input can always be forced back to inactive. The box also clears all injection if your program goes quiet, dropping every override and pending move and returning to plain passthrough. Any of these resets it: | Trigger | What happens | | --- | --- | | silence timeout | No valid frame arrives within the timeout (default `1000 ms`), so a crash while holding a button releases it a second later. | | link drop | The link to the host chip drops. | | mouse unplugged | The real mouse is detached, so there's nothing left to inject into. | | [`RESET`](/native/commands/admin.md#reset) | You send the reset command explicitly. | To hold an injected button deliberately, keep the link busy: any valid frame resets the timer, so a periodic [`QUERY(HEALTH)`](/native/commands/requests.md#health) suffices. > **Note** > > The [medius library](/library/lifecycle.md) automates this: it sends keepalives while you hold something, and reconnects and re-applies your state if the link drops. --- # Inject _Press and release any input_ [`INJECT`](/native/commands/inject.md#inject) sets a momentary input on top of whatever the user is physically doing: a mouse [button](/native/commands/inject.md#button), a keyboard [key](/native/commands/inject.md#key) or modifier, or a [media](/native/commands/inject.md#media) key. One verb covers all three, tagged by a `class` byte, so the same press / release logic works for every input class. The continuous axes (cursor and wheel) have their own verb, [`MOVE`](/native/commands/move.md#move). ## INJECT _Momentary-usage override_ `INJECT` sets a per-usage [override](/native/injection.md#state), the box's own held decision layered over the physical input. The `class` byte picks the input kind, `id` picks the usage within that class, and `action` is the same tri-state for all three. [Opcode](/native/frame.md#opcodes) `0x03`. ```text INJECT 0x03 · payload 4 bytes ``` _Fire-and-forget_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `class` | `u8` | 0=button 1=key 2=media (the input kind) | | 1 | `id` | `u16` | the usage within the class, little-endian (see each class below) | | 3 | `action` | `u8` | 0=soft-release 1=press 2=force-release | #### CLASSES | Class | Value | `id` is | | --- | --- | --- | | [button](/native/commands/inject.md#button) | `0` | a semantic [button id](/native/commands/usage.md#buttons) (0=Left .. 4=Side2) | | [key](/native/commands/inject.md#key) | `1` | a [HID keyboard usage](/native/commands/usage.md#keycodes) (0xE0-0xE7 = modifier) | | [media](/native/commands/inject.md#media) | `2` | a 16-bit [Consumer usage](/native/commands/usage.md#consumer) | #### ACTIONS | Action | Value | Effect | | --- | --- | --- | | press | `1` | Force the usage active regardless of physical state. | | soft-release | `0` | Drop our override (whether it was a press or a force-release); a physical hold stays active. | | force-release | `2` | Force the usage inactive, masking a physical hold too. The release the [safety auto-clear](/native/injection.md#safety) uses. | #### RESULT THE PC SEES The two releases differ only when the user is physically holding the same input: | Action | User holds nothing | User is holding it | | --- | --- | --- | | `press` | active | active | | `soft-release` | inactive | active (physical wins) | | `force-release` | inactive | inactive (masks physical) | #### RULES ``` additive layers over the user at the same merge point as MOVE; never evicts the user's own input no click no firmware click or chord; send a press, then your own client-timed soft-release RESET releases every override at once ``` > **Warning** > > A usage the cloned device can't report is a silent no-op. Check [`CAPS`](/native/commands/requests.md#caps) before you rely on it. Library binding: [`inject`](/library/inject.md#inject). ## class = button _Mouse button override_ With `class = 0`, `id` is a semantic [button id](/native/commands/usage.md#buttons) (0=Left, 1=Right, 2=Middle, 3=Side1, 4=Side2), bound at clone time to the real mouse's buttons. The override sets that button's bit in the report the PC sees. Library bindings: [`inject` / `press` / `release` / `force_release`](/library/inject.md#inject). #### EXAMPLE Press Left: `class` `0x00`, `id` `0x0000`, `action` `0x01`: ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 03 | 00 | 04 00 | 00 | 00 00 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | class | id | action | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## class = key _Keyboard key and modifier override_ With `class = 1`, `id` is a [HID keyboard usage](/native/commands/usage.md#keycodes). A usage of `0xE0`\-`0xE7` folds into the modifier byte; anything else fills a keycode slot (or sets its NKRO bit) in the report the PC sees. Physical keys keep their slots, so injection never evicts the user's typing; past the board's rollover limit it emits the board's own `ErrorRollOver`. A keycode the cloned board can't report is a no-op. Library bindings: [`inject` / `press` / `release` / `force_release`](/library/inject.md#inject). #### EXAMPLE Press `A`: `class` `0x01`, `id` `0x0004`, `action` `0x01`: ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 03 | 00 | 04 00 | 01 | 04 00 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | class | id | action | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## class = media _Media key override_ With `class = 2`, `id` is a 16-bit [Consumer usage](/native/commands/usage.md#consumer) (e.g. 0xCD Play/Pause, 0xE9 Volume Up), merged onto the cloned keyboard's Consumer report. Present-gated to a board that declares a Consumer collection, read from the [`CAPS`](/native/commands/requests.md#caps) `CONSUMER` flag; otherwise a no-op. Library bindings: [`inject` / `press` / `release` / `force_release`](/library/inject.md#inject). #### EXAMPLE Press Volume Up: `class` `0x02`, `id` `0x00E9`, `action` `0x01`: ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 03 | 00 | 04 00 | 02 | E9 00 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | class | id | action | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` --- # Move _Cursor motion and scroll_ [`MOVE`](/native/commands/move.md#move) drives a relative Axis: the cursor pair (X and Y together) or the wheel, picked by a `motion` byte. It injects on top of the real mouse, so the PC sees the combined result, and it's [fire-and-forget](/native/injection.md#fire-and-forget). The momentary inputs (buttons, keys, media) have their own verb, [`INJECT`](/native/commands/inject.md#inject). | motion | Axis | carries | payload | | --- | --- | --- | --- | | `0` | [cursor](/native/commands/move.md#move) (X, Y) | `dx`, `dy` (i16) | 5 bytes | | `1` | [wheel](/native/commands/move.md#wheel) | `dz` (i16) | 3 bytes | ## MOVE _Relative axis injection_ `MOVE` shifts an axis by a relative amount, not a screen position. The `motion` byte at offset 0 selects the axis. [Opcode](/native/frame.md#opcodes) `0x01`. ```text MOVE 0x01 · cursor payload 5 bytes ``` _Fire-and-forget_ #### PAYLOAD (cursor, motion = 0) | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `motion` | `u8` | `0` = cursor | | 1 | `dx` | `i16` | horizontal step; +x = right, little-endian | | 3 | `dy` | `i16` | vertical step; +y = down, little-endian | #### GUARANTEES ``` exact net move = the sum of every delta you send range full i16 per axis, no clamp signs +x right, +y down (screen-style), +z wheel up paced a large move drains across frames; nothing is dropped ``` The box carries any remainder in its [accumulator](/native/injection.md#state); [`RESET`](/native/commands/admin.md#reset) zeroes it. Library binding: [`move_rel`](/library/move.md#move-rel). #### EXAMPLE Cursor `dx = 100`, `dy = 0` (`motion = 0`): ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 01 | 00 | 05 00 | 00 | 64 00 | 00 00 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | motion | dx | dy | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## MOVE (wheel) _Vertical scroll_ With `motion = 1`, `MOVE` scrolls the wheel by a relative amount. Same opcode, a shorter payload. ```text MOVE 0x01 · wheel payload 3 bytes ``` _Fire-and-forget_ #### PAYLOAD (wheel, motion = 1) | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `motion` | `u8` | `1` = wheel | | 1 | `dz` | `i16` | scroll steps; + = up, - = down, little-endian | #### EFFECT The box adds `dz` into its [accumulator](/native/injection.md#state) and drains it across [frames](/native/frame.md) with carry, no clamp. [`RESET`](/native/commands/admin.md#reset) clears it. Library binding: [`wheel`](/library/move.md#wheel). #### EXAMPLE Wheel `dz = 1`, one step up (`motion = 1`): ``` +--------+--------+--------+--------+--------+--------+--------+ | A5 | 01 | 00 | 03 00 | 01 | 01 00 | lo hi | +--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | motion | dz | CRC16 | +--------+--------+--------+--------+--------+--------+--------+ ``` --- # Requests _Query the box for state_ [`QUERY`](/native/commands/requests.md#requests) is the only command that gets a reply, a [`RESP`](/native/commands/requests.md#resp). Pick what to read with the `what` selector: the firmware [version](/native/commands/requests.md#version), the box's [health](/native/commands/requests.md#health), the cloned [device info](/native/commands/requests.md#device-info), [device capabilities](/native/commands/requests.md#caps), [rate](/native/commands/requests.md#rate), delivery [stats](/native/commands/requests.md#stats), the active input [locks](/native/commands/requests.md#locks), the [catch](/native/commands/requests.md#catch) subscription, a persistent box [option](/native/commands/requests.md#options), or the buffered input [clip](/native/commands/requests.md#clip). ## QUERY _Ask the box for state_ `QUERY` asks for one piece of state, named by its `what` byte. [Opcode](/native/frame.md#opcodes) `0x05`. ```text QUERY 0x05 · payload 1 byte (2 for OPTIONS) ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | which state to read (see below) | | 1 | `id` | `u8` | only for `what = 9` (OPTIONS): which option to read; omitted otherwise | #### SELECTORS | `what` | Reads | Reply | | --- | --- | --- | | `0` | The firmware version. | [`VERSION`](/native/commands/requests.md#version) | | `1` | The box's health. | [`HEALTH`](/native/commands/requests.md#health) | | `2` | The cloned device's USB identity, kind, and product. | [`DEVICE_INFO`](/native/commands/requests.md#device-info) | | `3` | The whole device's capabilities (mouse + keyboard). | [`CAPS`](/native/commands/requests.md#caps) | | `4` | The native report rate. | [`RATE`](/native/commands/requests.md#rate) | | `5` | Delivery and telemetry counters. | [`STATS`](/native/commands/requests.md#stats) | | `6` | The active input locks. | [`LOCKS`](/native/commands/requests.md#locks) | | `7` | The active catch subscription. | [`CATCH`](/native/commands/requests.md#catch) | | `8` | reserved | \- | | `9` | A persistent box option, by `id`. | [`OPTIONS`](/native/commands/requests.md#options) | | `10` | The buffered-clip ring depth, playback state, and config. | [`CLIP`](/native/commands/requests.md#clip) | #### EFFECT The box replies with a [`RESP`](/native/commands/requests.md#resp) carrying the same `what` and the requested data, one per selector in the table above. `QUERY` is the only command that gets a reply; everything else is [fire-and-forget](/native/injection.md#fire-and-forget). Library bindings: [`query_version`](/library/requests.md#version), [`query_health`](/library/requests.md#health), [`device_info`](/library/requests.md#device-info), [`caps`](/library/requests.md#caps), [`query_rate`](/library/requests.md#query-rate), [`query_stats`](/library/requests.md#query-stats), [`query_locks`](/library/requests.md#query-locks), [`query_catch`](/library/requests.md#query-catch), [`query_imperfect`](/library/options.md#query-imperfect), [`query_movement_riding`](/library/options.md#query-movement-riding), [`query_emit_pace`](/library/options.md#query-emit-pace), and the clip [`status`](/library/requests.md#clip-status) query. #### EXAMPLE `what = 0` (read the version): ``` +--------+--------+--------+--------+--------+--------+ | A5 | 05 | 00 | 01 00 | 00 | lo hi | +--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | CRC16 | +--------+--------+--------+--------+--------+--------+ ``` ## RESP _The box's reply_ `RESP` is the box's reply to a [`QUERY`](/native/commands/requests.md#requests). [Opcode](/native/frame.md#opcodes) `0x06`. ```text RESP 0x06 · box → PC ``` _Reply_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | echoes the request's selector | | 1.. | `data` | `varies` | the layout for the requested `what` (see the [selectors](/native/commands/requests.md#requests)) | #### EFFECT You get exactly one `RESP` per `QUERY`. Its [`SEQ`](/native/frame.md#seq) matches the request's and `what` echoes the selector, so you can pair a reply with its request and tell which kind it is. Each selector's payload is laid out below, with an example frame. ## VERSION _RESP payload, what = 0_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 0`. `proto_ver` is the protocol version (this documentation describes `3`); the box reports its own firmware version, then its base `mac`, a stable per-box id, then a length-delimited ASCII [`name`](/native/commands/option.md#name) tail (a synthesized default when unset). The `name` is additive, so `proto_ver` stays `3`: an older box just sends an empty tail. ```text QUERY what = 0 · RESP 11-byte header + name ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | 0x00 | | 1 | `proto_ver` | `u8` | protocol version, expected 3 | | 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` | the box's human-readable name, filling the rest of the payload (the frame `LEN` delimits it); a synthesized `Medius-XXXX` default when unset, set via [`OPTION(NAME)`](/native/commands/option.md#name) | #### EFFECT The box also sends this unprompted at startup, as a [ready signal](/native/connection.md#hello). The `mac` stays fixed for a box, so it identifies the same box across replugs and port renumbering; the [`name`](/native/commands/option.md#name) is its readable label. Library binding: [`query_version`](/library/requests.md#version). #### EXAMPLE Firmware `3.0.0`, protocol `3`, MAC `123456789abc`, name "Loki": ``` +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 06 | 00 | 0F 00 | 00 | 03 | 03 | 00 | 00 | ... | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | proto | major | minor | patch | ... | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | ... | 12 34 56 78 9A BC | 4C 6F 6B 69 | lo hi | +--------+--------------------+--------------+--------+ | ... | mac (6 bytes) | "Loki" | CRC16 | +--------+--------------------+--------------+--------+ ``` ## HEALTH _RESP payload, what = 1_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 1`: a single `flags` byte, each bit an independent status. ```text QUERY what = 1 · RESP 2 bytes ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | 0x01 | | 1 | `flags` | `u8` | the status bits below | #### FLAGS | Bit | Mask | Set when | | --- | --- | --- | | b0 | `0x01` | the link to the host chip is up | | b1 | `0x02` | a real mouse is attached | | b2 | `0x04` | the PC has set up the cloned mouse | | b3 | `0x08` | [injection](/native/injection.md) is active | | b4 | `0x10` | `RATE_CONFIDENT`: the native-rate estimator window is full, so the [`RATE`](/native/commands/requests.md#rate) value is trustworthy | | b5 | `0x20` | `LOCK_ON`: at least one input [`LOCK`](/native/commands/lock.md) is active | | b6 | `0x40` | `CATCH_ON`: a [`CATCH`](/native/commands/catch.md) subscription is active, physical-input events are streaming | | b7 | `0x80` | `KBD_ATT`: a keyboard is attached on the host chip, cloned and injectable | #### EFFECT The first three bits set means the box is ready for input to reach the PC. Library binding: [`query_health`](/library/requests.md#health). #### EXAMPLE Ready, with link, mouse, and clone all up (`flags = 0x07`): ``` +--------+--------+--------+--------+--------+--------+--------+ | A5 | 06 | 00 | 02 00 | 01 | 07 | lo hi | +--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | flags | CRC16 | +--------+--------+--------+--------+--------+--------+--------+ ``` ## DEVICE_INFO _RESP payload, what = 2_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 2`: the USB identity, kind, and product string the box read from the real device. The clone shows up on the game PC, not here, so this is the only way the control PC can read it. The header is fixed at 11 bytes, then a length-delimited UTF-8 `product` tail (which may be empty). Every field is zero when nothing is attached. ```text QUERY what = 2 · RESP 11-byte header + product ``` _Returns RESP_ #### PAYLOAD | 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` | the cloned device's kind, from its Boot-interface protocol (see below) | | 11.. | `product` | `UTF-8` | the product string, filling the rest of the payload; may be empty | #### FLAGS | Bit | Mask | Set when | | --- | --- | --- | | b0 | `0x01` | `HAS_SERIAL`: the clone serves a serial string | | b1 | `0x02` | `HAS_BOS`: the clone serves a BOS descriptor | #### PRIMARY_KIND | Value | Kind | | --- | --- | | `0` | unknown | | `1` | keyboard | | `2` | mouse | #### EFFECT A `vid` of `0` means nothing is attached yet. Library binding: [`device_info`](/library/requests.md#device-info). #### EXAMPLE A Logitech G502 (`046D:C08B`), USB 2.01, serial and BOS served, kind mouse, product "G502": ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 06 | 00 | 0F 00 | 02 | 6D 04 | 8B C0 | ... | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | vid | pid | ... | +--------+--------+--------+--------+--------+--------+--------+--------+ | ... | 10 01 | 01 02 | 03 | 02 | 47 35 30 32 | lo hi | +--------+--------+--------+--------+--------+--------------+--------+ | ... | bcdDev | bcdUSB | flags | kind | "G502" | CRC16 | +--------+--------+--------+--------+--------+--------------+--------+ ``` ## CAPS _RESP payload, what = 3_ The [`RESP`](/native/commands/requests.md#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. Use it to check before you act: an [`INJECT`](/native/commands/inject.md#inject) for a usage the device lacks is silently ignored, so the counts tell you what is real. A class that is not present reads all-zero. ```text QUERY what = 3 · RESP 7 bytes ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | 0x03 | | 1 | `n_buttons` | `u8` | buttons the mouse report carries | | 2 | `axis_flags` | `u8` | mouse axes, the bits below | | 3 | `n_hid` | `u8` | cloned HID interfaces; >1 = composite | | 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) | #### AXIS_FLAGS | 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 | #### KBD_FLAGS | 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 (passthrough-only) | | b3 | `0x08` | `REPORT_ID`: the keyboard report sits behind a HID report ID | #### EFFECT Library binding: [`caps`](/library/requests.md#caps). #### EXAMPLE A 5-button mouse (X/Y/wheel, one interface) plus a 6-key Consumer keyboard (`axis_flags = 0x07`, `kbd_flags = 0x02`, keyboard change-driven): ``` +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 06 | 00 | 07 00 | 03 | 05 | 07 | 01 | 06 | 02 | 02 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | n_btn | axis | n_hid | n_keys | kbdfl | chgdrv | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## RATE _RESP payload, what = 4_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 4`: how fast the active input reports, plus the poll period the clone advertises. The answer is class-aware, so read the field that fits 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 | ```text QUERY what = 4 · RESP 6 bytes ``` _Returns RESP_ #### PAYLOAD | 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 honest figure for a change-driven input | | 5 | `flags` | `u8` | the bits below | #### FLAGS | Bit | Mask | Set when | | --- | --- | --- | | b0 | `0x01` | `CONFIDENT`: the estimator window is full, same source as HEALTH [`RATE_CONFIDENT`](/native/commands/requests.md#health) | | b1 | `0x02` | `CHANGE_DRIVEN`: the active input is event-driven (keyboard / media), so there is no continuous cadence and `native_period_us` is 0 | #### EFFECT A 1 kHz mouse reads ~1000 µs once learned. Library binding: [`query_rate`](/library/requests.md#query-rate). #### EXAMPLE A 1 kHz mouse, 1000 µs poll, estimator confident (`flags = 0x01`): ``` +--------+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 06 | 00 | 06 00 | 04 | E8 03 | E8 03 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | native | poll | flags | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## STATS _RESP payload, what = 5_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 5`: counters the box keeps about whether your commands were delivered. Commands are [fire-and-forget](/native/injection.md#fire-and-forget) with no acknowledgement, so these counters are the only way to tell that everything you sent landed. A nonzero `tx_drops` or `tx_wedges` means delivery slipped under load. Wide counters clamp at their max instead of wrapping around. ```text QUERY what = 5 · RESP 17 bytes ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | 0x05 | | 1 | `inject_emits` | `u32` | pure-injection reports emitted, little-endian | | 5 | `tx_drops` | `u16` | reports dropped on TX-queue overflow; should stay 0 | | 7 | `tx_merges` | `u16` | backed-up reports merged instead of queued | | 9 | `tx_maxdepth` | `u8` | deepest the TX queue has reached | | 10 | `tx_wedges` | `u8` | wedged-endpoint recoveries | | 11 | `wakeups` | `u16` | remote-wakeups issued | | 13 | `reset_count` | `u16` | USB bus resets seen | | 15 | `config_count` | `u16` | SET\_CONFIGURATION events (re-enumerations) | #### EFFECT `inject_emits` counts the no-halving / 1 kHz path. The rest are merge, queue, wakeup, reset, and reconfig counters. Library binding: [`query_stats`](/library/requests.md#query-stats). #### EXAMPLE 4096 emits, no drops, no wedges: ``` +--------+--------+--------+--------+--------+--------------+ | A5 | 06 | 00 | 11 00 | 05 | 00 10 00 00 | +--------+--------+--------+--------+--------+--------------+ | SOF | TYPE | SEQ | LEN | what | inject_emits | +--------+--------+--------+--------+--------+--------------+ | 00 00 | 00 00 | 00 | 00 | 00 00 | 00 00 | 00 00 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | drops | merges | maxdep | wedges | wakeup | resets | config | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## LOCKS _RESP payload, what = 6_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 6`: which physical inputs are currently locked by [`LOCK`](/native/commands/lock.md), as a variable list of entries, one per locked field across every class, so keyboard and media locks read the same as mouse ones. An empty list (`n = 0`) means nothing is locked. ```text QUERY what = 6 · RESP 2 + 4n bytes ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | 0x06 | | 1 | `n` | `u8` | number of lock entries that follow | | + | `class` | `u8` | per entry: 0=button 1=key 2=media 3=axis (as [`LOCK`](/native/commands/lock.md)) | | + | `id` | `u16` | the locked field's id, or 0xFFFF for a whole-class blanket, little-endian | | + | `dirbits` | `u8` | which edges are locked, the bits below | #### DIRBITS | Bit | Mask | Set when | | --- | --- | --- | | b0 | `0x01` | the positive / press edge is locked | | b1 | `0x02` | the negative / release edge is locked | #### EFFECT Read it to confirm a lock landed, or to mirror the box's lock state in a UI. Library binding: [`query_locks`](/library/requests.md#query-locks). #### EXAMPLE One entry: the wheel's negative (scroll-down) sign locked (`class = 3` axis, `id = 2` wheel, `dirbits = 0x02`): ``` +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 06 | 00 | 06 00 | 06 | 01 | 03 | 02 00 | 02 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | n | class | id | dirbits| CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## CATCH _RESP payload, what = 7_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 7`: the active [`CATCH`](/native/commands/catch.md) subscription `mask`, plus the box-side count of event frames dropped under back-pressure. A zero mask means nothing is subscribed. Mirrors the [`CATCH_ON`](/native/commands/requests.md#health) health bit. ```text QUERY what = 7 · RESP 6 bytes ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | 0x07 | | 1 | `mask` | `u8` | subscribed event classes; bits Motion 0x01, Wheel 0x02, Buttons 0x04, Keys 0x08, Media 0x10 | | 2 | `dropped` | `u32` | events dropped box-side under back-pressure, little-endian | #### EFFECT Read it to confirm a subscription landed, or to check whether you're losing events. Library binding: [`query_catch`](/library/requests.md#query-catch). #### EXAMPLE Motion and buttons subscribed, no drops (`mask = 0x05`): ``` +--------+--------+--------+--------+--------+--------+--------------+--------+ | A5 | 06 | 00 | 06 00 | 07 | 05 | 00 00 00 00 | lo hi | +--------+--------+--------+--------+--------+--------+--------------+--------+ | SOF | TYPE | SEQ | LEN | what | mask | dropped | CRC16 | +--------+--------+--------+--------+--------+--------+--------------+--------+ ``` ## OPTIONS _RESP payload, what = 9_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 9`: the value of one persistent box [`OPTION`](/native/commands/option.md), echoing the queried `id`. The value is id-specific, so the query takes the option's `id` as a second byte and each option is read on its own. An unknown id gets no reply. ```text QUERY what = 9, id · RESP varies ``` _Returns RESP_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `what` | `u8` | 0x09 | | 1 | `id` | `u8` | which option this value is for | | 2.. | `value` | `varies` | id-specific, mirroring the matching [`OPTION`](/native/commands/option.md) value | #### IMPERFECT VALUE The [`IMPERFECT`](/native/commands/option.md#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` = cloning an over-capacity device is allowed | | 3 | `over_capacity` | the attached device needs an interrupt-IN endpoint the box can't service | | 4 | `clone_imperfect` | the live clone is over-capacity and was cloned anyway, so one interface is dead | Read it to tell why a clone is missing (`over_capacity = 1`, `allowed = 0`), or to confirm an imperfect clone is live (`clone_imperfect = 1`). Library binding: [`query_imperfect`](/library/options.md#query-imperfect). #### MOVE_RIDE VALUE The current [`MOVE_RIDE`](/native/commands/option.md#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`](/library/options.md#query-movement-riding). #### EMIT VALUE The current [`EMIT`](/native/commands/option.md#emit) pacing (id 2): the mode, the configured fixed rate, and the rate actually in effect. | Offset | Field | Notes | | --- | --- | --- | | 2 | `mode` | `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 | Library binding: [`query_emit_pace`](/library/options.md#query-emit-pace). #### EXAMPLE Reading `id = 0`: opted in, an over-capacity device attached and cloned imperfectly: ``` +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 06 | 00 | 05 00 | 09 | 00 | 01 | 01 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | what | id | allow | overcap| imperf | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## CLIP _RESP payload, what = 10_ The [`RESP`](/native/commands/requests.md#resp) payload when `what = 10`: the buffered-clip ring depth, playback state, and full config, for host flow-control. A fixed 25-byte prefix, then the clip's held-usage snapshot (the same class-tagged list a [`USAGE_EVENT`](/native/commands/catch.md#usage-event) carries), then the config tail (autolock, flags, and the bound triggers). Read `free` before a [`CLIP_APPEND`](/native/commands/clip.md#append) to avoid an overrun, and `state` to see a fault or that playback finished. Backs [`ClipHandle::query_status`](/library/requests.md#clip-status) and [`query_config`](/library/requests.md#clip-config). ```text QUERY what = 10 · RESP 25-byte prefix + held usages + config ``` _Returns RESP_ #### PAYLOAD | 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 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 emitted since start (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 | `held_n` | `u8` | number of held usages that follow | | + | `class` | `u8` | per held usage: 0=button 1=key 2=media | | + | `id` | `u16` | the held usage's id (button id, HID keycode, or Consumer usage), little-endian | | + | `autolock` | `u8` | config: the [`CLIP_SET`](/native/commands/clip.md#set) autolock bitmask (`CLIP_LOCK_*`) | | + | `flags` | `u8` | config: b0 loop, b1 retain, b2 finalized | | + | `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`](/native/commands/clip.md#ctrl) op 0..5 (start/stop/pause/resume/restart/toggle) | | + | `consume` | `u8` | per trigger: 1 = swallow the triggering edge from the host | #### FLAGS | 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`](/native/commands/clip.md#append) accepted | #### EFFECT The held snapshot lists the usages the clip is currently forcing down, one class-tagged entry each (3 bytes), so buttons, keys, and media are reported one way. The config tail mirrors what [`CLIP_SET`](/native/commands/clip.md#set) and [`CLIP_TRIGGER`](/native/commands/clip.md#trigger) set, so a UI can read back the whole clip in one query. Library bindings: [`query_status`](/library/requests.md#clip-status) ([`ClipStatus`](/library/types/structs.md#clip-status)) and [`query_config`](/library/requests.md#clip-config) ([`ClipSettings`](/library/types/structs.md#clip-settings)). #### EXAMPLE Idle, empty ring, no held usages, no autolock, no triggers (`state = 0`, `free = 1024`): ``` +--------+--------+--------+--------+--------+--------+--------------+ | A5 | 06 | 00 | 1C 00 | 0A | 00 | 00 04 00 00 | +--------+--------+--------+--------+--------+--------+--------------+ | SOF | TYPE | SEQ | LEN | what | state | free | +--------+--------+--------+--------+--------+--------+--------------+ | 00 04 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 | 00 00 | 00 00 | +--------------+--------------+--------------+--------+--------+--------+ | total | played | ticks | undrun | ovrrun | seqgap | +--------------+--------------+--------------+--------+--------+--------+ | 00 | 00 | 00 | 00 | lo hi | +--------+--------+--------+--------+--------+ | held_n | autolk | flags | n_trig | CRC16 | +--------+--------+--------+--------+--------+ ``` --- # Admin _Reset, reboot, and logs_ Three [frames](/native/frame.md) that manage the box rather than inject input: [`RESET`](/native/commands/admin.md#reset), [`REBOOT`](/native/commands/admin.md#reboot), and [`LOG`](/native/commands/admin.md#log). ## RESET _Back to pure passthrough_ `RESET` drops all injection and returns the box to plain passthrough. [Opcode](/native/frame.md#opcodes) `0x04`. ```text RESET 0x04 · payload 0 bytes ``` _Fire-and-forget_ #### PAYLOAD No payload (`LEN` is `0`). #### EFFECT Zeroes the accumulators (`accX`, `accY`, `accWheel`) and sets every [`INJECT`](/native/commands/inject.md) override back to none, so injected-held buttons and keys release and the real device passes through. The report is then byte-identical to passthrough; sending it twice is a no-op. This is what the [safety auto-clear](/native/injection.md#safety) performs. Library binding: [`reset`](/library/admin.md#reset). #### EXAMPLE A bare `RESET`: ``` +--------+--------+--------+--------+--------+ | A5 | 04 | 00 | 00 00 | lo hi | +--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | CRC16 | +--------+--------+--------+--------+--------+ ``` ## REBOOT _Restart a chip_ `REBOOT` restarts one of the box's two chips, mainly to enter ROM download mode (the chip's built-in bootloader, which accepts new firmware) for flashing. [Opcode](/native/frame.md#opcodes) `0x07`. ```text REBOOT 0x07 · payload 1 byte ``` _Fire-and-forget_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `target` | `u8` | which chip and mode (see below) | #### TARGETS | Target | Value | Effect | | --- | --- | --- | | device download | `0` | Device chip enters ROM download, then flash over the [CH343 link](/native/transport.md). | | host download | `1` | Device relays a download reboot to the host chip; the host flashes over its own USB. | | device run | `2` | Device chip reboots to run firmware. | | host run | `3` | Device relays a run reboot to the host chip. | #### EFFECT Reboot-to-run (`2` or `3`) is the only software cold-reboot on this board; DTR/RTS auto-reset is not wired. No reply: the chip is rebooting. See [Flashing](/native/flashing.md). Library binding: [`reboot`](/library/admin.md#reboot). #### EXAMPLE `target = 2` (device chip to run): ``` +--------+--------+--------+--------+--------+--------+ | A5 | 07 | 00 | 01 00 | 02 | lo hi | +--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | target | CRC16 | +--------+--------+--------+--------+--------+--------+ ``` ## LOG _Device diagnostics_ `LOG` is diagnostic text the box sends on its own, in place of an ASCII console. [Opcode](/native/frame.md#opcodes) `0x08`. ```text LOG 0x08 · box → PC ``` _Unsolicited_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `level` | `u8` | severity (see below) | | 1.. | `text` | `UTF-8` | the log line, not NUL-terminated, length = `LEN - 1` | #### LEVELS | Value | Level | | --- | --- | | `0` | error | | `1` | warn | | `2` | info | | `3` | debug | | `4` | verbose | #### EFFECT Emitted only while a control PC is attached; logging before first contact is dropped. The box's other outbound frame is [`RESP`](/native/commands/requests.md#resp). Library binding: [`logs`](/library/diagnostics.md#logs). #### EXAMPLE `level = 2` (info), text `hi` (`68 69`): ``` +--------+--------+--------+--------+--------+--------+--------+ | A5 | 08 | 00 | 03 00 | 02 | 68 69 | lo hi | +--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | level | text | CRC16 | +--------+--------+--------+--------+--------+--------+--------+ ``` --- # LED _Drive a status LED_ [`LED`](/native/commands/led.md#led) overrides one of the box's status LEDs, or hands it back to the box's own status display. Each chip has a single green LED, and by default each shows its own state. It's [fire-and-forget](/native/injection.md#fire-and-forget). ## LED _Override or restore a status LED_ `LED` picks a chip's green LED and either forces it to a pattern or returns it to its auto status display. [Opcode](/native/frame.md#opcodes) `0x09`. ```text LED 0x09 · payload 3 bytes ``` _Fire-and-forget_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `target` | `u8` | which chip's LED (see below) | | 1 | `mode` | `u8` | what to drive it to (see below) | | 2 | `level` | `u8` | brightness 0-255; used by solid and blink, ignored for off and auto | #### TARGETS | Target | Value | LED | | --- | --- | --- | | device | `0` | The device chip's own LED. | | host | `1` | The host chip's LED; the device relays it over the [inter-chip link](/native/architecture.md#data-flow). | | both | `2` | Both LEDs at once. | #### MODES | Mode | Value | Effect | | --- | --- | --- | | auto | `0` | Hand the LED back to the box's status display (see below). | | off | `1` | LED dark. | | solid | `2` | LED lit steadily at `level` brightness. | | blink | `3` | LED blinks at `level` brightness. | #### STATUS DISPLAY In `auto` (the default), each chip drives its own LED from its state. The device chip is solid when the inter-chip link, the real mouse, and the clone are all up, slow-blinks when the link is up but the mouse or clone is missing, and is off with no link. The host chip is solid while the mouse is streaming and off when it isn't. #### EFFECT An override holds until you send `auto` again, and the box also reverts it to status on control-PC silence (the same ~1 s timeout that clears [injection](/native/injection.md#safety)), on [`RESET`](/native/commands/admin.md#reset), or on inter-chip link loss. The LED is PC-owned state, released like injection. There's no game-PC-visible surface: a host or both override travels the inter-chip link only. Library binding: [`led`](/library/led.md#led). #### EXAMPLE Both LEDs to blink at `level = 200` (`0xC8`): ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 09 | 00 | 03 00 | 02 | 03 | C8 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | target | mode | level | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` --- # Lock _Block one physical input by class_ [`LOCK`](/native/commands/lock.md#lock) stops the physical device from driving one input while leaving everything else alone. It's generic over the input class: a relative axis, a mouse button, a keyboard key, or a media usage, plus a blanket lock for a whole class. Host [injection](/native/injection.md) still drives a locked input, so a script can take one over; it's [fire-and-forget](/native/injection.md#fire-and-forget). ## LOCK _Lock or unlock a physical input_ `LOCK` picks a `class`, an `id` within it, and a `direction`, then either blocks it or clears the block. A momentary usage shares [`INJECT`](/native/commands/inject.md#inject)'s `(class, id)` space, so a button locks exactly like a key. [Opcode](/native/frame.md#opcodes) `0x0A`. ```text LOCK 0x0A · payload 5 bytes ``` _Fire-and-forget_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `class` | `u8` | the input class (see below) | | 1 | `id` | `u16` | which input within the class, little-endian; `0xFFFF` = the whole class | | 3 | `direction` | `u8` | which sign or which edge (see below) | | 4 | `state` | `u8` | `1` = lock, `0` = unlock | #### CLASSES | Class | Value | `id` is | | --- | --- | --- | | button | `0` | a [button id](/native/commands/usage.md#buttons) (0=Left .. 4=Side2) | | key | `1` | a [HID keyboard usage](/native/commands/usage.md#keycodes) (0xE0-0xE7 = modifier) | | media | `2` | a 16-bit [Consumer usage](/native/commands/usage.md#consumer) | | axis | `3` | 0=X, 1=Y, 2=wheel (the sign is the direction) | Classes `0`\-`2` mirror [`INJECT`](/native/commands/inject.md#inject). An `id` of `0xFFFF` is a blanket: it locks every usage in that class in one command (every button, every key, or every media usage). #### DIRECTION What `direction` means depends on the input. For an axis it's a sign, so you can block scrolling up but not down. For a button, key, or media usage it's an edge, so you can block the press but not the release. | Direction | Value | Axis | Button / key / media | | --- | --- | --- | --- | | both | `0` | Both signs. | Press and release. | | positive | `1` | Positive sign only (`+`). | Press only (`0 to 1`). | | negative | `2` | Negative sign only (`-`). | Release only (`1 to 0`). | #### PHYSICAL ONLY A lock blocks the physical device and nothing else. Host [injection](/native/injection.md) still reaches a locked input, so a [`MOVE`](/native/commands/move.md#move) moves a locked axis and an [`INJECT`](/native/commands/inject.md#inject) drives a locked button or key. Lock an input the user shouldn't touch, then drive it yourself. #### A LOCK CLEARS ON ``` unlock you send the matching unlock (state = 0) silence ~1 s with no control-PC frame (same net as injection) RESET a RESET command link loss the inter-chip link drops ``` > **Warning** > > A lock isn't permanent. It auto-clears on the same safety net as injection, so hold it with a keepalive if the user has to stay locked out. #### EFFECT Locks are PC-owned and never visible to the game PC. [`QUERY(LOCKS)`](/native/commands/requests.md#locks) reads the active set across every class; the HEALTH [`LOCK_ON`](/native/commands/requests.md#health) bit is set while any lock, of any class, is active. Library bindings: [`lock`](/library/lock.md#lock) / [`unlock`](/library/lock.md#unlock), and [`lock_all`](/library/lock.md#lock-all) / [`unlock_all`](/library/lock.md#lock-all) for a blanket. #### EXAMPLE Lock the wheel's negative (scroll-down) sign: `class = 3` (axis), `id = 2` (wheel), `direction = 2`, `state = 1`: ``` +--------+--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 0A | 00 | 05 00 | 03 | 02 00 | 02 | 01 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | class | id | dir | state | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+--------+ ``` --- # Catch _Stream the physical mouse, keyboard, and media input to the PC_ [`CATCH`](/native/commands/catch.md#catch) subscribes to the user's real input. While subscribed, the box pushes a [`MOTION_EVENT`](/native/commands/catch.md#motion-event) for movement and the wheel, and a [`USAGE_EVENT`](/native/commands/catch.md#usage-event) for buttons, keys, and media, captured at the merge point _before_ any [`LOCK`](/native/commands/lock.md) suppression or [injection](/native/injection.md), so you can lock an input and still see it to rebind it. Subscribing is [fire-and-forget](/native/injection.md#fire-and-forget); the box streams until you unsubscribe. ## CATCH _Subscribe to the physical-input event stream_ `CATCH` sends a one-byte class mask. A non-zero mask subscribes; `0` unsubscribes. [Opcode](/native/frame.md#opcodes) `0x0B`. ```text CATCH 0x0B · payload 1 byte ``` _Fire-and-forget_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `mask` | `u8` | which classes to stream (see below); `0` = unsubscribe | #### MASK Each bit turns on one class of change. The mask only chooses which reports _trigger_ an event (every event carries the full held-usage snapshot), so a buttons-only subscription stays sparse even though the mouse reports at ~1 kHz. Combine bits with OR; `0x1F` subscribes to every class. | Class | Bit | Triggers on | | --- | --- | --- | | Motion | `0x01` | a non-zero X or Y delta (a [`MOTION_EVENT`](/native/commands/catch.md#motion-event)). | | Wheel | `0x02` | a non-zero wheel delta (a [`MOTION_EVENT`](/native/commands/catch.md#motion-event)). | | Buttons | `0x04` | a mouse-button edge (a [`USAGE_EVENT`](/native/commands/catch.md#usage-event)). | | Keys | `0x08` | a keyboard key or modifier change (a [`USAGE_EVENT`](/native/commands/catch.md#usage-event)). | | Media | `0x10` | a media (Consumer) usage change (a [`USAGE_EVENT`](/native/commands/catch.md#usage-event)). | #### PHYSICAL ONLY The stream reports the user's _physical_ input, never your injected overrides, and it reads the report before [`LOCK`](/native/commands/lock.md) clamps it, so a locked axis or blocked button is still reported here. That's the intercept-and-rebind loop: lock an input to hide it from the game, catch it to act on it. #### EFFECT The box streams from the moment a non-zero mask arrives until you send `0`. The subscription is PC-owned and clears on the same triggers as injection: control-PC silence (the ~1 s timeout), a [`RESET`](/native/commands/admin.md#reset), a mouse detach, or inter-chip link loss, plus an explicit unsubscribe. The host library holds an open subscription alive with its keepalive (re-asserting it after a device-side blip) and across a reconnect. [`QUERY(CATCH)`](/native/commands/requests.md#catch) reads the active mask and a dropped-event count; the HEALTH [`CATCH_ON`](/native/commands/requests.md#health) bit is set while subscribed. Library binding: [`catch_events`](/library/catch.md#catch-events). #### EXAMPLE Subscribe to every class (`mask = 0x1F`): ``` +--------+--------+--------+--------+--------+--------+ | A5 | 0B | 00 | 01 00 | 1F | lo hi | +--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | mask | CRC16 | +--------+--------+--------+--------+--------+--------+ ``` ## MOTION_EVENT _One physical relative-axis snapshot, box → PC_ While a subscription with `Motion` or `Wheel` is active the box pushes a `MOTION_EVENT` for each physical report whose motion changed. It's unsolicited (there's no [`QUERY`](/native/commands/requests.md#requests) to correlate), so [`SEQ`](/native/frame.md#seq) is a rolling per-event counter shared with [`USAGE_EVENT`](/native/commands/catch.md#usage-event): a host detects dropped events as `SEQ` gaps. [Opcode](/native/frame.md#opcodes) `0x0C`. ```text MOTION_EVENT 0x0C · payload 6 bytes ``` _Unsolicited_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `dx` | `i16` | physical X this report; + = right, little-endian | | 2 | `dy` | `i16` | physical Y this report; + = down, little-endian | | 4 | `dz` | `i16` | physical wheel delta this report; + = up, little-endian | #### BEST-EFFORT Delivery is best-effort: under back-pressure the box drops events (counted in [`QUERY(CATCH)`](/native/commands/requests.md#catch)) rather than stalling the report path, so the stream never delays the game-PC-facing reports. #### EXAMPLE The user moves +10 right, no vertical or wheel motion (`dx = 10`): ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 0C | 2A | 06 00 | 0A 00 | 00 00 | 00 00 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | dx | dy | dz | CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## USAGE_EVENT _One physical held-usage snapshot, box → PC_ While a subscription with `Buttons`, `Keys`, or `Media` is active the box pushes a `USAGE_EVENT` when that class changes: a class-tagged snapshot of the usages currently held, so a mouse-button press and a key press have the same shape. It's a full snapshot, not edge deltas, so a dropped frame self-corrects on the next one; diff successive snapshots per class for press / release edges. [Opcode](/native/frame.md#opcodes) `0x0F`. ```text USAGE_EVENT 0x0F · payload 1 + 3n bytes ``` _Unsolicited_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `n` | `u8` | number of held usages that follow | | + | `class` | `u8` | per usage: 0=button 1=key 2=media (as [`INJECT`](/native/commands/inject.md#inject)) | | + | `id` | `u16` | the held usage's id (a button id, HID keycode with 0xE0-0xE7 modifiers, or Consumer usage), little-endian | #### ONE CLASS PER EVENT Each entry is 3 bytes; the snapshot is `n` of them, all one class (one physical report is one class), so a `Keys` event lists every held key and a `Buttons` event every held button. Best-effort like [`MOTION_EVENT`](/native/commands/catch.md#motion-event): dropped events are counted in [`QUERY(CATCH)`](/native/commands/requests.md#catch). #### EXAMPLE Left Shift held while pressing `A` (a keys snapshot, two usages both `class = 1`: Left Shift `id = 0xE1`, then A `id = 0x04`): ``` +--------+--------+--------+--------+--------+----------+----------+--------+ | A5 | 0F | 2B | 07 00 | 02 | 01 E1 00 | 01 04 00 | lo hi | +--------+--------+--------+--------+--------+----------+----------+--------+ | SOF | TYPE | SEQ | LEN | n | usage[0] | usage[1] | CRC16 | +--------+--------+--------+--------+--------+----------+----------+--------+ ``` --- # 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](/native/injection.md#fire-and-forget). An unknown id is ignored. | Option | `id` | Value | Does | Default | | --- | --- | --- | --- | --- | | [`IMPERFECT`](/native/commands/option.md#imperfect) | `0` | `[allow u8]` | Clone an over-capacity device anyway | off | | [`MOVE_RIDE`](/native/commands/option.md#move-ride) | `1` | `[timeout u16 LE]` | Inject motion only on a real move | off | | [`EMIT`](/native/commands/option.md#emit) | `2` | `[mode u8][rate_hz u16 LE]` | Pick what paces injected motion | learnt | | [`NAME`](/native/commands/option.md#name) | `3` | `[name ascii 0..32]` | Give the box a human-readable name | Medius-XXXX | ## OPTION _One generic, persistent option_ `OPTION` carries an `id` byte then an id-specific value, and the box persists the setting across a reboot. One opcode covers every persistent option; the id picks which. [Opcode](/native/frame.md#opcodes) `0x11`. ```text OPTION 0x11 · payload 1 + value bytes ``` _Fire-and-forget_ #### PAYLOAD | 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)`](/native/commands/requests.md#options). ## IMPERFECT _Clone an over-capacity device anyway_ ```text id 0 · [allow u8] ``` #### ALLOW | 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 | > **Note** > > Some devices need more interrupt-IN endpoints than the box serves (the Wooting Two HE's analog stream wants a sixth, past the [ESP32](https://www.espressif.com/en/products/socs)\-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)`](/native/commands/requests.md#options) (opt-in plus the over-capacity and imperfect-clone flags) · Library [`allow_imperfect_clones`](/library/options.md#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_ ```text id 1 · [timeout u16 LE] ms ``` #### TIMEOUT | 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). > **Warning** > > While on, pure idle injection (moving the cursor while the hand is still) stops working: motion waits for a native move and is dropped if none comes. Button, key, and media injection are unaffected. Read [`QUERY(OPTIONS, 1)`](/native/commands/requests.md#options) · Library [`set_movement_riding`](/library/options.md#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 | +--------+--------+--------+--------+--------+--------+--------+ ``` ## EMIT _Pick what paces injected motion_ ```text id 2 · [mode u8][rate_hz u16 LE] ``` #### MODE | `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` | > **Note** > > 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. Learnt keeps injection matched to the real mouse; the other modes are for a host that shapes its own report density and wants the box to stop re-pacing it. Read [`QUERY(OPTIONS, 2)`](/native/commands/requests.md#options) (mode plus the rate in effect) · Library [`set_emit_pace`](/library/options.md#set-emit-pace). #### EXAMPLE Fixed 1 kHz (`mode = 2`, `rate_hz = 0x03E8`): ``` +--------+--------+--------+--------+--------+--------+--------+--------+ | A5 | 11 | 00 | 04 00 | 02 | 02 | E8 03 | lo hi | +--------+--------+--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | id | mode | rate_hz| CRC16 | +--------+--------+--------+--------+--------+--------+--------+--------+ ``` ## NAME _Give the box a human-readable name_ ```text id 3 · [name ascii 1..32] (0 bytes = clear) ``` #### VALUE | 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. | > **Note** > > The name is the readable partner to the box [MAC](/native/commands/requests.md#version), persisted in NVS with no reboot. It rides on [`RESP(VERSION)`](/native/commands/requests.md#version) as the ASCII tail after the MAC, so it's read there, not through [`QUERY(OPTIONS)`](/native/commands/requests.md#options). Read it back on [`RESP(VERSION)`](/native/commands/requests.md#version) · Library [`set_name`](/library/options.md#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 | +--------+--------+--------+--------+--------+--------------+--------+ ``` --- # CLIP _Preload input and let the box play it back, frame by frame_ [`CLIP_APPEND`](/native/commands/clip.md#append) preloads a sequence of per-frame entries into a ring on the box, then [`CLIP_CTRL`](/native/commands/clip.md#ctrl) drives the playback engine and the box drains one entry per native frame into the same [injection state](/native/injection.md#state) that [`INJECT`](/native/commands/inject.md) and [`MOVE`](/native/commands/move.md) feed. Playback is box-clocked, so it carries no host scheduling jitter and no per-command send floor. Like [`INJECT`](/native/commands/inject.md) it is field-generic and [additive](/native/injection.md#state): one clip mixes mouse motion, buttons, keyboard, and media, each routed to its own interface, and follows [movement riding](/native/commands/option.md#move-ride) and [the emit rate](/native/commands/option.md#emit). 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)`](/native/commands/requests.md#clip). #### TWO MODES A clip runs in one of two shapes, set by the `retain` flag on [`CLIP_SET`](/native/commands/clip.md#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 [`FINALIZE`](/native/commands/clip.md#ctrl)d you can `START`, `RESTART`, or `loop` it 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`](/native/commands/clip.md#append) | PC→box | append a batch of entries to the ring | | `0x13` | [`CLIP_CTRL`](/native/commands/clip.md#ctrl) | PC→box | drive the playback engine (start, stop, pause, ...) | | `0x14` | [`CLIP_SET`](/native/commands/clip.md#set) | PC→box | set a clip setting (auto-lock, loop, retain) | | `0x15` | [`CLIP_TRIGGER`](/native/commands/clip.md#trigger) | PC→box | bind a physical edge to an engine verb | ## Entry format _The bytes CLIP_APPEND carries_ A 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. #### GAP RUN 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 | #### CONTENT TICK 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]` | #### EDGES An edge reuses [`INJECT`](/native/commands/inject.md#inject)'s tuple, so one clip drives every input class. #### CLASS | `class` | Value | `id` is | | --- | --- | --- | | button | `0` | a [button id](/native/commands/usage.md#buttons) (0=Left .. 4=Side2) | | key | `1` | a [HID keycode](/native/commands/usage.md#keycodes) (0xE0-0xE7 = modifier) | | media | `2` | a 16-bit [Consumer usage](/native/commands/usage.md#consumer) | #### ACTION | 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. #### MOTION AND EDGES ON ONE TICK 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] ``` #### A CLIP IS A TIMELINE 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 | #### EXAMPLE ENTRIES 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 ring_ Append a batch of whole [entries](/native/commands/clip.md#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](/native/frame.md#opcodes) `0x12`. ```text CLIP_APPEND 0x12 · payload = one or more whole entries ``` _Fire-and-forget_ #### PAYLOAD | Offset | Field | Type | Notes | | --- | --- | --- | --- | | 0 | `entries` | `bytes` | a whole number of [entries](/native/commands/clip.md#entries), back to back (up to the 512-byte frame limit) | #### DROP DETECTION The frame [`SEQ`](/native/frame.md#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](/native/injection.md#fire-and-forget), a lost frame shows up as a `SEQ` gap, and the box marks the clip `faulted` in [`QUERY(CLIP)`](/native/commands/requests.md#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. #### FLOW CONTROL 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)`](/native/commands/requests.md#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 faulted ``` Library binding: [`ClipBuilder`](/library/clip.md#builder) + [`append`](/library/clip.md#handle), which splits a large clip into whole-entry frames for you. #### EXAMPLE 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 engine_ One byte of `op` selects an engine verb. [Opcode](/native/frame.md#opcodes) `0x13`. There are no args: settings live on [`CLIP_SET`](/native/commands/clip.md#set), so a control frame is just the verb. ```text CLIP_CTRL 0x13 · payload [op u8] ``` _Fire-and-forget_ #### OPS | 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 the`action` byte a [`CLIP_TRIGGER`](/native/commands/clip.md#trigger) fires on a physical edge; `CLEAR` and `FINALIZE` are host-only. #### STATE [`QUERY(CLIP)`](/native/commands/requests.md#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 | #### UNDERRUN 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`](/native/commands/clip.md#set) is set. #### STOPS ON ``` 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`](/native/commands/admin.md#reset), detach, link loss) also flushes the ring. The [1 s safety net](/native/injection.md#safety) and a [`RESET`](/native/commands/admin.md#reset) reach a clip like any other injection. With [movement riding](/native/commands/option.md#move-ride) 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()`](/library/clip.md). #### EXAMPLE 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 setting_ Set one of the clip's settings, [OPTION](/native/commands/option.md)\-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)`](/native/commands/requests.md#clip). [Opcode](/native/frame.md#opcodes) `0x14`. ```text CLIP_SET 0x14 · payload [id u8][value u8] ``` _Fire-and-forget_ #### SETTINGS | 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 | #### AUTO-LOCK 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`](/native/commands/lock.md) 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`](/library/clip.md#handle), [`set_loop`](/library/clip.md#handle), [`set_retain`](/library/clip.md#handle). #### EXAMPLE 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 verb_ Fire a [`CLIP_CTRL`](/native/commands/clip.md#ctrl) verb the instant the user physically moves an input (the same physical edge [`CATCH`](/native/commands/catch.md) reports), with no host round-trip, so even the first emitted frame is box-timed. Triggers are a [`LOCK`](/native/commands/lock.md)\-shaped managed set of up to eight bindings, keyed by `(class, id, edge)`. [Opcode](/native/frame.md#opcodes) `0x15`. ```text CLIP_TRIGGER 0x15 · payload [class u8][id u16][edge u8][action u8][flags u8] ``` _Fire-and-forget_ #### PAYLOAD | 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`](/native/commands/clip.md#ctrl) op to fire, `0`\-`5` | | 5 | `flags` | `u8` | bit0 present, bit1 consume (below) | #### CLASS | `class` | Value | `id` is | | --- | --- | --- | | button | `0` | a [button id](/native/commands/usage.md#buttons) | | key | `1` | a [HID keycode](/native/commands/usage.md#keycodes) | | media | `2` | a [Consumer usage](/native/commands/usage.md#consumer) | | any | `0xFF` | ignored (any input fires) | #### EDGE | Edge | Value | Fires on | | --- | --- | --- | | both | `0` | press and release | | press | `1` | the physical press | | release | `2` | the physical release | #### FLAGS | 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 [`FINALIZE`](/native/commands/clip.md#ctrl)d) before you bind. Library binding: [`bind`](/library/clip.md#handle), [`unbind`](/library/clip.md#handle), [`clear_triggers`](/library/clip.md#handle). #### EXAMPLE 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 | +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ ``` --- # Usage IDs _The id numbers inject and lock take_ [`INJECT`](/native/commands/inject.md#inject) and [`LOCK`](/native/commands/lock.md#lock) both name an input by an `id`, and the numbers depend on the class: a [button id](/native/commands/usage.md#buttons) for the mouse, a [HID keyboard usage](/native/commands/usage.md#keycodes) for a key, or a 16-bit [Consumer usage](/native/commands/usage.md#consumer) for a media key. They're gathered here so the command pages stay short. ## Button ids _Mouse buttons (class = button)_ A small semantic id, bound at clone time to the real mouse's buttons, and the same id for [`INJECT`](/native/commands/inject.md#button) and [`LOCK`](/native/commands/lock.md#lock) (a button locks as `class = 0`, `id = button id`, the same `(class, id)` form a key or media usage takes). A command for an id the mouse lacks is a no-op, so read [`CAPS`](/native/commands/requests.md#caps) `n_buttons` first. | Button | `id` | | --- | --- | | Left | `0` | | Right | `1` | | Middle | `2` | | Side1 (first thumb) | `3` | | Side2 (second thumb) | `4` | The Rust library exposes these as the named [`Button`](/library/types/enums.md#button) enum (`Button::Left`, `Button::Side2`, ...). ## HID keyboard usages _Keys and modifiers (class = key)_ The `id` is a HID Keyboard/Keypad usage from the [USB HID Usage Tables](https://www.usb.org/sites/default/files/hut1_5.pdf) (page 0x07). A usage of `0xE0`\-`0xE7` is a modifier and folds into the modifier byte; any other usage fills a keycode slot. The common ones: | Key | Usage | | --- | --- | | `A` .. `Z` | `0x04` .. `0x1D` | | `1` .. `9` | `0x1E` .. `0x26` | | `0` | `0x27` | | Enter / Escape / Backspace / Tab | `0x28` / `0x29` / `0x2A` / `0x2B` | | Space | `0x2C` | | Caps Lock | `0x39` | | `F1` .. `F12` | `0x3A` .. `0x45` | | Insert / Home / Page Up | `0x49` / `0x4A` / `0x4B` | | Delete / End / Page Down | `0x4C` / `0x4D` / `0x4E` | | Right / Left / Down / Up | `0x4F` / `0x50` / `0x51` / `0x52` | #### MODIFIERS | Modifier | Usage | | --- | --- | | Left Ctrl / Shift / Alt / GUI | `0xE0` / `0xE1` / `0xE2` / `0xE3` | | Right Ctrl / Shift / Alt / GUI | `0xE4` / `0xE5` / `0xE6` / `0xE7` | The Rust library exposes these as named [`Key`](/library/types/structs.md#key) constants (`Key::A`, `Key::LEFT_SHIFT`, ...). ## Consumer usages _Media keys (class = media)_ The `id` is a 16-bit usage from the Consumer page (0x0C) of the [USB HID Usage Tables](https://www.usb.org/sites/default/files/hut1_5.pdf). Present-gated to a board with a Consumer collection, read from the [`CAPS`](/native/commands/requests.md#caps) `CONSUMER` flag. The usual transport controls: | Media key | Usage | | --- | --- | | Play / Pause toggle | `0x00CD` | | Play | `0x00B0` | | Pause | `0x00B1` | | Stop | `0x00B7` | | Next track | `0x00B5` | | Previous track | `0x00B6` | | Mute | `0x00E2` | | Volume up | `0x00E9` | | Volume down | `0x00EA` | The Rust library exposes these as named [`MediaKey`](/library/types/structs.md#media-key) constants (`MediaKey::VOLUME_UP`, ...). --- # Flashing _Writing new firmware_ Flashing writes new firmware onto the box, to update or recover it. The box has two chips; flash each separately. With firmware already running, no physical button is needed: a [`REBOOT`](/native/commands/admin.md#reboot) command restarts a chip into ROM download mode (a built-in loader that takes firmware over serial), then a flashing tool writes the image. ## Two chips _Flash each separately_ Pick the chip with the [`REBOOT`](/native/commands/admin.md#reboot) `target` byte, then flash it over the link in the table. | Chip | Reboot | Flashed over | | --- | --- | --- | | [Device chip](/native/architecture.md) | `target = 0` | The same [CH343](/native/transport.md) serial link, with [`esptool`](https://github.com/espressif/esptool). | | [Host chip](/native/architecture.md) | `target = 1` | Its own USB connection; the device relays the reboot over the inter-chip link. | ## Version scheme _major.minor.patch_ Read the running version with [`QUERY(VERSION)`](/native/commands/requests.md#version). ## Notes > **Warning** > > - The [`REBOOT`](/native/commands/admin.md#reboot) path needs working firmware to receive the frame. A chip with no firmware yet (a first flash) or a bad image can't, so enter download mode the hardware way: hold the chip's BOOT button while you reset or power on the box. > - A run reboot (`target = 2` or `3`) is the only software cold-reboot; `DTR`/`RTS` aren't wired to a reset on this board. > - After a download reboot the serial port sits in the ROM bootloader (plain ASCII the [frame decoder](/native/frame.md) ignores) until you finish flashing or power-cycle. > - Persisted per-box data survives an app reflash. > **Note** > > The `medius` crate's [flash feature](/library/features/flash.md) does this from Rust. --- # Troubleshooting _Common failures and what they mean_ Every message in either direction is a [`frame`](/native/frame.md) (one packet on the wire). Most commands are [fire-and-forget](/native/injection.md#fire-and-forget): you send, the box stays silent. The exception is [`QUERY`](/native/commands/requests.md#requests), which asks for a piece of the box's state and gets back one [`RESP`](/native/commands/requests.md#resp) frame. Most checks below use that round-trip. ## No reply to QUERY(VERSION) _Wrong baud, a held port, or not a Medius box_ [`QUERY(VERSION)`](/native/commands/requests.md#version) asks for the firmware and protocol version; the reply is a [`RESP(VERSION)`](/native/commands/requests.md#version) frame. If none comes back, check: - The port wasn't opened at `4,000,000` baud. The box speaks [framed binary](/native/frame.md) from the first byte, with no slower startup speed. - Another process holds the port. Only one program can have it open. - It isn't a Medius box, or you opened the wrong port. - You opened the port after the hello already fired. The hello is a [`RESP(VERSION)`](/native/commands/requests.md#version) the box sends on its own once the control link comes up. Missing it costs nothing: send [`QUERY(VERSION)`](/native/commands/requests.md#version) and read the reply. See [the ready hello](/native/connection.md#hello). ## Injection does nothing _Check HEALTH before you rely on it_ [Injection](/native/injection.md) is the input your program adds on top of the real mouse's passthrough (movement, buttons, scroll). If it has no effect, send [`QUERY(HEALTH)`](/native/commands/requests.md#health) and read the `flags` byte. The box only merges injection once the first three flags are set: `LINK_UP`, `MOUSE_ATTACHED` (a mouse is on [`USB3`](/native/hardware.md)), and `CLONE_CONFIGURED` (the PC has enumerated the clone). The full flags byte is on [HEALTH](/native/commands/requests.md#health). ## A held button releases on its own _The silence auto-clear dropped it_ The box clears all injection if your program goes quiet, so input can't get stuck. | Event | Effect | | --- | --- | | Silence timeout (default `1000 ms` with no valid inbound frame) | Drops every held button and pending move, returns to plain passthrough. | | Any frame that passes its [checksum](/native/frame.md#crc) (including a [`QUERY`](/native/commands/requests.md#requests)) | Resets the timer. | To hold an injected button, keep the link busy with periodic frames (a [`QUERY(HEALTH)`](/native/commands/requests.md#health) is enough), or let [the library's keepalive](/library/guides/connection.md#keepalive) do it. > **Note** > > See [Injection](/native/injection.md#safety) for the safety state machine. ## A machine shuts off or drains its battery _USB1 and USB3 on the same machine_ [`USB1`](/native/hardware.md) and [`USB3`](/native/hardware.md) share one internal rail, and the [`USB3`](/native/hardware.md) 5V rail can't be pulled low in firmware, so wiring both to one machine back-feeds power into it. Keep them apart: | Port | Carries | Connects to | | --- | --- | --- | | [`USB1`](/native/hardware.md) | the clone to the PC | the game PC | | [`USB2`](/native/hardware.md) | the control link | the control PC | | [`USB3`](/native/hardware.md) | the real mouse | the mouse | > **Danger** > > ⚠️ [`USB1`](/native/hardware.md) and [`USB3`](/native/hardware.md) must never both connect to the same machine. See [Hardware](/native/hardware.md). ## The serial port disappeared after a REBOOT _The chip is in ROM download mode_ A download [`REBOOT`](/native/commands/admin.md#reboot) (`target` `0` or `1`) drops a chip into ROM download mode, so its running firmware, and the serial port that firmware provides, goes away. Flash the chip or power-cycle the box to get the port back. See [Flashing](/native/flashing.md). ## No LOG frames arrive _Only sent while a control PC is attached_ [`LOG`](/native/commands/admin.md#log) is the box's unsolicited diagnostic frame (box to PC). If none arrive, check: - No control PC is attached. [`LOG`](/native/commands/admin.md#log) is only emitted while one is. - It logged before first contact. That output is dropped, not buffered. - It's early ROM-bootloader output, which is plain ASCII the frame decoder ignores. --- # AI & LLM access _Read these docs from an agent_ Every page is also served as Markdown, indexed for LLMs, and exposed by an MCP server. Point your agent at whichever fits: a page's [Markdown twin](/ai.md#markdown), the [llms.txt](/ai.md#llms) index, or the [MCP server](/ai.md#mcp). ## Markdown twins _Any page as clean Markdown_ Append `.md` to a doc URL, or send `Accept: text/markdown` on the page URL. Both return the page as Markdown, with links rewritten to the other `.md` pages. #### EXAMPLE ```bash # the .md twin of any page curl https://medius.k4tech.net/library/clip.md # or content negotiation on the page URL curl -H "Accept: text/markdown" https://medius.k4tech.net/library/clip ``` ## llms.txt _The index and the full corpus_ [/llms.txt](https://medius.k4tech.net/llms.txt) lists every page as a Markdown link, following the [llms.txt](https://llmstxt.org/) convention; [/llms-full.txt](https://medius.k4tech.net/llms-full.txt) is the whole documentation in one file. ## MCP server _Search and fetch the docs from a coding agent_ A read-only [MCP](https://modelcontextprotocol.io) server at `https://medius.k4tech.net/mcp` (Streamable HTTP, no auth). It exposes `search` and `fetch` for ChatGPT deep research, plus `search_docs`, `get_page`, and `list_pages` for other clients. #### CLAUDE CODE ```bash claude mcp add --transport http medius-docs https://medius.k4tech.net/mcp ``` #### CURSOR / WINDSURF / VS CODE ```json { "mcpServers": { "medius-docs": { "url": "https://medius.k4tech.net/mcp" } } } ``` #### CLAUDE DESKTOP / CLAUDE.AI / CHATGPT Add a custom connector with the URL `https://medius.k4tech.net/mcp`. --- # Medius Rust Library _Official Rust client_ The [`medius`](https://crates.io/crates/medius) crate injects input on top of a real mouse, keyboard, or combo over a USB-serial link. | Property | Value | | --- | --- | | Crate version | `3.0.0` | | [Edition](https://doc.rust-lang.org/edition-guide/rust-2024/index.html) | `2024` | | [MSRV](https://doc.rust-lang.org/cargo/reference/rust-version.html) (minimum supported Rust version) | `1.85` | | License | [`MIT`](https://opensource.org/license/mit) | | Transport | 4 Mbaud, framed-only | | Thread safety | `Send + Sync` (clone freely) | | Safety | `#![forbid(unsafe_code)]` | ## Installation ```bash cargo add medius ``` With optional features: ```bash cargo add medius --features async,mock ``` | Feature | Description | | --- | --- | | [`async`](/library/features/async.md) | Runtime-agnostic `AsyncDevice`, async queries. | | [`mock`](/library/features/mock.md) | In-process fake box for tests. | | [`flash`](/library/features/flash.md) | [`esptool`](https://github.com/espressif/esptool) firmware flashing. | | [`tracing`](/library/features/tracing.md) | Tracing instrumentation across the connection lifecycle. | ## Getting started - [Connection](/library/connection.md): Open, find, handshake - [Discovery](/library/discovery.md): List boxes, open by identity ## API - [Inject](/library/inject.md): press, release, force_release - [Move](/library/move.md): move_axis, move_rel, wheel - [Lock](/library/lock.md): lock, lock_axis, lock_all - [Catch](/library/catch.md): Stream physical input - [Clip](/library/clip.md): Preload input, box-clocked playback - [Requests](/library/requests.md): version, health, and the device-info queries - [LED](/library/led.md): Drive a status LED - [Admin](/library/admin.md): reset, reboot - [Options](/library/options.md): imperfect clones, movement riding - [Lifecycle](/library/lifecycle.md): reapply, reconnect - [Logs & counters](/library/diagnostics.md): Read logs, snapshot counters ## Features - [async](/library/features/async.md): AsyncDevice - [mock](/library/features/mock.md): In-process fake box - [flash](/library/features/flash.md): esptool flashing - [tracing](/library/features/tracing.md): Structured diagnostics ## Guides _Behavior and how-to, outside the reference_ - [Calls & input](/library/guides/calls.md): Call kinds, async, motion, clicks - [Connection](/library/guides/connection.md): Ports, threads, keepalive - [Testing](/library/guides/testing.md): MockBox in tests ## Reference - [Types & errors](/library/types.md): Enums, Result, Error --- # Connecting _Open, find, and hand the box back_ A Medius box bridges a mouse and a PC over USB-serial. The `medius` crate is the Rust client and `Device` is the handle; opening one finds the box, runs the [handshake](/native/connection.md), and starts the background threads in one call. See also: [choosing a port](/library/guides/connection.md#choosing-a-port), [threading](/library/guides/connection.md#threading), [keepalive & teardown](/library/guides/connection.md#keepalive), and the box [handshake](/native/connection.md#handshake). ## Open a device _Auto-detect, or a path you already have_ ```text fn Device::open(path: impl AsRef) -> Result ``` _Blocks_ ```text fn Device::find() -> Result ``` _Blocks_ ```text fn Device::find_medius() -> Vec ``` _No round-trip_ `open` and `find` block on the [handshake](/native/connection.md#handshake). Auto-detect matches on [USB identity](/native/transport.md) (vid `0x1A86`, pid `0x55D3`), the WCH CH343 bridge in every box. #### FUNCTIONS | Function | Description | | --- | --- | | `open` | Opens a serial path you already have (Linux `/dev/ttyACM0`, Windows `COM3`). | | `find` | Opens the first matching port, or returns [`Error::NotFound`](/library/types/errors.md). | | `find_medius` | Lists every match as a [`PortInfo`](/library/types/structs.md#port-info) without opening one. | #### EXAMPLE ```rust use medius::Device; // auto-detect the box: let dev = Device::find()?; // or, open a path you already know: let dev = Device::open("/dev/ttyACM0")?; ``` ## Zero config _No settings struct, just two defaults_ Nothing to configure; two read-only defaults bound the [`QUERY`](/native/commands/requests.md#requests) wait and keepalive timer. | Constant | Value | | --- | --- | | `DEFAULT_QUERY_TIMEOUT` | `1 s` | | `DEFAULT_KEEPALIVE_CADENCE` | `500 ms` | #### EXAMPLE ```rust use medius::{DEFAULT_QUERY_TIMEOUT, DEFAULT_KEEPALIVE_CADENCE}; println!("query timeout: {:?}", DEFAULT_QUERY_TIMEOUT); // 1s println!("keepalive cadence: {:?}", DEFAULT_KEEPALIVE_CADENCE); // 500ms ``` ## Async device _The same link, with awaitable queries_ ```text fn AsyncDevice::open(path: impl AsRef) -> Result ``` _Blocks_ ```text fn AsyncDevice::find() -> Result ``` _Blocks_ ```text fn into_async(self) -> AsyncDevice ``` _No round-trip_ ```text fn into_inner(self) -> Device ``` _No round-trip_ Behind the `async` feature, [`AsyncDevice`](/library/features/async.md) turns the reply-reading queries into futures; the [fire-and-forget](/native/injection.md#fire-and-forget) calls stay synchronous. Construct one with `AsyncDevice::find`, `open` by path, or `into_async`; full surface on the async feature page. ```bash cargo add medius --features async ``` #### EXAMPLE ```rust use futures::executor::block_on; use medius::AsyncDevice; // discover and open directly as async: let dev = AsyncDevice::find()?; let version = block_on(dev.query_version())?; // awaits the reply dev.move_rel(10, 0)?; // fire-and-forget, stays sync // or open a path you already have: let dev = AsyncDevice::open("/dev/ttyACM0")?; ``` --- # Discovery _Find and open one box out of several_ With more than one box plugged in, [`find`](/library/connection.md#open) just opens the first match. These calls enumerate every box and open a specific one by a stable [identity](/library/discovery.md#identity), or by the kind of device it clones. See also: [connecting](/library/connection.md), [choosing a port](/library/guides/connection.md#choosing-a-port), and the box [handshake](/native/connection.md#handshake). ## list _Enumerate every connected box_ ```text fn list() -> Vec ``` _Blocks_ Opens each connected box in turn, handshakes, reads its [`Version`](/library/types/structs.md#version) (with the box MAC and name) and cloned [`DeviceInfo`](/library/types/structs.md#device-info), then closes it, returning one [`BoxInfo`](/library/discovery.md#box-info) per box. Use it to show a picker, or to choose a box yourself. #### EXAMPLE ```rust use medius::Device; for b in Device::list() { // id() is the box MAC hex; name() is its readable label; b.device displays as "VVVV:PPPP product". println!("{} {} {} {}", b.id(), b.name(), b.device, b.port.path); } ``` ## open_by_id _Open the box with a given identity_ ```text fn open_by_id(id: &str) -> Result ``` _Blocks_ Opens the box whose identity matches `id`: either the device MAC hex (from [`Version::mac_hex`](/library/types/structs.md#version)) or the CH343 [serial](/library/types/structs.md#port-info). Returns [`Error::NotFound`](/library/types/errors.md) when no connected box matches. #### EXAMPLE ```rust use medius::Device; // the MAC hex printed by Device::list(), stable across replugs: let device = Device::open_by_id("123456789abc")?; ``` ## find_mouse_box _Open the first box cloning a mouse_ ```text fn find_mouse_box() -> Result ``` _Blocks_ Opens the first box whose clone's [`DeviceKind`](/library/types/enums.md#device-kind) is a mouse. Handy when one box clones a mouse and another a keyboard: it grabs the right one without naming an id. Returns [`Error::NotFound`](/library/types/errors.md) if no connected box matches. #### EXAMPLE ```rust use medius::Device; let mouse_box = Device::find_mouse_box()?; mouse_box.move_rel(10, 0)?; ``` ## find_keyboard_box _Open the first box cloning a keyboard_ ```text fn find_keyboard_box() -> Result ``` _Blocks_ The keyboard counterpart of [`find_mouse_box`](/library/discovery.md#find-mouse-box): opens the first box whose clone is a keyboard. #### EXAMPLE ```rust use medius::{Device, Key}; let kbd_box = Device::find_keyboard_box()?; kbd_box.press(Key::A)?; ``` ## find_where _Open the first box matching a predicate_ ```text fn find_where(pred: impl Fn(&BoxInfo) -> bool) -> Result ``` _Blocks_ The general form the `find_*_box` helpers build on: opens the first box whose [`BoxInfo`](/library/discovery.md#box-info) satisfies `pred`. Match on any field, e.g. a specific vendor id or product string. Returns [`Error::NotFound`](/library/types/errors.md) when none match. #### EXAMPLE ```rust use medius::Device; // the box cloning a Logitech device: let device = Device::find_where(|b| b.device.vid == 0x046D)?; ``` ## BoxInfo _One discovered box_ One entry from [`Device::list`](/library/discovery.md#list) (and the value[`find_where`](/library/discovery.md#find-where)'s predicate sees): the box's control port, firmware version, and the device it clones. | Field | Type | Meaning | | --- | --- | --- | | `port` | [`PortInfo`](/library/types/structs.md#port-info) | The control port (path + CH343 serial). | | `version` | [`Version`](/library/types/structs.md#version) | The firmware version, with the box MAC and [name](/library/options.md#set-name). | | `device` | [`DeviceInfo`](/library/types/structs.md#device-info) | The device it clones. | | Method | Returns | Meaning | | --- | --- | --- | | `id()` | `String` | The box identity: the MAC hex, as passed to [`open_by_id`](/library/discovery.md#open-by-id). | | `name()` | `&str` | The box's human-readable [name](/library/options.md#set-name) (from `version.name`), a display label rather than an opener key. | | `serial()` | `Option<&str>` | The CH343 adapter's serial, when it has one. | ## Identity & reconnect _The same physical box, across replugs_ A box has a stable identity: the device chip's base MAC (from [`Version::mac_hex`](/library/types/structs.md#version)) and the CH343 adapter's serial. Serial-port paths renumber when you replug, but the identity does not, so [`open_by_id`](/library/discovery.md#open-by-id) re-finds the same box every time. Opening a box anchors [`reconnect`](/library/lifecycle.md#reconnect) to that identity. An automatic reconnect re-finds the _same_ physical box even if the ports renumbered, and never adopts a different box that happens to be plugged in. The readable partner to that MAC is the box's [name](/library/options.md#set-name) (from [`Version::name`](/library/types/structs.md#version)), a display label for telling boxes apart, not an opener key. ## On AsyncDevice _The same discovery, awaitable device_info_ ```text fn AsyncDevice::list() -> Vec ``` ```text fn AsyncDevice::open_by_id(id: &str) -> Result ``` ```text fn AsyncDevice::find_mouse_box() -> Result ``` ```text fn AsyncDevice::find_keyboard_box() -> Result ``` _Blocks_ [`AsyncDevice`](/library/features/async.md) mirrors the discovery constructors; they block on the per-box handshake, like their [`Device`](/library/connection.md#async) counterparts. The reply-reading [`device_info`](/library/requests.md#device-info) query is the awaitable part. ```rust use medius::AsyncDevice; let device = AsyncDevice::find_mouse_box()?; // blocks on the handshake let info = futures::executor::block_on(device.device_info())?; // awaits the reply ``` --- # Inject _Press and release any input_ One field-generic verb drives every momentary input. [`inject`](/library/inject.md#inject) takes any [`Usage`](/library/types/enums.md#usage) (a button, key, or media usage); [`press`](/library/inject.md#press), [`release`](/library/inject.md#press), and [`force_release`](/library/inject.md#press) are convenience wrappers. Each call queues one [fire-and-forget](/native/injection.md#fire-and-forget) [`INJECT`](/native/commands/inject.md#inject) frame. ## inject _Press or release any usage_ ```text fn inject(&self, usage: impl Into, action: Action) -> Result<()> ``` _Fire-and-forget_ `usage` is any [`Usage`](/library/types/enums.md#usage): a [`Button`](/library/types/enums.md#button), a [`Key`](/library/types/structs.md#key), and a [`MediaKey`](/library/types/structs.md#media-key) all convert into one, so one verb drives every input class. `action` is the shared [`Action`](/library/types/enums.md#action) tri-state: press, soft-release, or force-release. A usage the cloned device can't report is a no-op, and there's no firmware click or chord (compose a press then a client-timed release). [`reset`](/library/admin.md#reset) releases every override. #### EXAMPLE ```rust use medius::{Button, Key, MediaKey, Action}; device.inject(Button::Left, Action::Press)?; // mouse button device.inject(Key::LEFT_SHIFT, Action::Press)?; // keyboard key device.inject(MediaKey::VOLUME_UP, Action::Press)?; // media key ``` ## Press and release _press, release, force_release_ ```text fn press(&self, usage: impl Into) -> Result<()> ``` ```text fn release(&self, usage: impl Into) -> Result<()> ``` ```text fn force_release(&self, usage: impl Into) -> Result<()> ``` _Fire-and-forget_ The convenience wrappers over [`inject`](/library/inject.md#inject), generic over any [`Usage`](/library/types/enums.md#usage). `press` holds it down, `release` clears the box's press or force while a physical hold stays down, and `force_release` forces it up over a physical press. #### EXAMPLE ```rust use medius::{Button, Key}; device.press(Button::Left)?; // held down device.release(Button::Left)?; // your press cleared; a physical hold survives device.force_release(Key::LEFT_GUI)?; // forced up even under a physical hold ``` ## On AsyncDevice _Same calls, still synchronous_ [`AsyncDevice`](/library/features/async.md) queues these frames too, so no `.await`; only the query methods are `async`. #### EXAMPLE ```rust use medius::{Button, Key}; // async_device: medius::AsyncDevice async_device.press(Button::Left)?; // no .await, it just queues the frame async_device.press(Key::ESCAPE)?; ``` > **Note** > > Build an `AsyncDevice` with `cargo add medius --features async` and [`AsyncDevice::open`](/library/connection.md#async) or `Device::into_async`. --- # Move _Cursor motion and scroll_ One field-generic verb, [`move_axis`](/library/move.md#move), drives the relative axes. [`move_rel`](/library/move.md#move-rel) and [`wheel`](/library/move.md#wheel) are thin wrappers over it. Each call queues one [fire-and-forget](/native/injection.md#fire-and-forget) [`MOVE`](/native/commands/move.md#move) frame. | You want | Method | Same as | | --- | --- | --- | | move the cursor | [`move_rel(dx, dy)`](/library/move.md#move-rel) | `move_axis(Motion::Cursor { dx, dy })` | | scroll the wheel | [`wheel(delta)`](/library/move.md#wheel) | `move_axis(Motion::Wheel(dz))` | ## move_axis _Field-generic motion verb_ ```text fn move_axis(&self, motion: Motion) -> Result<()> ``` _Fire-and-forget_ `motion` is a [`Motion`](/library/types/enums.md#motion): `Cursor { dx, dy }` for pointer movement or `Wheel(dz)` for scroll. Backs the [`MOVE`](/native/commands/move.md#move) command. #### EXAMPLE ```rust use medius::Motion; device.move_axis(Motion::Cursor { dx: 20, dy: 20 })?; // right and down device.move_axis(Motion::Wheel(1))?; // one notch up ``` ## move_rel _Relative cursor movement_ ```text fn move_rel(&self, dx: i16, dy: i16) -> Result<()> ``` _Fire-and-forget_ A wrapper over [`move_axis`](/library/move.md#move) with `Motion::Cursor`. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `dx` | `i16` | Horizontal offset in mouse counts. Positive moves right, negative moves left. | | `dy` | `i16` | Vertical offset in mouse counts. Positive moves down, negative moves up (screen-style, not math-style). | Offsets are mouse counts, not pixels, scaled by the OS pointer-speed and acceleration curve. Both span the full `i16` range (`-32768 to 32767`). #### EXAMPLE ```rust device.move_rel(20, 20)?; // right and down device.move_rel(-40, 0)?; // left device.move_rel(0, -10)?; // up ``` ## wheel _Wheel scroll_ ```text fn wheel(&self, delta: i16) -> Result<()> ``` _Fire-and-forget_ A wrapper over [`move_axis`](/library/move.md#move) with `Motion::Wheel`. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `delta` | `i16` | Scroll steps. Positive scrolls up, negative scrolls down. | `delta` spans the full `i16` range (`-32768 to 32767`) and feeds the same [accumulator](/native/injection.md#state) as cursor motion, pacing large values across reports. #### EXAMPLE ```rust device.wheel(3)?; // up three notches device.wheel(-1)?; // down one notch ``` ## On AsyncDevice _Movement stays synchronous_ [`AsyncDevice`](/library/features/async.md) keeps `move_axis`, `move_rel`, and `wheel` synchronous: no `.await`, same signatures. The [`block_on`](https://docs.rs/futures/latest/futures/executor/fn.block_on.html) pattern is only for async queries. #### EXAMPLE ```rust let dev = Device::find()?.into_async(); dev.move_rel(40, 0)?; // no .await dev.wheel(1)?; ``` --- # Requests _Asking the box a question and waiting for the answer_ Unlike [fire-and-forget](/native/injection.md#fire-and-forget), the queries are blocking: a question frame out, one answer frame back. They are [`query_version`](/library/requests.md#version), [`query_health`](/library/requests.md#health), [`device_info`](/library/requests.md#device-info), [`caps`](/library/requests.md#caps), [`query_rate`](/library/requests.md#query-rate), [`query_stats`](/library/requests.md#query-stats), [`query_locks`](/library/requests.md#query-locks), [`query_catch`](/library/requests.md#query-catch), each covered below. The [clip handle](/library/clip.md#handle) adds [`query_status`](/library/requests.md#clip-status) and [`query_config`](/library/requests.md#clip-config), also here. ## query_version _Firmware identity, round-trip_ ```text fn query_version(&self) -> Result ``` _Blocks_ Returns a [`Version`](/library/types/structs.md#version). The box's [name](/library/options.md#set-name) rides on it, in the `name` field. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; // or Device::open("/dev/ttyACM0")? let v = device.query_version()?; println!("{v}"); // fw 3.0.0 println!("proto {}", v.proto_ver); // proto 3 println!("name {}", v.name); // Loki ``` > **Note** > > [`Device::find()`](/library/connection.md#open) already runs a version query during the handshake; calling `query_version` again just re-reads it. ## query_health _Is the mouse-to-box-to-PC chain live_ ```text fn query_health(&self) -> Result ``` _Blocks_ Returns a [`Health`](/library/types/structs.md#health), eight booleans from one status byte. `link_up`, `mouse_attached`, and `clone_configured` must all be true before [injection](/native/injection.md) has anywhere to land. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let h = device.query_health()?; if h.link_up && h.mouse_attached && h.clone_configured { // chain is live, safe to inject } else { eprintln!("not ready: {h:?}"); } ``` ## device_info _USB identity, kind, and product of the clone_ ```text fn device_info(&self) -> Result ``` _Blocks_ Returns a [`DeviceInfo`](/library/types/structs.md#device-info): the `vid`, `pid`, USB version, a [`DeviceKind`](/library/types/enums.md#device-kind), and the `product` string the box read off the real device. The clone sits on the game PC's bus, so this is the only way to see it from the control link. Every field is zero/empty when nothing is cloned. `Display` prints `VVVV:PPPP product`. #### EXAMPLE ```rust use medius::{Device, DeviceKind}; let device = Device::find()?; let d = device.device_info()?; if d.vid == 0 { eprintln!("nothing cloned yet"); } else { println!("{d}"); // 046D:C08B G502 println!("usb {:#06x}", d.bcd_usb); println!("kind={} serial={} bos={}", d.kind, d.has_serial, d.has_bos); if d.kind == DeviceKind::Mouse { // the clone is a mouse } } ``` ## caps _Feature-detect the whole device_ ```text fn caps(&self) -> Result ``` _Blocks_ One query describes the whole cloned device. Returns a [`Caps`](/library/types/structs.md#caps) with a [`mouse`](/library/types/structs.md#mouse-caps) half and a [`keyboard`](/library/types/structs.md#kbd-caps) half, plus the per-class change-driven flags. Use it for feature detection: an [`inject`](/library/inject.md#inject) for a usage the device lacks is a silent no-op, so the counts tell you what is real. A class that is not present reads all-zero; `has_mouse()` and `has_keyboard()` say which are bound. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let caps = device.caps()?; println!("{} buttons", caps.mouse.n_buttons); if caps.mouse.has_wheel { device.wheel(1)?; } if caps.has_keyboard() && caps.keyboard.has_consumer { device.press(medius::MediaKey::MUTE)?; } ``` ## query_rate _Read the live native report rate_ ```text fn query_rate(&self) -> Result ``` _Blocks_ Returns a [`Rate`](/library/types/structs.md#rate). `native_hz()` converts the period to a frequency, returning `None` while `native_period_us` is still `0` (not learned yet). `confident` is true once the estimator window is full and the value is trustworthy. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let r = device.query_rate()?; match r.native_hz() { Some(hz) if r.confident => println!("{hz:.0} Hz"), Some(hz) => println!("{hz:.0} Hz (still settling)"), None => println!("rate not learned yet"), } ``` ## query_stats _Read the delivery counters_ ```text fn query_stats(&self) -> Result ``` _Blocks_ Returns a [`Stats`](/library/types/structs.md#stats). `inject_emits` counts pure-injection reports emitted; a nonzero `tx_drops` or `tx_wedges` is the signal that delivery degraded under load. The narrowed counters saturate, so a maxed field clamps instead of wrapping. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let s = device.query_stats()?; println!("{} emits", s.inject_emits); if s.tx_drops > 0 || s.tx_wedges > 0 { eprintln!("delivery degraded: {} drops, {} wedges", s.tx_drops, s.tx_wedges); } ``` ## query_locks _Read the active input locks_ ```text fn query_locks(&self) -> Result ``` _Blocks_ Returns a [`Locks`](/library/types/structs.md#locks), the list of inputs currently blocked by [`lock`](/library/lock.md#lock). `entries()` walks them and `is_locked(target, direction)` answers whether one particular lock is set. Read it to confirm a lock landed, or to mirror the box's lock state in a UI. #### EXAMPLE ```rust use medius::{Device, Axis, LockDirection}; let device = Device::find()?; let locks = device.query_locks()?; if locks.is_locked(Axis::X, LockDirection::Both) { println!("horizontal motion is frozen"); } ``` ## query_catch _Read the active catch subscription_ ```text fn query_catch(&self) -> Result ``` _Blocks_ Returns a [`CatchState`](/library/types/structs.md#catch-state): the `mask` currently streaming via [`catch_events`](/library/catch.md#catch-events), plus `dropped`, the box-side count of events shed under back-pressure. Read it after subscribing to confirm the mask took, or to reflect the live catch mask in your own UI. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let c = device.query_catch()?; if !c.mask.is_empty() { println!("catching {:?}, {} dropped", c.mask, c.dropped); } ``` ## query_status (clip) _Read the buffered-clip ring depth, progress, and playback state_ ```text fn query_status(&self) -> Result ``` _Blocks_ On the [`ClipHandle`](/library/clip.md#handle) from [`device.clip()`](/library/clip.md#clip), not `Device` itself. Returns a [`ClipStatus`](/library/types/structs.md#clip-status): `state`, ring `free`, retained `played`/`total`, the drain counters, and the `held` usages. Pace clip top-ups off `free`, and watch `state` for a [`Faulted`](/library/types/enums.md#clip-state) re-sync or for playback reaching `Idle`. Backs [`QUERY(CLIP)`](/native/commands/requests.md#clip). #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let clip = device.clip(); let s = clip.query_status()?; if s.state == medius::ClipState::Faulted { clip.clear()?; } println!("{} free, {} played", s.free, s.played); ``` ## query_config (clip) _Read the whole clip config back_ ```text fn query_config(&self) -> Result ``` _Blocks_ The config view of the same [`QUERY(CLIP)`](/native/commands/requests.md#clip) frame [`query_status`](/library/requests.md#clip-status) reads, also on the [`ClipHandle`](/library/clip.md#handle). Returns a [`ClipSettings`](/library/types/structs.md#clip-settings) with the auto-lock, loop, retain, finalized flag, and [triggers](/library/clip.md#triggers) you set; nothing is write-only, every setting round-trips. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let cfg = device.clip().query_config()?; println!("{} triggers, loop={}", cfg.triggers.len(), cfg.loop_); ``` ## Async queries _The same queries on AsyncDevice_ ```text async fn query_version(&self) -> Result ``` ```text async fn query_health(&self) -> Result ``` ```text async fn device_info(&self) -> Result ``` ```text async fn caps(&self) -> Result ``` ```text async fn query_rate(&self) -> Result ``` ```text async fn query_stats(&self) -> Result ``` ```text async fn query_locks(&self) -> Result ``` ```text async fn query_catch(&self) -> Result ``` _Blocks_ ```bash cargo add medius --features async ``` With the `async` feature, `Device::into_async()` yields an [`AsyncDevice`](/library/features/async.md) whose queries are futures; other methods stay synchronous. The crate is runtime-agnostic (no tokio), so drive a future with anything, such as [`futures::executor::block_on`](https://docs.rs/futures/latest/futures/executor/fn.block_on.html). #### EXAMPLE ```rust use futures::executor::block_on; use medius::Device; let device = Device::find()?.into_async(); let v = block_on(device.query_version())?; let h = block_on(device.query_health())?; println!("{v} link_up={}", h.link_up); ``` --- # Admin _Reboot a chip and return to passthrough_ Box-maintenance calls: [`reboot`](/library/admin.md#reboot) restarts one of the two chips, [`reset`](/library/admin.md#reset) drops the box back to [passthrough](/native/injection.md). Both are [fire-and-forget](/native/injection.md#fire-and-forget): send one frame, no reply. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; // first box on the system, handshake done device.reset()?; // back to passthrough ``` [`find`](/library/connection.md#open) opens the first box; related calls [`reapply`](/library/lifecycle.md#reapply) and [`reconnect`](/library/lifecycle.md#reconnect) are on the [Lifecycle](/library/lifecycle.md) page. ## reset _Clear all injection, return to passthrough_ ```text fn reset(&self) -> Result<()> ``` _Fire-and-forget_ #### EFFECT | State | What reset does | | --- | --- | | Box accumulator | Zeroed. This is the box's running total of injected motion and scroll not yet emitted to the PC. | | Box overrides | All released. An [override](/library/inject.md) is a per-usage decision to hold an input down or up. | | Library held-state | Cleared. The library forgets which overrides it was holding, so a later [`reapply`](/library/lifecycle.md#reapply) or [`reconnect`](/library/lifecycle.md#reconnect) re-asserts nothing. | Sends one [`RESET`](/native/commands/admin.md#reset) frame and clears the library's held state. Afterward the box behaves as if nothing was injected. #### EXAMPLE ```rust use medius::Button; device.move_rel(40, 0)?; // nudge the cursor 40 right device.press(Button::Left)?; // hold left down device.release(Button::Left)?; // let it back up device.reset()?; // drop all of the above, back to passthrough ``` ## reboot _Restart or download-mode one of the two chips_ ```text fn reboot(&self, target: RebootTarget) -> Result<()> ``` _Fire-and-forget_ [`RebootTarget`](/library/types/enums.md#reboot-target) picks the chip and whether it comes back running its firmware or in download mode; the four variants and their bytes are on [Types](/library/types/enums.md). See the native [`REBOOT`](/native/commands/admin.md#reboot) command for the wire layout. #### EXAMPLE ```rust use medius::RebootTarget; device.reboot(RebootTarget::DeviceRun)?; // restart the chip you're talking to ``` > **Warning** > > A `Download` variant leaves the chip in ROM download mode: it stops acting as a mouse and stops answering until reflashed or power-cycled. Don't send one unless you're about to flash. > **Note** > > Rebooting the device chip drops the serial link, so the call can return `Ok` as the connection goes away. The reader thread auto-reconnects, or force it with [`reconnect`](/library/lifecycle.md#reconnect). The [flash](/library/features/flash.md) feature issues the download reboot for you, so you rarely send a `Download` variant by hand. ## On AsyncDevice _Still fire-and-forget, no await_ [`AsyncDevice`](/library/features/async.md) re-exposes `reset` and `reboot` unchanged: they expect no reply, so no `.await` and no [`block_on`](https://docs.rs/futures/latest/futures/executor/fn.block_on.html). Only the queries are async. #### EXAMPLE ```rust use medius::{AsyncDevice, RebootTarget}; let device = AsyncDevice::open("/dev/ttyACM0")?; device.reset()?; // sync, no await device.reboot(RebootTarget::HostRun)?; // sync, no await ``` --- # LED _Drive a status LED, or hand it back to the box_ [`led`](/library/led.md#led) overrides one of the box's two green status LEDs, or with [`LedMode::Auto`](/library/types/enums.md#led-mode) returns it to the box's own status display. It's [fire-and-forget](/native/injection.md#fire-and-forget): one frame, no reply. ## led _Override or restore a status LED_ ```text fn led(&self, target: LedTarget, mode: LedMode, level: u8) -> Result<()> ``` _Fire-and-forget_ [`LedTarget`](/library/types/enums.md#led-target) picks which chip's LED, and [`LedMode`](/library/types/enums.md#led-mode) picks what to drive it to; both enums and their bytes are on [Types](/library/types/enums.md).`level` is brightness `0..=255`, used by `Solid` and `Blink` and ignored for `Off` and `Auto`. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `target` | [`LedTarget`](/library/types/enums.md#led-target) | Which chip's LED: `Device`, `Host`, or `Both`. | | `mode` | [`LedMode`](/library/types/enums.md#led-mode) | `Auto` restores the status display; `Off`, `Solid`, and `Blink` override it. | | `level` | `u8` | Brightness 0-255 for `Solid` and `Blink`; ignored otherwise. | An override holds until you send `Auto`, and the box also reverts the LED to its status display on control-PC silence, on [`reset`](/library/admin.md#reset), or on inter-chip link loss. See the native [`LED`](/native/commands/led.md#led) command for the status patterns each chip shows. #### EXAMPLE ```rust use medius::{Device, LedTarget, LedMode}; let device = Device::find()?; device.led(LedTarget::Both, LedMode::Blink, 200)?; // both LEDs blink, bright device.led(LedTarget::Device, LedMode::Auto, 0)?; // hand the device LED back ``` ## On AsyncDevice _Still fire-and-forget, no await_ [`AsyncDevice`](/library/features/async.md) re-exposes `led` unchanged: it expects no reply, so there's no `.await` and no [`block_on`](https://docs.rs/futures/latest/futures/executor/fn.block_on.html). Only the queries are async. #### EXAMPLE ```rust use medius::{AsyncDevice, LedTarget, LedMode}; let device = AsyncDevice::open("/dev/ttyACM0")?; device.led(LedTarget::Host, LedMode::Solid, 128)?; // sync, no await ``` --- # Lock _Block one physical input; injection still drives it_ A lock blocks the _physical_ device from one input, while host [injection](/native/injection.md) still drives that same input. Lock what the user shouldn't touch, then drive it yourself. ``` a locked input: physical --X blocked injected --> still reaches the PC ``` | Block a... | Lock | Release | | --- | --- | --- | | relative axis (X / Y / wheel) | [`lock`](/library/lock.md#lock) / [`lock_axis`](/library/lock.md#lock-axis) | [`unlock`](/library/lock.md#unlock) / [`unlock_axis`](/library/lock.md#lock-axis) | | button, key, or media usage | [`lock`](/library/lock.md#lock) | [`unlock`](/library/lock.md#unlock) | | a whole class (blanket) | [`lock_all`](/library/lock.md#lock-all) | [`unlock_all`](/library/lock.md#lock-all) | All are [fire-and-forget](/native/injection.md#fire-and-forget): one frame, no reply. [`query_locks`](/library/requests.md#query-locks) reads the active set. ## lock _Block a physical input_ ```text fn lock(&self, target: impl Into, direction: LockDirection) -> Result<()> ``` _Fire-and-forget_ [`LockTarget`](/library/types/enums.md#lock-target) picks the input and [`LockDirection`](/library/types/enums.md#lock-direction) picks the sign or edge. For an axis or the wheel the direction is a sign, so you can block scrolling up but not down; for a usage it's an edge, so you can block the press but not the release. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `target` | `impl Into<[LockTarget](/library/types/enums.md#lock-target)>` | An [`Axis`](/library/types/enums.md#axis) (X, Y, or wheel) or any [`Usage`](/library/types/enums.md#usage) (a button, key, or media usage). | | `direction` | [`LockDirection`](/library/types/enums.md#lock-direction) | `Both`, `Positive` (axis +, usage press), or `Negative` (axis -, usage release). | A lock blocks the physical device only, and holds until you [`unlock`](/library/lock.md#unlock) it. The box also clears every lock on control-PC silence, on [`reset`](/library/admin.md#reset), or on inter-chip link loss. See the native [`LOCK`](/native/commands/lock.md#lock) command for the wire layout. #### EXAMPLE ```rust use medius::{Device, Axis, Button, Key, LockDirection}; let device = Device::find()?; device.lock(Axis::X, LockDirection::Both)?; // freeze horizontal motion device.lock(Button::Left, LockDirection::Positive)?; // block left-click press device.lock(Key::LEFT_GUI, LockDirection::Both)?; // block the GUI/Windows key device.move_rel(50, 0)?; // injection still moves X ``` ## unlock _Clear a block_ ```text fn unlock(&self, target: impl Into, direction: LockDirection) -> Result<()> ``` _Fire-and-forget_ The inverse of [`lock`](/library/lock.md#lock): same `target` and `direction`, but it clears the block instead of setting it. Hand a physical input back to the user. #### EXAMPLE ```rust use medius::{Device, Axis, LockDirection}; let device = Device::find()?; device.unlock(Axis::X, LockDirection::Both)?; // hand horizontal motion back ``` ## lock_axis / unlock_axis _Block a relative axis by sign_ ```text fn lock_axis(&self, axis: Axis, direction: LockDirection) -> Result<()> ``` ```text fn unlock_axis(&self, axis: Axis, direction: LockDirection) -> Result<()> ``` _Fire-and-forget_ Convenience for [`lock`](/library/lock.md#lock) / [`unlock`](/library/lock.md#unlock) with an [`Axis`](/library/types/enums.md#axis). The direction is a sign, so a positive-only lock freezes scroll-up while scroll-down still passes. #### EXAMPLE ```rust use medius::{Device, Axis, LockDirection}; let device = Device::find()?; device.lock_axis(Axis::Wheel, LockDirection::Positive)?; // block scroll up, keep scroll down device.unlock_axis(Axis::Wheel, LockDirection::Positive)?; ``` ## lock_all / unlock_all _Blanket-block a whole class_ ```text fn lock_all(&self, what: Blanket, direction: LockDirection) -> Result<()> ``` ```text fn unlock_all(&self, what: Blanket, direction: LockDirection) -> Result<()> ``` _Fire-and-forget_ Block an entire input group at once with a [`Blanket`](/library/types/enums.md#blanket) (`Aim`, `Wheel`, `Buttons`, `Keys`, or `Media`); `direction` applies to the whole group. Injection still drives any field you choose to. #### EXAMPLE ```rust use medius::{Device, Blanket, LockDirection}; let device = Device::find()?; device.lock_all(Blanket::Keys, LockDirection::Both)?; // every physical key blocked device.unlock_all(Blanket::Keys, LockDirection::Both)?; ``` ## On AsyncDevice _locks fire, query_locks awaits_ [`AsyncDevice`](/library/features/async.md) keeps every lock call synchronous (`lock`/`unlock`, `lock_axis`, and `lock_all` with their unlock pairs) since they expect no reply; `query_locks` is a future like the other queries. #### EXAMPLE ```rust use futures::executor::block_on; use medius::{AsyncDevice, Axis, LockDirection}; let device = AsyncDevice::open("/dev/ttyACM0")?; device.lock(Axis::Y, LockDirection::Both)?; // sync, no await let locks = block_on(device.query_locks())?; // query awaits ``` --- # Catch _Stream the physical mouse and keyboard input_ [`catch_events`](/library/catch.md#catch-events) subscribes to the user's real input and hands back an [`EventStream`](/library/catch.md#event-stream) of [`CatchEvent`](/library/types/enums.md#catch-event) snapshots, relative motion and held-usage sets, captured before any [`lock`](/library/lock.md) suppression or [injection](/library/inject.md). Drop the stream to unsubscribe. ## catch_events _Subscribe to the physical-input stream_ ```text fn catch_events(&self, mask: CatchMask) -> Result ``` _Fire-and-forget_ [`CatchMask`](/library/types/structs.md#catch-mask) picks which classes of change emit an event: `MOTION`, `WHEEL`, `BUTTONS`,`KEYS`, `MEDIA`, combined with `|`, or `CatchMask::all()` for the full mirror. The returned [`EventStream`](/library/catch.md#event-stream) receives every event; the subscribe itself sends one frame and doesn't wait for a reply. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `mask` | [`CatchMask`](/library/types/structs.md#catch-mask) | Bitmask selecting which input classes emit events (see above). | The subscription is held alive by the library's keepalive (which re-asserts it after a device-side blip) and across a [reconnect](/library/lifecycle.md#reconnect); it clears like injection: on control-PC silence, a [`reset`](/library/admin.md#reset) (which ends the stream, so its `recv` returns `Err`), or link loss. The reported input is the user's _physical_ input; a locked or injected target still reports its real hand value here. See the native [`CATCH`](/native/commands/catch.md#catch) command for the wire layout. #### EXAMPLE ```rust use medius::{Device, CatchMask, CatchEvent, Button}; let device = Device::find()?; let events = device.catch_events(CatchMask::all())?; // or MOTION | BUTTONS | KEYS | MEDIA while let Ok(event) = events.recv() { match event { CatchEvent::Motion(m) => println!("dx={} dy={} dz={}", m.dx, m.dy, m.dz), CatchEvent::Usages(u) if u.is_held(Button::Side1) => { // the side button is held; rebind it... } CatchEvent::Usages(u) => println!("{} usages held", u.usages.len()), } } // dropping `events` unsubscribes ``` ## EventStream _Receive physical-input reports_ The handle [`catch_events`](/library/catch.md#catch-events) returns. Pull [`CatchEvent`](/library/types/enums.md#catch-event) snapshots with whichever method fits your loop; cloning shares the queue (like [`LogStream`](/library/diagnostics.md#logs)). When the stream and all its clones drop, the subscription ends and the box returns to passthrough. #### METHODS | Method | Returns | Description | | --- | --- | --- | | `recv()` | `Result` | Block until the next event. | | `try_recv()` | `Option` | The next buffered event, or `None` (never blocks). | | `recv_timeout(dur)` | `Option` | Block up to `dur`; `None` on timeout. | | `try_iter()` | `impl Iterator` | Drain every buffered event without blocking. | | `recv_async().await` | `Result` | Await the next event (`async` feature), runtime-agnostic. | | `dropped()` | `u64` | Events lost host-side because this consumer fell behind. | > **Note** > > The buffer is bounded and lossy: a slow consumer drops the OLDEST events, keeping the freshest input (count them with `dropped()`). The box's own drop count, under back-pressure on the wire, is on [`query_catch`](/library/requests.md#query-catch). ## On AsyncDevice _catch_events fires, the stream awaits_ [`AsyncDevice`](/library/features/async.md) keeps `catch_events` synchronous (it just sends the subscribe and returns the stream) while the stream itself offers `recv_async().await`. `query_catch` is a future, like the other queries. #### EXAMPLE ```rust use medius::{AsyncDevice, CatchMask}; let device = AsyncDevice::open("/dev/ttyACM0")?; let events = device.catch_events(CatchMask::BUTTONS)?; // sync, no await let report = events.recv_async().await?; // stream awaits ``` --- # Options _Persistent box settings_ Options are persistent box settings, each one set and read on its own. There are four: imperfect clones, movement riding, emit-rate pacing, and the box name. All persist in NVS and survive a reboot. See the native [`OPTION`](/native/commands/option.md) command for the wire contract. | Option | Set | Read | | --- | --- | --- | | imperfect clone | [`allow_imperfect_clones`](/library/options.md#allow-imperfect-clones) | [`query_imperfect`](/library/options.md#query-imperfect) | | movement riding | [`set_movement_riding`](/library/options.md#set-movement-riding) | [`query_movement_riding`](/library/options.md#query-movement-riding) | | emit-rate pacing | [`set_emit_pace`](/library/options.md#set-emit-pace) | [`query_emit_pace`](/library/options.md#query-emit-pace) | | box name | [`set_name`](/library/options.md#set-name) / [`clear_name`](/library/options.md#clear-name) | [`Version::name`](/library/types/structs.md#version) | ## allow_imperfect_clones _Clone an over-capacity device anyway_ ```text fn allow_imperfect_clones(&self, allow: bool) -> Result<()> ``` _Fire-and-forget_ By default the box refuses a device it can't clone faithfully. `true` opts into cloning an over-capacity device anyway, the rest faithful and the over-capacity interface dead; `false` is faithful-only (the default). It's persisted in NVS. When the setting changes for an _attached over-capacity_ device the box reboots itself to re-clone, so it lands without unplugging anything; a normal device is unaffected (no reboot). #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `allow` | `bool` | Clone an over-capacity device anyway, or stay faithful-only. | #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; device.allow_imperfect_clones(true)?; // reboots + re-clones if an over-capacity device is attached ``` ## set_movement_riding _Inject motion only on a native move_ ```text fn set_movement_riding(&self, window: Option) -> Result<()> ``` _Fire-and-forget_ `Some(window)` turns movement riding on: injected cursor and wheel motion ride a native cursor-motion report seen within `window`, the box emits no synthetic motion frame, and motion left unridden past the window is dropped rather than dumped on the next move. So injected motion's report density matches the real mouse's, erasing the density tell. `None` turns it off (the default). The window rounds to whole milliseconds, a non-zero `Some` is at least 1 ms, and it clamps to 65535 ms; persisted in NVS. The tradeoff is deliberate: pure idle injection, moving the cursor while the user holds still, stops working while riding is on. Button, key, and media injection are unaffected. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `window` | `Option` | `Some` with the ride window, or `None` to turn it off. | #### EXAMPLE ```rust use std::time::Duration; use medius::Device; let device = Device::find()?; device.set_movement_riding(Some(Duration::from_millis(20)))?; // ride native moves device.set_movement_riding(None)?; // back to gapless fill ``` ## set_emit_pace _Pick what paces injected motion_ ```text fn set_emit_pace(&self, pace: EmitPace) -> Result<()> ``` _Fire-and-forget_ Picks what sets the emit-rate ceiling for injected motion. [`EmitPace::Learned`](/library/types/enums.md#emit-pace) is the default: the box paces injection to the rate the real mouse actually reports at. `EmitPace::Interval` paces to the cloned mouse's declared poll rate (its`bInterval`). `EmitPace::Fixed(hz)` paces to a rate you set; the 1 ms frame clock snaps it to `1000/n` Hz and caps it at 1 kHz. It raises the ceiling only, so idle stays idle (the box still emits a frame solely when injection is pending). Persisted in NVS. The learnt default keeps injected motion's cadence matched to the native mouse. The other modes are for a host that models its own report density and wants the box to stop re-pacing an already-shaped stream. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `pace` | [`EmitPace`](/library/types/enums.md#emit-pace) | `Learned`, `Interval`, or `Fixed(hz)`. | #### EXAMPLE ```rust use medius::{Device, EmitPace}; let device = Device::find()?; device.set_emit_pace(EmitPace::Fixed(1000))?; // emit at a fixed 1 kHz device.set_emit_pace(EmitPace::Learned)?; // back to the learnt native pace ``` ## set_name _Give the box a human-readable name_ ```text fn set_name(&self, name: &str) -> Result<()> ``` _Fire-and-forget_ Sets the box's name, its readable partner to the [MAC](/library/discovery.md#identity). Like the other setters it sends the value and lets the box own the rules: the firmware keeps the leading printable-ASCII run capped at 32 bytes, and an empty string clears it. Unlike the other options it is read not by a query but off [`Version::name`](/library/types/structs.md#version), like the MAC. #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `name` | `&str` | The new name, 1 to 32 printable ASCII characters. | #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; device.set_name("Loki")?; // the box now answers to "Loki" let name = device.query_version()?.name; // read it back off Version ``` ## clear_name _Back to the synthesized default_ ```text fn clear_name(&self) -> Result<()> ``` _Fire-and-forget_ Clears the custom name, reverting the box to a firmware-synthesized `Medius-XXXX` default derived from its MAC. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; device.clear_name()?; // back to "Medius-XXXX" ``` ## query_imperfect _Read the imperfect-clone state_ ```text fn query_imperfect(&self) -> Result ``` _Blocks_ Returns an [`ImperfectStatus`](/library/types/structs.md#imperfect-status): the opt-in toggle, whether the attached device is over-capacity, and whether the live clone went over-capacity anyway with one interface dead. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; let status = device.query_imperfect()?; if status.over_capacity && !status.allowed { // the device was refused; opt in to clone it imperfectly device.allow_imperfect_clones(true)?; } ``` ## query_movement_riding _Read the ride window_ ```text fn query_movement_riding(&self) -> Result> ``` _Blocks_ Returns the current ride window as a `Duration`, or `None` when movement riding is off. #### EXAMPLE ```rust use medius::Device; let device = Device::find()?; match device.query_movement_riding()? { Some(window) => println!("riding, window {window:?}"), None => println!("off"), } ``` ## query_emit_pace _Read the pacing mode and rate_ ```text fn query_emit_pace(&self) -> Result ``` _Blocks_ Returns an [`EmitPaceStatus`](/library/types/structs.md#emit-pace-status): the selected [`EmitPace`](/library/types/enums.md#emit-pace) mode plus `resolved_hz`, the ceiling actually in effect (0 when the pace is learnt/adaptive, or no device is attached yet in `Interval` mode). #### EXAMPLE ```rust use medius::{Device, EmitPace}; let device = Device::find()?; let status = device.query_emit_pace()?; if let EmitPace::Fixed(hz) = status.mode { println!("fixed {hz} Hz, emitting at {} Hz", status.resolved_hz); } ``` ## On AsyncDevice _setters fire, queries await_ [`AsyncDevice`](/library/features/async.md) keeps `allow_imperfect_clones`, `set_movement_riding`, `set_emit_pace`, `set_name`, and `clear_name` fire-and-forget (no await) and makes `query_imperfect`, `query_movement_riding`, and `query_emit_pace` futures, like the other queries. #### EXAMPLE ```rust use std::time::Duration; use medius::Device; let device = Device::find()?.into_async(); device.set_movement_riding(Some(Duration::from_millis(20)))?; // sync, no await let window = device.query_movement_riding().await?; // awaits ``` --- # Clip _Preload input and let the box play it back_ Build a sequence of per-frame input with a [`ClipBuilder`](/library/clip.md#builder), hand it to the [`ClipHandle`](/library/clip.md#handle) from [`Device::clip()`](/library/clip.md#clip), and the box drains one entry per native frame into the same injection state your live [`move`](/library/move.md) and [`inject`](/library/inject.md) calls feed. Playback is box-clocked, so it carries no host scheduling jitter. It's field-generic (mouse, keyboard, and media in one clip) and backs the [`CLIP`](/native/commands/clip.md) commands. ``` 1. build a clip with ClipBuilder clip.move_by(10, 0) clip.press(Button::Left) clip.gap(20) clip.release(...) 2. drive playback through a ClipHandle handle = device.clip(); handle.append(&clip) ──▶ copy the entries into the box's ring handle.start() ──▶ box plays one entry per native frame handle.query_status() ◀── ring depth + progress + playback state handle.stop() ──▶ stop (retained: rewind; streaming: flush) or let the box play it on a physical key, no host round-trip: handle.bind(ClipTrigger::new(Key::F1, Edge::Press, ClipAction::Start)) ``` ## clip _Open a clip handle_ ```text fn clip(&self) -> ClipHandle ``` Returns a [`ClipHandle`](/library/clip.md#handle) bound to this box. The handle owns the append-sequence counter the box uses to spot a dropped append, so keep one handle for a clip session (top it up through that handle) rather than reopening one per append. > **Note** > > Playback lives in the box's RAM: a [reboot](/library/admin.md#reboot) or [reconnect](/library/lifecycle.md#reconnect) drops the loaded clip, so re-preload after one. Its config (auto-lock, loop, retain, and the trigger set) goes too: unlike a held lock or catch subscription, a clip isn't re-asserted for you, so re-set it after a reconnect. A clip needs a cloned mouse, since its frame clock is the mouse's report tick; keyboard and media edges ride that tick. ## ClipBuilder _Build the entry stream_ ```text fn new() -> ClipBuilder ``` Each method appends one per-frame entry, so a builder is a timeline read top to bottom. Motion is a relative delta; an edge (button, key, or media) is an [`Action`](/library/types/enums.md#action) that stays held until a later frame changes it; a `gap` is N idle frames (the box NAKs, like an idle mouse). The methods take `&mut self` and return `&mut Self`, so chain them or push in a loop; `clear()` reuses the allocation. #### METHODS | Method | Appends | | --- | --- | | `gap(frames)` | N idle frames (0 is a no-op). | | `move_by(dx, dy)` | a cursor-motion frame. | | `wheel(dz)` | a wheel frame. | | `press / release / force_release(usage)` | a one-frame press, soft-release, or force-release of any [`Usage`](/library/types/enums.md#usage) (button, key, or media), like [`Device::press`](/library/inject.md#inject). | | `edge(usage, action)` | a one-edge frame for any [`Usage`](/library/types/enums.md#usage) with an explicit [`Action`](/library/types/enums.md#action). | | `frame(dx, dy, wheel, edges)` | a motion delta plus up to 8 [`Usage`](/library/types/enums.md#usage) / [`Action`](/library/types/enums.md#action) edges on one frame. | `press`/`release`/`force_release` are wrappers over `edge`, which is a wrapper over `frame`. Reach for `frame` only when you need motion and edges (or several edges) on the same frame, e.g. moving while a button is held, which is how a faithful recording of "aim and hold fire" looks. #### EXAMPLE ```rust use medius::{ClipBuilder, Button, Key}; let mut clip = ClipBuilder::new(); for _ in 0..200 { clip.move_by(10, 0); } // 200 box-timed frames of +10 dx clip.press(Button::Left) // a click, held for 20 frames .gap(20) .release(Button::Left); clip.press(Key::A) // then type 'a' .gap(3) .release(Key::A); ``` #### MOTION AND AN EDGE ON ONE FRAME `frame` is the one builder call that fills more than a single field, so it's how a move and an edge share a frame: one entry, one wire report. Edges are sticky, so a hold is a press once then plain motion until the release. ```rust use medius::{Action, Button, ClipBuilder}; let mut clip = ClipBuilder::new(); // move (+10, -4) AND press Left on the same frame clip.frame(10, -4, 0, &[(Button::Left.into(), Action::Press)]); // "aim and hold fire": press once, keep moving while held, then release clip.frame(8, -2, 0, &[(Button::Left.into(), Action::Press)]); for _ in 0..60 { clip.move_by(8, -2); } // Left stays down (edges are sticky) clip.frame(0, 0, 0, &[(Button::Left.into(), Action::SoftRelease)]); ``` ## ClipHandle _Fill the ring, configure, and drive playback_ From [`Device::clip()`](/library/clip.md#clip). Every method below is [fire-and-forget](/native/injection.md#fire-and-forget): it queues a frame and returns. The [auto-lock](/library/lock.md), loop, and retain settings and the trigger set are the clip's config; the engine verbs drive playback. [`query_status`](/library/requests.md#clip-status) reads the ring depth and playback state, and [`query_config`](/library/requests.md#clip-config) reads the config back. #### LOAD & SETTINGS | Method | Does | | --- | --- | | `append(clip: &ClipBuilder)` | Send a [`ClipBuilder`](/library/clip.md#builder)'s entries to the ring; splits a large clip into whole-entry frames with contiguous append seqs. | | `set_autolock(scope: &[Blanket])` | Which [input groups](/library/lock.md) to lock while playing (clip-owned, released on stop). | | `set_loop(on: bool)` | Loop playback at the clip end (retained mode only). | | `set_retain(on: bool)` | Retain the clip so it can rewind and replay (`false` = streaming, the default). Set before the first `append`. | | `finalize()` | Close a retained clip: fix its end so it can replay and loop. | #### TRIGGERS (a managed set) | Method | Does | | --- | --- | | `bind(trigger: [ClipTrigger](/library/types/structs.md#clip-trigger))` | Add or overwrite a binding: a physical edge drives an action on the box, no host round-trip. | | `unbind(usage, edge: [Edge](/library/types/enums.md#edge))` | Remove the binding on that usage and edge. | | `clear_triggers()` | Remove every binding. | #### ENGINE VERBS | Method | Does | | --- | --- | | `start()` | Rewind to the clip start and play (resume from a pause). | | `stop()` | Stop, release held input and the clip lock; a streaming clip flushes, a retained clip rewinds and is kept. | | `pause()` / `resume()` | Halt mid-clip keeping the cursor and held input / continue from the paused cursor. | | `restart()` | Force a rewind and play, even mid-playback. | | `toggle()` | Play if idle/paused, stop if playing. | | `clear()` | Discard the loaded clip, free the ring, clear a fault. | > **Note** > > A dropped append (or an overflow) leaves the clip [`ClipState::Faulted`](/library/types/enums.md#clip-state) and stops it. Recover with `clear` and rebuild, not by appending more; a faulted stream has a hole in it. ## Streaming and retained _Drain-and-discard, or keep-and-replay_ A clip runs in one of two shapes, chosen by [`set_retain`](/library/clip.md#handle) before the first [`append`](/library/clip.md#handle). | Aspect | Streaming (default) | Retained | | --- | --- | --- | | Turn on | nothing, it's the default | `set_retain(true)` before the first `append` | | The ring | each entry is freed as it plays, so it's unbounded | entries are kept after playing, up to 64 KiB | | Top up mid-play | yes, append to the tail in real time | append until `finalize`, then it's sealed | | Replay | no, it plays once | yes, it rewinds and replays | | `loop` | not available | available once `finalize`d | | `stop` | flushes the buffer | rewinds and keeps the clip | | Best for | open-ended or generated input | a fixed macro you replay on a trigger | #### HOW THE RING MOVES ``` streaming (drain-and-discard) append ──▶ [ e4 e3 e2 ] ──▶ play ──▶ freed (unbounded, top up forever) box reclaims each entry once played; no replay retained (keep-and-replay, up to 64 KiB) append ──▶ [ e0 e1 e2 e3 e4 ] ──▶ finalize ──▶ sealed base │──▶ play cursor ──▶ end └──── start / loop rewinds to base ◀────┘ ``` > **Note** > > `set_retain` only takes effect before the first `append`, and an `append` after `finalize` is rejected. To reload a retained clip, [`clear`](/library/clip.md#handle) it and build again. #### EXAMPLE ```rust // Streaming: preload, play with auto-lock, then top up in real time, pacing against free. use medius::{Blanket, ClipState}; use std::time::Duration; let handle = device.clip(); // device: an open Device handle.set_autolock(&[Blanket::Aim])?; // lock only the aim axes while playing handle.append(&clip)?; // preload (clip, next_chunk: ClipBuilders you built) handle.start()?; loop { let s = handle.query_status()?; if s.state == ClipState::Idle { break; } // done, or stopped if s.free as usize > next_chunk.as_bytes().len() { handle.append(&next_chunk)?; // stream more while there's room } std::thread::sleep(Duration::from_millis(5)); } handle.stop()?; ``` ## Triggers _Play, stop, or toggle on a physical key_ A [`ClipTrigger`](/library/types/structs.md#clip-trigger) binds one [`Edge`](/library/types/enums.md#edge) of a usage to one [`ClipAction`](/library/types/enums.md#clip-action), so the box drives playback itself with no host round-trip. #### ANATOMY | Part | Is | Example | | --- | --- | --- | | usage | the button, key, or media the edge is on (or any of a class) | `Key::F1` | | edge | which transition fires it: press, release, or both | `Edge::Press` | | action | the engine verb to run | `ClipAction::Start` | Bindings are a managed set keyed by `(usage, edge)`, like a [lock](/library/lock.md): [`bind`](/library/clip.md#handle) adds or overwrites, [`unbind`](/library/clip.md#handle) drops one, and [`clear_triggers`](/library/clip.md#handle) wipes them. A physical edge runs the one most-specific match, so a binding on `Key::F1` beats an any-key binding. #### RECIPES | To get | Bind | | --- | --- | | Hold to play | `F1 Press → Start` and `F1 Release → Stop` | | Toggle play/stop on one key | `Side1 Press → Toggle` | | Separate play and stop keys | `F1 Press → Start` and `F2 Press → Stop` | | Pause, then resume | `F3 Press → Pause` and `F4 Press → Resume` | > **Note** > > `.consume()` swallows the triggering edge from the game for the length of the hold, so the key drives the clip without also reaching what you're playing. #### EXAMPLE ```rust use medius::{Button, ClipAction, ClipTrigger, Edge, Key}; let clip = device.clip(); clip.set_retain(true)?; // set the mode before loading clip.append(&recording)?; clip.finalize()?; // close it so it can replay // Hold-to-play: F1 down starts, F1 up stops. Consume F1 so the game never sees it. clip.bind(ClipTrigger::new(Key::F1, Edge::Press, ClipAction::Start).consume())?; clip.bind(ClipTrigger::new(Key::F1, Edge::Release, ClipAction::Stop).consume())?; // Or one side-button that toggles play/stop: clip.bind(ClipTrigger::new(Button::Side1, Edge::Press, ClipAction::Toggle))?; ``` ## On AsyncDevice _AsyncClipHandle: control fires, queries await_ [`AsyncDevice::clip()`](/library/features/async.md) returns an `AsyncClipHandle` that keeps `append`, the settings, and the engine verbs synchronous (they just queue a frame), while `query_status().await` and `query_config().await` are futures like the other queries. #### EXAMPLE ```rust let device = Device::find()?.into_async(); let handle = device.clip(); handle.append(&clip)?; // sync, no await handle.start()?; // sync let s = handle.query_status().await?; // the query awaits ``` --- # Lifecycle _Holding injected input alive and recovering a dropped link_ The library holds deliberate overrides past the box's [silence-timeout clear](/native/injection.md#safety), and restores them if the link drops and reopens. - [`keepalive`](/library/guides/connection.md#keepalive) (automatic) holds an override past the silence timeout. - [`reapply`](/library/lifecycle.md#reapply) re-sends the held overrides so the box matches the library. - [`reconnect`](/library/lifecycle.md#reconnect) rescans, reopens the port, and restores held state after a dropped link. ## reapply _Re-send the held overrides so the box matches the library_ ```text fn reapply(&self) -> Result<()> ``` _Fire-and-forget_ One frame goes out per held override; [`reconnect`](/library/lifecycle.md#reconnect) does this for you after a drop. #### EXAMPLE ```rust device.press(Button::Left)?; device.reapply()?; // re-assert the held override, e.g. if the box reset under you ``` #### EXAMPLE ```rust // The no-op case: reset clears every override, so reapply sends nothing. device.reset()?; device.reapply()?; // does nothing, no buttons are held ``` > **Note** > > Overrides are keyed by their [`Usage`](/library/types/enums.md#usage) (a button, key, or media usage). [`press`](/library/inject.md#inject) and [`force_release`](/library/inject.md#inject) add a held override; [`release`](/library/inject.md#inject) and [`reset`](/library/admin.md#reset) clear them. ## reconnect _Rescan, reopen the port, and restore held state_ ```text fn reconnect(&self) -> Result<()> ``` The reader thread auto-reconnects on any read error; call this by hand only to force a rescan. It blocks while it rescans and reopens the port, and returns an error if the box can't be found or opened, but it never waits on a box reply. On each call: 1. Rescans for the box by its USB identity, vendor ID `0x1A86` and product ID `0x55D3` (see [Transport](/native/transport.md)). 2. Reopens the port. 3. Re-applies the held overrides, the same work as [`reapply`](/library/lifecycle.md#reapply). 4. Bumps the reconnect counter. #### EXAMPLE ```rust // After a known unplug and replug, force a rescan and confirm it took. let before = device.counters().reconnects; device.reconnect()?; let after = device.counters().reconnects; assert!(after > before); ``` > **Note** > > The reconnect count is the `reconnects` field in [diagnostics](/library/diagnostics.md#counters). Held state is keyed by [`Usage`](/library/types/enums.md#usage), so the right inputs come back after a reopen. ## On AsyncDevice _reapply and reconnect, still direct_ [`AsyncDevice`](/library/features/async.md) exposes `reapply` and `reconnect` directly, same signatures. `reapply` is a fire-and-forget frame; `reconnect` blocks while it rescans and reopens the port, so keep it off a latency-sensitive task. #### EXAMPLE ```rust use medius::AsyncDevice; let device = AsyncDevice::open("/dev/ttyACM0")?; device.reapply()?; // re-assert held overrides device.reconnect()?; // blocks: rescan + reopen ``` > **Note** > > See [async](/library/features/async.md) for the full async surface and how `into_inner` and `into_async` move between the two views. --- # Diagnostics _Read-only views of the link_ `logs` and `counters` are lock-free, read-only views on [`Device`](/library/connection.md) and [`AsyncDevice`](/library/features/async.md). See also: [testing with MockBox](/library/guides/testing.md#testing). ## logs _Stream of box messages_ ```text fn logs(&self) -> LogStream ``` _No round-trip_ Yields each box [`LOG`](/native/commands/admin.md#log) frame as a [`LogLine`](/library/types/structs.md#log-line). #### EXAMPLE ```rust for line in device.logs() { println!("[{:?}] {}", line.level, line.text); } // the loop ends here when the link drops ``` > **Note** > > The box emits [`LOG`](/native/commands/admin.md#log) frames only while the control link is up; with no control PC attached they are dropped, not buffered. ## Reading the stream _Blocking, polling, and draining_ #### METHODS | Method | Returns | Blocks | Description | | --- | --- | --- | --- | | `recv()` | `Result` | Yes | Waits for the next line. `Err` is [`Error::Disconnected`](/library/types/errors.md) once the link is gone. | | `try_recv()` | `Option` | No | One queued line, or `None` if nothing is waiting. | | `recv_timeout(d)` | `Option` | Up to `d` | The next line within the window, or `None` on timeout. | | `try_iter()` | `impl Iterator` | No | Drains every line queued right now, then stops. | | `recv_async().await` | `Result` | Awaits | Await the next line (`async` feature), runtime-agnostic. | | `for line in stream` | `LogLine` | Yes | Blocking `IntoIterator`; yields each line until the link closes. | `try_iter()` or `try_recv()` for a per-frame loop; `recv()` or the blocking iterator for a dedicated log thread. #### EXAMPLE ```rust let stream = device.logs(); // once per frame: drain whatever is queued, never blocking for line in stream.try_iter() { println!("[{:?}] {}", line.level, line.text); } // or wait a bounded window for the next line if let Some(line) = stream.recv_timeout(Duration::from_millis(50)) { println!("got {}", line.text); } else { // no log this window, carry on } ``` > **Warning** > > The channel buffers 1024 lines and evicts the oldest when full. A slow poller drops old lines silently, so drain often if you can't miss any. ## counters _Link statistics_ ```text fn counters(&self) -> CountersSnapshot ``` _No round-trip_ A `Copy` snapshot of four totals that only climb, reset only on process restart: rising `crc_drops` means a flaky cable, `reconnects` counts port reopens after a dropped link. Full field reference on [`CountersSnapshot`](/library/types/structs.md#counters-snapshot). #### EXAMPLE ```rust println!("{:?}", device.counters()); ``` --- # Async _AsyncDevice on any executor_ `AsyncDevice` is [`Device`](/library/connection.md) with its queries as futures, behind the off-by-default `async` flag. ```bash cargo add medius --features async ``` Reply waits use [`flume`](https://crates.io/crates/flume), so futures run under any executor, no [`tokio`](https://tokio.rs). ```bash cargo add futures ``` `Result` is the fallible return type (see [Errors](/library/types/errors.md)). See also: [call kinds & timeouts](/library/guides/calls.md#call-kinds), [concurrency](/library/guides/connection.md#threading), [testing](/library/guides/testing.md#testing). ## Constructing an AsyncDevice _open, find, into_async, into_inner_ ```text fn open(path: impl AsRef) -> Result ``` _Blocks_ ```text fn find() -> Result ``` _Blocks_ ```text fn into_async(self) -> AsyncDevice ``` _No round-trip_ ```text fn into_inner(self) -> Device ``` _No round-trip_ #### CONSTRUCTORS | Constructor | Description | | --- | --- | | `open` | Takes a serial-port path, opens it, and runs the handshake. It blocks like [`Device::open`](/library/connection.md#open), then hands back an `AsyncDevice`. | | `find` | Discovers the first medius box by USB id, opens it, and runs the handshake. Blocks like [`Device::find`](/library/connection.md#open), then hands back an `AsyncDevice`. | | `into_async` | Reinterprets an already-open [`Device`](/library/connection.md) as an `AsyncDevice`. It's zero-cost over the same `Link` core, no new connection. | | `into_inner` | Hands the sync `Device` back. | #### EXAMPLE ```rust // discover and open in one call (runs the handshake, blocks) let device = AsyncDevice::find()?; // or by path: let device = AsyncDevice::open("/dev/ttyACM0")?; // or reinterpret an already-open Device: let device = Device::find()?.into_async(); ``` > **Note** > > Everything else on [`Device`](/library/connection.md) is mirrored on `AsyncDevice` and stays synchronous, including [`counters`](/library/diagnostics.md#counters), [`logs`](/library/diagnostics.md#logs), [`reapply`](/library/lifecycle.md#reapply), and [`reconnect`](/library/lifecycle.md#reconnect). Only the queries are futures. ## Awaiting a query _every query method is a future_ ```text async fn query_version(&self) -> Result ``` _Blocks_ ```text async fn query_health(&self) -> Result ``` _Blocks_ Each future awaits the correlated [`RESP`](/native/commands/requests.md#resp) with the default timeout, then resolves to its struct. #### RETURNS | Method | Resolves to | Description | | --- | --- | --- | | `query_version().await` | [`Version`](/library/types/structs.md#version) | Firmware identity. | | `query_health().await` | [`Health`](/library/types/structs.md#health) | Whether the box is wired and ready. | Two shown; the rest (`device_info`, `caps`, `query_rate`, `query_stats`, `query_locks`, `query_catch`) resolve the same way. The full list is on [`Requests`](/library/requests.md#async). `.await` is only legal inside an `async` function or block. #### EXAMPLE ```rust async fn run(device: &AsyncDevice) -> medius::Result<()> { let v = device.query_version().await?; let h = device.query_health().await?; println!("{v}, link_up={}", h.link_up); Ok(()) } ``` --- # Mock _Test without hardware_ A `MockBox` is an in-process fake Medius box behind the `mock` cargo feature. ```bash cargo add medius --features mock ``` It's a cheap `Clone`: hand one to the [`Device`](/library/connection.md), keep one to script and inspect. See also: [testing with MockBox](/library/guides/testing.md#testing). ## Building a MockBox _new, and why you clone it_ ```text fn new() -> MockBox ``` _No round-trip_ `new()` records every command and auto-answers `QUERY(VERSION)` and `QUERY(HEALTH)` with defaults. #### EXAMPLE ```rust use medius::{Device, MockBox}; let mock = MockBox::new(); let device = Device::with_mock(mock.clone()); // `device` drives the fake; `mock` still scripts and observes it. ``` ## Wrapping it in a Device _with_mock and open_mock_ ```text fn with_mock(mock: MockBox) -> Device ``` _No round-trip_ ```text fn open_mock(mock: MockBox) -> Result ``` _Blocks_ #### CONSTRUCTORS | Constructor | Handshake | Returns | Description | | --- | --- | --- | --- | | `with_mock` | No | `Device` | Wraps the fake and hands back the device directly. | | `open_mock` | Yes | [`Result`](/library/types/errors.md) | Also runs the version handshake, so it can fail the same way a real port can. | #### EXAMPLE ```rust use medius::{Device, MockBox}; let device = Device::open_mock(MockBox::new())?; device.move_rel(5, 5)?; ``` See the [dead-box card](/library/features/mock.md#silent) for the two ways `open_mock` can fail. ## Scripting query answers _Set the version, health, and device-info a query returns_ ```text fn with_version(self, version: Version) -> MockBox ``` _No round-trip_ ```text fn with_health(self, health: Health) -> MockBox ``` _No round-trip_ ```text fn with_device_info(self, device_info: DeviceInfo) -> MockBox ``` _No round-trip_ ```text fn with_caps(self, caps: Caps) -> MockBox ``` _No round-trip_ ```text fn with_mouse_caps(self, mouse: MouseCaps) -> MockBox ``` _No round-trip_ ```text fn with_kbd_caps(self, keyboard: KbdCaps) -> MockBox ``` _No round-trip_ ```text fn with_rate(self, rate: Rate) -> MockBox ``` _No round-trip_ ```text fn with_stats(self, stats: Stats) -> MockBox ``` _No round-trip_ ```text fn set_version(&self, version: Version) ``` _No round-trip_ ```text fn set_health(&self, health: Health) ``` _No round-trip_ The `with_*` builders set what each query returns: [`query_version`](/library/requests.md#version), [`query_health`](/library/requests.md#health), and the device-info queries ([`device_info`](/library/requests.md#device-info), [`caps`](/library/requests.md#caps), [`query_rate`](/library/requests.md#query-rate), [`query_stats`](/library/requests.md#query-stats)). `set_*` changes a live fake in place to flip the version or health mid-test. [`Version`](/library/types/structs.md#version), [`Health`](/library/types/structs.md#health), and the device-info [structs](/library/types/structs.md) live on the types page, and `Health::from_flags` builds one from the raw status byte. #### EXAMPLE ```rust use medius::{Device, Health, MockBox, Version}; let mock = MockBox::new() .with_version(Version { proto_ver: 3, fw_major: 5, fw_minor: 6, fw_patch: 7, mac: [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], name: "Loki".into() }) .with_health(Health::from_flags(0x0F)); let device = Device::with_mock(mock.clone()); let v = device.query_version()?; assert_eq!((v.fw_major, v.fw_minor, v.fw_patch), (5, 6, 7)); assert!(device.query_health()?.mouse_attached); // Change it mid-test: flip a later query_health. mock.set_health(Health::from_flags(0x00)); assert!(!device.query_health()?.mouse_attached); ``` ## Injecting inbound traffic _push_log and push_raw_ ```text fn push_log(&self, level: LogLevel, text: &str) ``` _No round-trip_ ```text fn push_raw(&self, bytes: &[u8]) ``` _No round-trip_ ```text fn push_motion(&self, seq: u8, dx: i16, dy: i16, dz: i16) ``` _No round-trip_ ```text fn push_usages(&self, seq: u8, usages: &[Usage]) ``` _No round-trip_ All put bytes on the inbound stream as if the box emitted them. `push_log` frames a `LOG` line that surfaces on [`logs()`](/library/diagnostics.md#logs) as a [`LogLine`](/library/types/structs.md#log-line) ( [`LogLevel`](/library/types/enums.md#log-level) plus `text`); `push_raw` sends arbitrary bytes. The two event calls feed the [`EventStream`](/library/catch.md#event-stream): `push_motion` arrives as a [`CatchEvent::Motion`](/library/types/enums.md#catch-event) (a [`MotionEvent`](/library/types/structs.md#motion-event)) and `push_usages` as a [`CatchEvent::Usages`](/library/types/enums.md#catch-event) (a [`UsageSnapshot`](/library/types/structs.md#usage-snapshot)), with `seq` as the rolling counter so a test can assert gap detection. #### EXAMPLE ```rust use std::time::Duration; use medius::{CatchEvent, CatchMask, Device, Key, LogLevel, MockBox, Usage}; let mock = MockBox::new(); let device = Device::with_mock(mock.clone()); let rx = device.logs(); mock.push_log(LogLevel::Warn, "overheating"); let line = rx.recv_timeout(Duration::from_secs(1))?; assert_eq!(line.text, "overheating"); // Fake a catch subscription seeing the user hold A. let stream = device.catch_events(CatchMask::KEYS)?; mock.push_usages(0, &[Usage::from(Key::A)]); assert!(matches!(stream.recv()?, CatchEvent::Usages(s) if s.is_held(Key::A))); ``` ## Asserting what was sent _recorded_frames, saw, recorded, clear_recorded_ ```text fn recorded_frames(&self) -> Vec ``` _No round-trip_ ```text fn recorded(&self) -> usize ``` _No round-trip_ ```text fn saw(&self, ty: FrameType) -> bool ``` _No round-trip_ ```text fn clear_recorded(&self) ``` _No round-trip_ #### METHODS | Method | Returns | Description | | --- | --- | --- | | `recorded_frames` | [`Vec`](/library/types/frames.md) | Every command the host sent so far, decoded, in order. | | `recorded` | `usize` | The count of commands recorded so far. | | `saw` | `bool` | Whether the host sent at least one frame of the given type. | | `clear_recorded` | `()` | Drops the recorded history so you can assert only on the next phase. | A [`DecodedFrame`](/library/types/frames.md) is `{ ty, seq, payload }`; a [`press(Button::Left)`](/library/inject.md) records a [`FrameType::Inject`](/library/types/frames.md) frame with payload `[0, 0, 0, 1]` (class `0` = button, id `0`, action `1`). #### EXAMPLE ```rust use medius::{Button, Device, FrameType, MockBox}; let mock = MockBox::new(); let device = Device::with_mock(mock.clone()); device.press(Button::Left)?; let frames = mock.recorded_frames(); let inject = frames .iter() .find(|f| f.ty == FrameType::Inject) .expect("press recorded"); assert_eq!(inject.payload, vec![0, 0, 0, 1]); assert!(mock.saw(FrameType::Inject)); mock.clear_recorded(); // next assertions see a fresh log ``` ## Simulating a dead box _silent, and the handshake failures_ ```text fn silent(self) -> MockBox ``` _No round-trip_ `silent()` records commands but never answers a query. The two [`open_mock`](/library/features/mock.md#wrap) failures are a silent box ([`Error::NoReply`](/library/types/errors.md)) and an unknown protocol version ([`Error::BadProtoVer`](/library/types/errors.md)). #### EXAMPLE ```rust use medius::{Device, Error, MockBox, Version}; // A silent box answers nothing: no reply. let err = Device::open_mock(MockBox::new().silent()).unwrap_err(); assert!(matches!(err, Error::NoReply)); // A box on an unknown protocol version fails the handshake. let mock = MockBox::new().with_version(Version { proto_ver: 9, fw_major: 0, fw_minor: 0, fw_patch: 0, mac: [0; 6], name: String::new(), }); let err = Device::open_mock(mock).unwrap_err(); assert!(matches!(err, Error::BadProtoVer { got: 9 })); ``` --- # Flash _Reflash a box from Rust_ Writes new firmware onto a box's two chips (see [Flashing](/native/flashing.md)), rebooting a chip into download mode then writing the image. Behind the `flash` Cargo feature, off by default. ```bash cargo add medius --features flash ``` With the feature off, none of the `medius::flash` items below exist. ## What you need first _esptool.py, the chip, the address_ [`esptool.py`](https://github.com/espressif/esptool) must be on `PATH` as the script, not a bare `esptool` binary (else [`Error::FlashTool`](/library/features/flash.md#errors)). > **Warning** > > [`flash`](/library/features/flash.md#flash) exists on Linux and Windows only, compiled out on macOS. The consts and [`esptool_args`](/library/features/flash.md#args) are always present. The four consts are public: ```text const ESPTOOL: &str ``` ```text const CHIP: &str ``` ```text const FLASH_ADDR: &str ``` ```text const ROM_SETTLE: Duration ``` #### EXAMPLE ```rust use medius::flash; println!("tool: {}", flash::ESPTOOL); // esptool.py println!("chip: {}", flash::CHIP); // esp32s3 println!("address: {}", flash::FLASH_ADDR); // 0x10000 println!("settle: {:?}", flash::ROM_SETTLE); // 2s ``` ## flash _Reboot into download mode, then write the image_ ```text fn flash(port: &str, bin_path: impl AsRef, host: bool) -> Result<()> ``` _Blocks_ #### PARAMETERS | Parameter | Type | Description | | --- | --- | --- | | `port` | `&str` | Serial port the box is on, for example `/dev/ttyACM0`. | | `bin_path` | `impl AsRef` | Path to the firmware image to write. | | `host` | `bool` | Picks the chip. `false` flashes the device chip, `true` the host chip. | The `host` flag picks the reboot target: `false` is [`RebootTarget::DeviceDownload`](/library/types/enums.md#reboot-target), `true` is [`RebootTarget::HostDownload`](/library/types/enums.md#reboot-target) (see [`reboot`](/library/admin.md#reboot) for the full set). One call reboots the chosen chip into download mode, frees the port, then runs `esptool.py write_flash`; the firmware-side sequence is on [Flashing](/native/flashing.md#two-chips). Blocks for the wait plus the tool's runtime, returning `Ok(())` on a clean exit else [`Err(Error::FlashTool)`](/library/features/flash.md#errors). #### EXAMPLE ```rust use medius::flash; // false -> device chip flash::flash("/dev/ttyACM0", "device.bin", false)?; // true -> host chip flash::flash("/dev/ttyACM0", "host.bin", true)?; ``` ## Inspecting the command _See exactly what esptool runs_ ```text fn esptool_args(port: &str, bin_path: &Path) -> Vec ``` _No round-trip_ Builds the exact argv [`flash`](/library/features/flash.md#flash) passes to `esptool.py`, without running it. To debug, reboot the chip into download mode via [`reboot`](/library/admin.md#reboot) and run these args by hand. #### EXAMPLE ```rust use std::path::Path; use medius::flash; let argv = flash::esptool_args("/dev/ttyACM0", Path::new("device.bin")); println!("esptool.py {}", argv.join(" ")); // esptool.py --chip esp32s3 --port /dev/ttyACM0 --before no_reset // --after hard_reset write_flash 0x10000 device.bin ``` ## When it fails _Error::FlashTool_ The feature adds `Error::FlashTool(String)` to the [`Error`](/library/types/errors.md) enum. It fires when `esptool.py` can't be spawned (not on `PATH`) or exits non-zero, the inner `String` carrying the stderr tail. #### EXAMPLE ```rust use medius::{flash, Error}; match flash::flash("/dev/ttyACM0", "device.bin", false) { Ok(()) => println!("flashed"), Err(Error::FlashTool(msg)) => { eprintln!("flash failed: {msg}"); eprintln!("hint: is esptool.py on PATH and the port correct?"); } Err(e) => eprintln!("other error: {e}"), } ``` ## Flashing in tests _Swap the runner, skip the hardware_ [`flash`](/library/features/flash.md#flash) wraps `flash_with`; pass a fake `CommandRunner` and a no-op reboot closure to test without hardware. ```text fn flash_with(port: &str, bin_path: &Path, host: bool, runner: &R, reboot: F) -> Result<()> where R: CommandRunner, F: FnOnce(&str, bool) -> Result<()> ``` _Blocks_ #### EXAMPLE ```rust // A fake runner records the argv instead of running esptool; the reboot is a no-op. flash::flash_with("/dev/ttyACM0", bin, false, &recording_runner, |_port, _host| Ok(()))?; ``` --- # Tracing _Structured diagnostics over the link_ The `tracing` feature wires the crate into [`tracing`](https://docs.rs/tracing): it emits a span and events as it works the link, but adds no medius functions and changes no behavior. The sections below are what it emits; you read them by installing a [subscriber](/library/features/tracing.md#subscriber). ```bash cargo add medius --features tracing ``` Off by default; with the feature off the macros expand to nothing, so there's no runtime cost. ## Targets and levels _What the crate emits and where_ #### TARGETS | Target | Levels | Emitted | | --- | --- | --- | | `medius::device` | `INFO`, `DEBUG`, `WARN` | The `connect` span and `connected` event (`INFO`), handshake retries (`DEBUG`) and failures (`WARN`), query resolved (`DEBUG`) and timed out (`WARN`), the `reconnected` event (`INFO`), plus the box's own logs re-emitted with `device_log=true`. | | `medius::transport` | `TRACE` | One event per frame written or read, with `dir`, `opcode`, `seq`, and `len`. | | `medius::flash` | `INFO`, `ERROR` | Reboot-into-download and esptool progress, then tool failure. Present only with the [`flash`](/library/features/flash.md) feature. | > **Note** > > Keepalive has no target of its own; its periodic frame shows up as an ordinary `medius::transport` tx event. ## The connect span _A span wraps related events_ The `connect` span wraps the handshake; retry and `connected` events nest inside it. Its fields are the numbers [`query_version`](/library/requests.md#version) returns as a [`Version`](/library/types/structs.md#version). #### EXAMPLE ```rust // With "medius=debug" and a box that answers on the second probe, the // fmt subscriber prints the span name on each nested event: // DEBUG connect: medius::device: handshake: version probe timed out, retrying // INFO connect: medius::device: connected proto_ver=3 fw_major=1 fw_minor=3 fw_patch=0 // "connect:" is the span; the rest is the event with its fields. ``` ## Frames, device logs, and reconnects _The events worth knowing_ `medius::transport` emits one `TRACE` per frame, the per-frame mirror of the [`frames_tx` / `frames_rx`](/library/diagnostics.md#counters) counters. Each [`LOG`](/native/commands/admin.md#log) frame the box sends is re-emitted on `medius::device` with `device_log=true` at its matching [`LogLevel`](/library/types/enums.md#log-level) (the same data the [`logs`](/library/diagnostics.md#logs) stream hands back). A recovered link fires an `INFO` `reconnected` event with `port` and `reason`. #### EXAMPLE ```rust // medius::transport=trace, one line per frame: // TRACE medius::transport: dir="tx" opcode=1 seq=7 len=4 // a box log, mirrored: // WARN medius::device: mouse detached device_log=true // a recovered link: // INFO medius::device: reconnected port="/dev/ttyACM0" reason="rescan" ``` ## Install a subscriber _Print the events to stderr_ The crate ships no subscriber, so add one alongside the feature, usually [`tracing-subscriber`](https://docs.rs/tracing-subscriber). The [`fmt`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/index.html) subscriber writes lines to stderr; call `init()` once before opening. Without one, every span and event is dropped silently. ```bash cargo add tracing-subscriber ``` #### EXAMPLE ```rust use medius::Device; tracing_subscriber::fmt::init(); let device = Device::find()?; device.move_rel(10, 0)?; // stderr now carries the connect span and an INFO event, e.g.: // INFO connect: medius::device: connected proto_ver=3 fw_major=1 fw_minor=3 fw_patch=0 ``` ## Filter by level and target _EnvFilter and RUST_LOG_ Transport events sit below the default `INFO` floor. Lower it with a per-target [`EnvFilter`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) (target names in [targets](/library/features/tracing.md#targets)). #### EXAMPLE ```rust // Code-side: medius events at DEBUG, everything else at the default. tracing_subscriber::fmt() .with_env_filter("medius=debug") .init(); // Or set it at runtime instead, no recompile: // RUST_LOG=medius=debug ./your-program // RUST_LOG=medius::transport=trace ./your-program # every frame ``` > **Warning** > > `medius::transport=trace` emits one line per frame in both directions. Leave it off unless you're chasing a wire-level bug. ## JSON output _Ship structured events_ Swap the formatter for JSON and each event becomes one object with its fields as keys. Needs `tracing-subscriber`'s `json` feature. #### EXAMPLE ```rust // cargo add tracing-subscriber --features json tracing_subscriber::fmt() .json() .with_env_filter("medius=debug") .init(); // Each event is now a JSON line, e.g.: // {"level":"INFO","target":"medius::device","fields":{"message":"connected","proto_ver":3}} ``` --- # Three kinds of call _Fire-and-forget, blocking query, no round-trip_ Every [`Device`](/library/connection.md) method is one of three kinds. The [API pages](/library.md) tag each method with a badge; this is what the three mean. _Fire-and-forget_ Writes one frame, returns once the bytes are out, no reply. #### EXAMPLE ```rust device.move_rel(100, -50)?; // one frame out, no reply ``` _Blocks_ Sends a [`QUERY`](/native/commands/requests.md#requests) and waits for the correlated [`RESP`](/native/commands/requests.md#resp). #### EXAMPLE ```rust let v = device.query_version()?; // waits for the box to reply ``` _No round-trip_ Reads state the library already holds; can't fail on the link. #### EXAMPLE ```rust let c = device.counters(); // local snapshot, no network ``` ## Why the queries are async _Queries await a reply, everything else fires and forgets_ With the [`async`](/library/features/async.md) feature, the [`QUERY`](/native/commands/requests.md#requests) methods are the `async fn`s, because a query blocks for its correlated [`RESP`](/native/commands/requests.md#resp). Every other method is [fire-and-forget](/native/injection.md#fire-and-forget), so it stays synchronous. #### METHOD SPLIT | Async (you `.await` it) | Stays sync (no `.await`) | | --- | --- | | [`query_version`](/library/requests.md#version), [`query_health`](/library/requests.md#health), [`device_info`](/library/requests.md#device-info), [`caps`](/library/requests.md#caps), [`query_rate`](/library/requests.md#query-rate), [`query_stats`](/library/requests.md#query-stats), [`query_locks`](/library/requests.md#query-locks), [`query_catch`](/library/requests.md#query-catch), the option `query_*` methods, and the clip [`status`](/library/requests.md#clip-status) query | [`move_rel`](/library/move.md#move-rel), [`wheel`](/library/move.md#wheel), [`inject`](/library/inject.md#inject), [`press`](/library/inject.md#inject), [`release`](/library/inject.md#inject), [`force_release`](/library/inject.md#inject), [`reset`](/library/admin.md#reset), [`reboot`](/library/admin.md#reboot), [`led`](/library/led.md#led), [`lock`](/library/lock.md#lock), [`unlock`](/library/lock.md#unlock), [`catch_events`](/library/catch.md#catch-events), and the clip [`append`](/library/clip.md#handle) / [`start`](/library/clip.md#handle) / [`stop`](/library/clip.md#handle) | ## Driving futures without a runtime _futures::executor::block_on_ [`block_on`](https://docs.rs/futures/latest/futures/executor/fn.block_on.html) runs one future to completion on the current thread, so you can await a query with no async runtime at all. #### EXAMPLE ```rust use futures::executor::block_on; let device = Device::find()?.into_async(); let v = block_on(device.query_version())?; println!("{v}"); ``` > **Note** > > Inside an async `main`, `.await` the same future instead; it runs unchanged under [`tokio`](https://tokio.rs), [`async-std`](https://crates.io/crates/async-std), or [`smol`](https://crates.io/crates/smol). ## When the box is silent _Default timeout and QueryTimeout_ A query waits [`DEFAULT_QUERY_TIMEOUT`](/library/connection.md#zero-config) (1 second), then returns `Err(Error::QueryTimeout)`. This applies to both the sync and async queries. #### EXAMPLE ```rust match device.query_health() { Ok(h) => println!("{h:?}"), Err(medius::Error::QueryTimeout) => eprintln!("no reply in time"), Err(e) => return Err(e), } ``` > **Note** > > `QueryTimeout` means silence; `NoReply` means a reply arrived but didn't parse. Both are on the [Errors](/library/types/errors.md) page. ## Smooth motion _Glide instead of teleport_ [`move_rel`](/library/move.md#move-rel) applies one delta at once. For a glide rather than a jump, subdivide the move and pace the steps yourself, roughly one per millisecond. There's no `move_smooth`. #### EXAMPLE ```rust use std::thread::sleep; use std::time::Duration; // Glide ~400 counts to the right over 200 steps (~200 ms at 1 kHz). for _ in 0..200 { device.move_rel(2, 0)?; sleep(Duration::from_millis(1)); } ``` > **Warning** > > The library applies no rate limit. A no-sleep loop floods the 4 Mbaud link; pace your own steps. ## Making a click _Press, wait, release_ There's no one-shot `click`: [`press`](/library/inject.md#inject), wait, then release with [`release`](/library/inject.md#inject) so you don't stomp a physical hold. #### EXAMPLE ```rust use std::{thread, time::Duration}; use medius::Button; device.press(Button::Left)?; thread::sleep(Duration::from_millis(20)); device.release(Button::Left)?; ``` [`reset`](/library/admin.md#reset) drops every override at once; a held press is re-asserted on reconnect via [`reapply`](/library/lifecycle.md#reapply). --- # Choosing a port _When more than one box is plugged in_ [`find`](/library/connection.md#open) grabs the first match. With more than one box, `find_medius` lists every match as a [`PortInfo`](/library/types/structs.md#port-info) (`path`, `vid`, `pid`) without opening, then [`open`](/library/connection.md#open) the chosen one. #### EXAMPLE ```rust use medius::{Device, find_medius}; let ports = find_medius(); for port in &ports { println!("{} (vid={:#06x} pid={:#06x})", port.path, port.vid, port.pid); } // open a chosen one (here, the first): let port = ports.first().ok_or(medius::Error::NotFound)?; let dev = Device::open(&port.path)?; ``` ## Threading and Clone _Share one connection across threads_ [`Device`](/library/connection.md) is `Send + Sync` and clones cheaply (an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) inside), so one connection serves any number of threads; a clone bumps the reference count and doesn't reopen the port. #### EXAMPLE ```rust use std::thread; let worker = device.clone(); let handle = thread::spawn(move || { worker.move_rel(10, 0) }); handle.join().unwrap()?; ``` ## Running queries concurrently _join and cloning across tasks_ [`AsyncDevice`](/library/features/async.md) clones the same way, so hand a clone to another task and await both queries together with [`futures::future::join`](https://docs.rs/futures/latest/futures/future/fn.join.html). #### EXAMPLE ```rust let (v, h) = futures::future::join( device.query_version(), device.query_health(), ).await; let v = v?; let h = h?; println!("{v}, link_up={}", h.link_up); ``` > **Note** > > Only the queries need `.await`; mutators stay synchronous on either handle. ## Keepalive and holds _Holding injected input past the silence window_ The box clears every injected input and pending move once no frame arrives for its [silence window](/native/injection.md#safety). A live `Device` holds your overrides past that on its own: a background thread (`medius-keepalive`) sends a [`QUERY(HEALTH)`](/native/commands/requests.md#health) every `DEFAULT_KEEPALIVE_CADENCE` (500 ms) while anything is held, so a press survives. There's no `keepalive()` to call. | State | Behavior | | --- | --- | | Override held | Keepalive thread runs; the health reply is dropped. | | Idle | No override held, so the thread sends nothing. | #### EXAMPLE ```rust device.press(Button::Left)?; // No further calls. The keepalive thread sends QUERY(HEALTH) on its own, // so the hold survives well past the 1000 ms silence window. std::thread::sleep(std::time::Duration::from_secs(5)); device.reset()?; ``` An _override_ is the box holding an input down or up itself, set by [`press`](/library/inject.md#inject) or [`force_release`](/library/inject.md#inject); the library keeps a copy. After a dropped link, [`reapply`](/library/lifecycle.md#reapply) and [`reconnect`](/library/lifecycle.md#reconnect) restore it. ## Releasing the device _Drop it; no close() call_ There's no `close`: dropping the last [`Arc`\-backed handle](/library/guides/connection.md#threading) tears the connection down. Its [`Drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html) stops the threads and closes the port. | Step | What drop does | | --- | --- | | Signal | Sets the stop flag the reader and keepalive threads watch. | | Join | Waits for both threads to exit, so none are left running. | | Close | Releases the serial port as the transport drops. | For immediate hand-off call [`reset`](/library/admin.md#reset) before drop. #### EXAMPLE ```rust // hand the mouse back right now, not a second from now: device.reset()?; // box returns to passthrough immediately drop(device); // tears down threads and closes the port ``` --- # Testing without hardware _Assert the frames with MockBox_ With the [`mock`](/library/features/mock.md) feature, drive a [`Device`](/library/connection.md) with a [`MockBox`](/library/features/mock.md) and assert the queued frames through [`recorded_frames`](/library/features/mock.md#inspect). ```bash cargo add medius --features mock ``` #### EXAMPLE ```rust use medius::{Button, Device, FrameType, MockBox}; #[test] fn press_queues_an_inject() { let mock = MockBox::new(); let device = Device::with_mock(mock.clone()); device.press(Button::Left).unwrap(); assert!(mock.saw(FrameType::Inject)); let frame = mock .recorded_frames() .into_iter() .find(|f| f.ty == FrameType::Inject) .unwrap(); assert_eq!(frame.payload, vec![0, 0, 0, 1]); } ``` ## Driving logs in a test _Push log lines with a MockBox_ `push_log` on a `MockBox` emits a [`LOG`](/native/commands/admin.md#log) frame that surfaces on [`logs()`](/library/diagnostics.md#logs) like a real one. #### EXAMPLE ```rust use medius::{Device, LogLevel, MockBox}; use std::time::Duration; #[test] fn logs_reach_the_stream() { let mock = MockBox::new(); let device = Device::with_mock(mock.clone()); let stream = device.logs(); mock.push_log(LogLevel::Warn, "overheating"); let line = stream.recv_timeout(Duration::from_secs(1)).unwrap(); assert_eq!(line.level, LogLevel::Warn); assert_eq!(line.text, "overheating"); } ``` ## Testing async code _A MockBox behind an AsyncDevice_ There's no `AsyncDevice::with_mock`: build a mocked `Device`, then call [`into_async`](/library/features/async.md). Drive the futures with [`block_on`](/library/guides/calls.md#block-on), so the test needs no async runtime. A [`silent`](/library/features/mock.md) box never answers, resolving the query to [`Err(Error::QueryTimeout)`](/library/types/errors.md). #### EXAMPLE ```rust use futures::executor::block_on; use medius::{Device, Error, MockBox, Version}; let mock = MockBox::new().with_version(Version { proto_ver: 3, fw_major: 1, fw_minor: 2, fw_patch: 3, mac: [0; 6], name: "Loki".into(), }); let device = Device::with_mock(mock).into_async(); let v = block_on(device.query_version())?; assert_eq!((v.fw_major, v.fw_minor, v.fw_patch), (1, 2, 3)); // a silent box times out: let device = Device::with_mock(MockBox::new().silent()).into_async(); assert!(matches!(block_on(device.query_version()).unwrap_err(), Error::QueryTimeout)); ``` --- # Types & errors _Arguments, results, and errors_ Every public type is re-exported at the crate root: import from `medius::`, not `medius::types::`. The argument [enums](/library/types/enums.md), the [structs](/library/types/structs.md) the box reports back, the low-level [frame types](/library/types/frames.md), and the one [`Error`](/library/types/errors.md) all live here. #### EXAMPLE ```rust use medius::{Button, Action, Health, Version, Error, Result}; // One flat namespace. This does NOT work: // use medius::types::Button; ``` ## Reference pages _Pick a group_ - [Enums](/library/types/enums.md): Button, Action, RebootTarget, LogLevel, CatchEvent - [Structs](/library/types/structs.md): Version, Health, MouseCaps, KbdCaps, Key, MediaKey, and more - [Frames](/library/types/frames.md): FrameType, DecodedFrame - [Errors](/library/types/errors.md): Error, Result --- # Enums _Command and status enumerations_ Command and status enums, each tied to a wire byte. Conversion helpers are listed with each. ## DeviceKind _The cloned device's primary kind_ ```text enum DeviceKind { Unknown, Keyboard, Mouse } ``` The `kind` field of a [`DeviceInfo`](/library/types/structs.md#device-info), read from the cloned device's USB Boot-interface `bInterfaceProtocol`. It also drives [`find_mouse_box`](/library/discovery.md#find-mouse-box) and [`find_keyboard_box`](/library/discovery.md#find-keyboard-box). `Display` prints the lowercase name. | Variant | Byte | Meaning | | --- | --- | --- | | `Unknown` | `0` | Neither a Boot keyboard nor a Boot mouse. | | `Keyboard` | `1` | The device is a keyboard. | | `Mouse` | `2` | The device is a mouse. | ## Button _The button a command acts on_ ```text enum Button { Left, Right, Middle, Side1, Side2 } ``` The button an [`INJECT`](/native/commands/inject.md#inject) command acts on. A `Button` converts `Into<[Usage](/library/types/enums.md#usage)>` as class button, so you pass one straight to [`inject`](/library/inject.md#inject). Convert with `as_id() -> u8` and `from_id(u8) -> Option