medius 3.3.1

Host control library for the medius transparent mouse passthrough box
Documentation
//! Frame opcodes and wire constants, pinned to `ctrl_proto.h` / `control-protocol.md`.

use core::fmt;

/// Start-of-frame byte; the receiver resyncs by scanning for it (§2).
pub const SOF: u8 = 0xA5;

/// Maximum payload length (§2); a larger `LEN` is rejected as bogus.
pub const MAX_PAYLOAD: usize = 512;

/// Protocol version in `RESP(VERSION)` (§4.1); the handshake requires this exact value.
pub const PROTO_VER: u8 = 6;

/// `INJECT` class byte: the momentary-usage field kind.
pub const INJ_BTN: u8 = 0;
pub const INJ_KEY: u8 = 1;
pub const INJ_MEDIA: u8 = 2;
/// `MOVE` motion byte: the relative-axis field kind.
pub const INJ_MOTION_CURSOR: u8 = 0;
pub const INJ_MOTION_WHEEL: u8 = 1;
/// `MOVE` flags byte: the per-command movement-riding override (§3.1).
pub const MV_F_NOW: u8 = 0x01;
pub const MV_F_FLUSH: u8 = 0x02;
pub const MV_F_DISCARD: u8 = 0x04;

pub const Q_VERSION: u8 = 0;
pub const Q_HEALTH: u8 = 1;
/// Cloned device identity: vid/pid/bcd + serial/bos flags + primary kind + product (§4.3).
pub const Q_DEVICE_INFO: u8 = 2;
/// Unified device capabilities (mouse, keyboard, per-class change_driven) for the whole cloned device (§4.4).
pub const Q_CAPS: u8 = 3;
/// Live native report rate + clone poll period + confidence (§4.5).
pub const Q_RATE: u8 = 4;
/// Delivery/telemetry counters (§4.6).
pub const Q_STATS: u8 = 5;
/// Active lock bitmask (§4.8, v1.5.0).
pub const Q_LOCKS: u8 = 6;
/// Active catch subscription mask + dropped-event count (§4.9, v1.6.0).
pub const Q_CATCH: u8 = 7;
// selector 8 retired (was Q_KBD_CAPS; keyboard caps folded into the unified Q_CAPS = 3).
/// Persistent box options, read one at a time by id: `QUERY [Q_OPTIONS][id]` → `RESP [Q_OPTIONS][id][value..]` (§4.14).
pub const Q_OPTIONS: u8 = 9;

/// `OPTION` id: imperfect-clone opt-in. Set value `[allow u8]`; readback adds `over_capacity`/`clone_imperfect`.
pub const OPT_IMPERFECT: u8 = 0;
/// `OPTION` id: movement riding. Value `[timeout u16 LE ms]`; 0 = off, N = ride window in ms.
pub const OPT_MOVE_RIDE: u8 = 1;
/// `OPTION` id: emit-rate pacing. Value `[mode u8][rate_hz u16 LE][force_hz u16 LE]`; mode 0 learnt / 1 bInterval / 2 fixed.
pub const OPT_EMIT: u8 = 2;
/// `OPTION` id: box name. Value `[name ascii]` 1..32 printable ASCII (0 bytes clears to default); set-only, read off `RESP(VERSION)`.
pub const OPT_NAME: u8 = 3;
/// `OPTION` id: the bearing. Value `[window u16 LE ms][mode u8]`; what `LOCK_DIR_WITH`/`AGAINST` are measured against (§3.12).
pub const OPT_BEARING: u8 = 4;
/// `OPTION` id: what the box renders motion with. Value `[mode u8][full u8]`; mode 0 off / 1 stock / 2 de-spiked / 3 unsmoothed.
pub const OPT_RENDER: u8 = 5;
/// `OPTION` id: how far an injected delta is spread across the host's command interval. Value `[percent u16 LE]`.
pub const OPT_SPREAD: u8 = 6;
/// `OPTION(BEARING)` mode: each axis reads its own sign against its own bearing.
pub const BEARING_PER_AXIS: u8 = 0;
/// `OPTION(BEARING)` mode: the physical delta is projected onto the injected XY vector; motion across it is untouched.
pub const BEARING_VECTOR: u8 = 1;

/// Buffered-clip status selector: `QUERY [Q_CLIP]` → `RESP(CLIP)` (§4.15).
pub const Q_CLIP: u8 = 10;
/// Both chips' firmware versions and slot state: `QUERY [Q_FIRMWARE]` → `RESP(FIRMWARE)` (§4.16).
pub const Q_FIRMWARE: u8 = 11;

/// `UPDATE` sub-ops (§3.13).
pub const OTA_OP_BEGIN: u8 = 0;
pub const OTA_OP_DATA: u8 = 1;
pub const OTA_OP_END: u8 = 2;
pub const OTA_OP_ABORT: u8 = 3;
pub const OTA_OP_ACTIVATE: u8 = 4;
/// Image bytes per `DATA` frame: the frame's 512 minus op, target and a 2-byte chunk index, rounded
/// down to a multiple of four so every flash write on the box is aligned.
pub const OTA_CHUNK: usize = 504;
/// `DATA` frames the box accepts before it must acknowledge, when it does not say otherwise.
pub const OTA_CREDIT: usize = 16;
/// `UPDATE_RESP` payload length: op, target, status, arg.
pub const UPD_RESP_LEN: usize = 7;
/// `CLIP_CTRL` engine verbs. Ops 0..5 are the shared action space (also a trigger `action` byte).
pub const CLIP_OP_START: u8 = 0;
pub const CLIP_OP_STOP: u8 = 1;
pub const CLIP_OP_PAUSE: u8 = 2;
pub const CLIP_OP_RESUME: u8 = 3;
pub const CLIP_OP_RESTART: u8 = 4;
pub const CLIP_OP_TOGGLE: u8 = 5;
pub const CLIP_OP_CLEAR: u8 = 6;
pub const CLIP_OP_FINALIZE: u8 = 7;
/// `CLIP_SET` scalar setting ids (OPTION-shaped `[id][value]`).
pub const CLIP_SET_AUTOLOCK: u8 = 0;
pub const CLIP_SET_LOOP: u8 = 1;
pub const CLIP_SET_RETAIN: u8 = 2;
pub const CLIP_SET_RIDE: u8 = 3;
/// `CLIP_TRIGGER` set: max bindings and the flags byte bits.
pub const CLIP_TRIG_MAX: usize = 8;
pub const CLIP_TRIG_F_PRESENT: u8 = 0x01;
pub const CLIP_TRIG_F_CONSUME: u8 = 0x02;
/// `RESP(CLIP)` config-section flags byte.
pub const CLIP_CFG_F_LOOP: u8 = 0x01;
pub const CLIP_CFG_F_RETAIN: u8 = 0x02;
pub const CLIP_CFG_F_FINALIZED: u8 = 0x04;
pub const CLIP_CFG_F_RIDE: u8 = 0x08;
/// Autolock scope bits (`CLIP_SET(AUTOLOCK)` value): which physical-input classes the clip auto-locks (0 = none).
pub const CLIP_LOCK_AIM: u8 = 0x01;
pub const CLIP_LOCK_WHEEL: u8 = 0x02;
pub const CLIP_LOCK_BUTTONS: u8 = 0x04;
pub const CLIP_LOCK_KEYS: u8 = 0x08;
pub const CLIP_LOCK_MEDIA: u8 = 0x10;
/// Trigger binding class/id wildcards: any class, any usage within a class.
pub const CLIP_COND_ANY_CLASS: u8 = 0xFF;
pub const CLIP_COND_ANY_ID: u16 = 0xFFFF;
/// Clip entry tags/flags (see [`ClipBuilder`](crate::ClipBuilder)).
pub const CLIP_TAG_GAP: u8 = 0x00;
pub const CLIP_F_XY: u8 = 0x01;
pub const CLIP_F_WHEEL: u8 = 0x02;
pub const CLIP_F_EDGES: u8 = 0x04;

pub const BTN_LEFT: u8 = 0;
pub const BTN_RIGHT: u8 = 1;
pub const BTN_MIDDLE: u8 = 2;
pub const BTN_SIDE1: u8 = 3;
pub const BTN_SIDE2: u8 = 4;
pub const BTN_COUNT: u8 = 5;

/// Clear our injected press; defer to physical state.
pub const ACT_SOFTREL: u8 = 0;
/// Force the button down regardless of physical state.
pub const ACT_PRESS: u8 = 1;
/// Force the button up, masking a physical hold.
pub const ACT_FORCEREL: u8 = 2;

/// Inter-chip link to the host chip is up.
pub const H_LINK_UP: u8 = 0x01;
/// A real mouse is attached on the host chip.
pub const H_MOUSE_ATT: u8 = 0x02;
/// The clone has been configured by the game PC.
pub const H_CLONE_CFG: u8 = 0x04;
/// Injection is currently active.
pub const H_INJECT_ON: u8 = 0x08;
/// The native-rate estimator window is full, so the `RATE` value is trustworthy (§4.2, v1.4.0).
pub const H_RATE_CONFIDENT: u8 = 0x10;
/// At least one lock is active (§4.2, v1.5.0).
pub const H_LOCK_ON: u8 = 0x20;
/// A catch subscription is active; physical-input events are streaming (§4.2, v1.6.0).
pub const H_CATCH_ON: u8 = 0x40;
/// A keyboard is attached on the host chip, cloned and injectable (§4.2, v2.0.0).
pub const H_KBD_ATT: u8 = 0x80;

/// `CATCH` class: a mouse button (§3.9). Classes 0..3 are `LOCK`'s classes unchanged.
pub const CATCH_CLS_BTN: u8 = 0;
/// `CATCH` class: a keyboard key or modifier (§3.9).
pub const CATCH_CLS_KEY: u8 = 1;
/// `CATCH` class: a media (Consumer) usage (§3.9).
pub const CATCH_CLS_MEDIA: u8 = 2;
/// `CATCH` class: a relative axis (§3.9).
pub const CATCH_CLS_AXIS: u8 = 3;
/// `CATCH` class: raw HID input report bytes, keyed by interface number (§3.9).
pub const CATCH_CLS_HID_IN: u8 = 4;
/// `CATCH` class: interrupt-OUT report bytes, keyed by endpoint address (§3.9).
pub const CATCH_CLS_HID_OUT: u8 = 5;
/// `CATCH` class: vendor-interface interrupt traffic, keyed by endpoint address (§3.9).
pub const CATCH_CLS_VEND_INTR: u8 = 6;
/// `CATCH` class: vendor-interface bulk traffic, keyed by endpoint address (§3.9).
pub const CATCH_CLS_VEND_BULK: u8 = 7;
/// `CATCH` class: a proxied control transaction, keyed by endpoint number (§3.9).
pub const CATCH_CLS_CONTROL: u8 = 8;
/// `CATCH` class: the bytes the clone emitted, keyed by interface number (§3.9).
pub const CATCH_CLS_EMIT: u8 = 9;
/// `CATCH` class: bus lifecycle events (§3.9).
pub const CATCH_CLS_BUS: u8 = 10;
/// `CATCH` class wildcard: every class (§3.9).
pub const CATCH_CLS_ANY: u8 = 0xFF;
/// `CATCH` id wildcard: every id within the class (§3.9), the same sentinel `LOCK` uses.
pub const CATCH_ID_ANY: u16 = 0xFFFF;

/// Entries the box's subscription table holds (`CATCH_TAB_MAX`). Past this the box refuses the entry
/// and says so only in `RESP(CATCH).table_full`, so a caller that never reads that flag would get a
/// silently incomplete subscription.
pub const CATCH_MAX_ENTRIES: usize = 32;

/// `TRAFFIC_EVENT.flags` for a `CATCH_CLS_CONTROL` event: what the real device answered.
pub const CATCH_CTRL_OK: u8 = 0x00;
/// The device STALLed the transfer.
pub const CATCH_CTRL_STALL: u8 = 0xFD;
/// The device NAKed to timeout, or never answered.
pub const CATCH_CTRL_NAK: u8 = 0xFE;

/// `RESP(CATCH).clk_rate_ppb` sentinel: the box has fitted no drift rate. Distinct from a fitted 0,
/// which says the two crystals are matched.
pub const CLK_RATE_NONE: i32 = i32::MIN;

/// `LOCK` class byte (§3.8): momentary usages share `INJECT`'s space, plus a relative-axis class.
pub const LOCK_CLS_BTN: u8 = 0;
pub const LOCK_CLS_KEY: u8 = 1;
pub const LOCK_CLS_MEDIA: u8 = 2;
pub const LOCK_CLS_AXIS: u8 = 3;
/// `LOCK` id sentinel: the whole class (a blanket lock).
pub const LOCK_ID_ALL: u16 = 0xFFFF;
/// `LOCK` axis ids (for `LOCK_CLS_AXIS`).
pub const LOCK_AXIS_X: u16 = 0;
pub const LOCK_AXIS_Y: u16 = 1;
pub const LOCK_AXIS_WHEEL: u16 = 2;
/// `LOCK` direction byte: both / positive-or-press / negative-or-release, then the two measured
/// against the bearing rather than a fixed sign (§3.12).
pub const LOCK_DIR_BOTH: u8 = 0;
pub const LOCK_DIR_POS: u8 = 1;
pub const LOCK_DIR_NEG: u8 = 2;
pub const LOCK_DIR_WITH: u8 = 3;
pub const LOCK_DIR_AGAINST: u8 = 4;
/// `LOCK` scale byte (§3.8): percent of the physical value kept. 0 blocks, 100 passes it untouched,
/// above 100 amplifies, to a ceiling of 255 (2.55x).
pub const LOCK_SCALE_BLOCK: u8 = 0;
pub const LOCK_SCALE_PASS: u8 = 100;
pub const LOCK_SCALE_MAX: u8 = 255;

/// `CAPS` kbd_flags: keys are an NKRO bitmap (`n_keys` = 0xFF), else a keycode array (§4.4).
pub const KBC_NKRO: u8 = 0x01;
/// `CAPS` kbd_flags: a Consumer (media-key) collection is present and injectable/catchable.
pub const KBC_CONSUMER: u8 = 0x02;
/// `CAPS` kbd_flags: a System-control collection is present (passthrough-only, not injectable).
pub const KBC_SYSTEM: u8 = 0x04;
/// `CAPS` kbd_flags: the keyboard report sits behind a HID report ID.
pub const KBC_REPORT_ID: u8 = 0x08;

/// `CAPS` change_driven flag: the mouse class is change-driven (never set, mouse motion is continuous).
pub const CAPS_CD_MOUSE: u8 = 0x01;
/// `CAPS` change_driven flag: the keyboard/media class is change-driven (set when a keyboard is bound).
pub const CAPS_CD_KBD: u8 = 0x02;

/// `DEVICE_INFO` flag: the clone serves a serial string (§4.3).
pub const DI_HAS_SERIAL: u8 = 0x01;
/// `DEVICE_INFO` flag: the clone serves a BOS descriptor (§4.3).
pub const DI_HAS_BOS: u8 = 0x02;

/// `CAPS` axis flag: relative X present (§4.4).
pub const CAP_X: u8 = 0x01;
/// `CAPS` axis flag: relative Y present (§4.4).
pub const CAP_Y: u8 = 0x02;
/// `CAPS` axis flag: wheel present (§4.4).
pub const CAP_WHEEL: u8 = 0x04;
/// `CAPS` axis flag: the mouse report sits behind a HID report ID (§4.4).
pub const CAP_REPORT_ID: u8 = 0x08;

/// `RATE` flag: estimator window full (same source as [`H_RATE_CONFIDENT`], §4.5).
pub const RATE_CONFIDENT: u8 = 0x01;
/// `RATE` flag: the active input is change-driven (keyboard/media); no continuous cadence, poll floor only.
pub const RATE_CHANGE_DRIVEN: u8 = 0x02;

pub const LOG_ERROR: u8 = 0;
pub const LOG_WARN: u8 = 1;
pub const LOG_INFO: u8 = 2;
pub const LOG_DEBUG: u8 = 3;
pub const LOG_VERBOSE: u8 = 4;

/// A frame opcode (the `TYPE` byte, §3 / §4).
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FrameType {
    /// `MOVE`: relative-axis movement, motion-tagged (cursor dx/dy or wheel dz) (PC→box).
    Move = 0x01,
    /// `INJECT`: set a momentary-usage override (button/key/media), class-tagged (PC→box).
    Inject = 0x03,
    /// `RESET`: clear all injection (PC→box).
    Reset = 0x04,
    /// `QUERY`: request a state snapshot, elicits `RESP` (PC→box).
    Query = 0x05,
    /// `RESP`: reply to a `QUERY`, `SEQ` echoes the request (box→PC).
    Resp = 0x06,
    /// `REBOOT_DL`: reboot a chip to ROM download or to run (PC→box).
    RebootDl = 0x07,
    /// `LOG`: unsolicited device diagnostics (box→PC).
    Log = 0x08,
    /// `LED`: status LED override (PC→box).
    Led = 0x09,
    /// `LOCK`: lock/unlock an axis or button edge (PC→box).
    Lock = 0x0A,
    /// `CATCH`: subscribe to the physical-input event stream (PC→box).
    Catch = 0x0B,
    /// `MOTION_EVENT`: one unsolicited relative-axis catch event (dx/dy/dz); `SEQ` rolling (box→PC).
    MotionEvent = 0x0C,
    /// `USAGE_EVENT`: one unsolicited held-usage snapshot (class-tagged button/key/media); box→PC.
    UsageEvent = 0x0F,
    /// `TRAFFIC_EVENT`: one unsolicited byte-oriented catch event (HID / vendor / control / emit / bus); box→PC.
    TrafficEvent = 0x16,
    /// `OPTION`: set a persistent box option by id (imperfect-clone, movement riding, emit pacing, box name) (PC→box).
    Option = 0x11,
    /// `CLIP_APPEND`: append buffered-clip entries to the device ring; `SEQ` = append seq (PC→box).
    ClipAppend = 0x12,
    /// `CLIP_CTRL`: a clip engine verb (start/stop/pause/resume/restart/toggle/clear/finalize) (PC→box).
    ClipCtrl = 0x13,
    /// `CLIP_SET`: a clip scalar setting `[id][value]` (autolock/loop/retain) (PC→box).
    ClipSet = 0x14,
    /// `CLIP_TRIGGER`: add/remove a clip trigger binding `[class][id u16][edge][action][flags]` (PC→box).
    ClipTrigger = 0x15,
    /// `UPDATE`: stage and activate firmware on either chip (PC→box) (§3.13).
    Update = 0x17,
    /// `UPDATE_RESP`: the answer to one `UPDATE` op (box→PC) (§4.16).
    UpdateResp = 0x18,
}

/// Error returned when a byte does not name a known [`FrameType`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownFrameType(pub u8);

impl fmt::Display for UnknownFrameType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "unknown frame type 0x{:02X}", self.0)
    }
}

impl core::error::Error for UnknownFrameType {}

impl TryFrom<u8> for FrameType {
    type Error = UnknownFrameType;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        Ok(match value {
            0x01 => FrameType::Move,
            0x03 => FrameType::Inject,
            0x04 => FrameType::Reset,
            0x05 => FrameType::Query,
            0x06 => FrameType::Resp,
            0x07 => FrameType::RebootDl,
            0x08 => FrameType::Log,
            0x09 => FrameType::Led,
            0x0A => FrameType::Lock,
            0x0B => FrameType::Catch,
            0x0C => FrameType::MotionEvent,
            0x0F => FrameType::UsageEvent,
            0x16 => FrameType::TrafficEvent,
            0x11 => FrameType::Option,
            0x12 => FrameType::ClipAppend,
            0x13 => FrameType::ClipCtrl,
            0x14 => FrameType::ClipSet,
            0x15 => FrameType::ClipTrigger,
            0x17 => FrameType::Update,
            0x18 => FrameType::UpdateResp,
            other => return Err(UnknownFrameType(other)),
        })
    }
}

impl From<FrameType> for u8 {
    fn from(t: FrameType) -> u8 {
        t as u8
    }
}