VersionFirmware identity and box id

Firmware identity from query_version(). Display prints fw M.m.p and omits proto_ver, so read proto_ver from the field.

FieldTypeMeaning
proto_veru8Wire-protocol version the firmware speaks (9 here).
fw_majoru8Firmware major version.
fw_minoru8Firmware minor version.
fw_patchu8Firmware patch version.
mac[u8; 6]The device chip's base MAC, a stable per-box id.
nameStringThe box's human-readable name (a synthesised default when unset), set via set_name.
MethodReturnsMeaning
mac_hex()StringThe MAC as 12 lowercase hex digits, the id used by open_by_id.
EXAMPLE
use medius::Version;

let v = Version { proto_ver: 9, fw_major: 3, fw_minor: 4, fw_patch: 6, mac: [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], name: "Loki".into() };
assert_eq!(v.to_string(), "fw 3.4.6"); // Display omits proto_ver
assert_eq!(v.mac_hex(), "123456789abc");
println!("{v} (protocol {}, box {}, name {})", v.proto_ver, v.mac_hex(), v.name);

HealthBox readiness flags

Box readiness from query_health(), one bool per bit of a u16 flags word. from_flags(u16) and to_flags() convert it: bits 0-7 hold the first eight fields, and the high byte holds the advanced control layer's three.

FieldTypeTrue when
link_upboolThe link to the host chip is up.
mouse_attachedboolA real mouse is plugged in.
clone_configuredboolThe game PC has configured the clone.
injection_activeboolThe box is holding at least one injected button or move.
rate_confidentboolA mouse is bound and the native-rate estimator window is full, so Rate is trustworthy.
lock_onboolAt least one input is off a full pass, whether locked or merely scaled.
catch_onboolThe catch table holds at least one CatchFilter, whatever class it addresses.
kbd_attachedboolA keyboard is attached on the host chip, cloned and injectable.
rewrite_onboolThe rewrite-rule table is non-empty (v3.4.0).
patch_onboolThe clone serves a descriptor-patch set (v3.4.0).
transform_onboolA field transform is active (v3.4.0).
EXAMPLE
use medius::Health;

let h = Health::from_flags(0b0000_0011); // link_up | mouse_attached
assert!(h.link_up && h.mouse_attached);
assert!(!h.clone_configured && !h.rewrite_on);
assert_eq!(h.to_flags(), 0b0000_0011); // round-trips to the same word

DeviceInfoClone's USB identity, kind, product

USB identity from device_info(). With nothing cloned, every field is zero or empty, and is_cloned() reads false. A clone with no HID interface (a vendor-class pad, n_hid 0 in Caps) still has its identity filled in. Display prints VVVV:PPPP product.

FieldTypeMeaning
vidu16USB vendor id (idVendor).
pidu16USB product id (idProduct).
bcd_deviceu16Device release (bcdDevice).
bcd_usbu16USB version (bcdUSB), e.g. 0x0200.
has_serialboolThe clone serves a serial string.
has_bosboolThe clone serves a BOS descriptor.
kindDeviceKindThe real device's primary kind, from its HID report descriptors.
productStringThe product string the real device serves, in ASCII with ? for each character outside it (empty when it serves none).
EXAMPLE
use medius::{DeviceInfo, DeviceKind};

let d = DeviceInfo {
    vid: 0x046D, pid: 0xC08B, bcd_device: 0, bcd_usb: 0x0201,
    has_serial: true, has_bos: true, kind: DeviceKind::Mouse, product: "G502".into(),
};
assert_eq!(d.to_string(), "046D:C08B G502"); // Display is VVVV:PPPP product
assert!(d.is_cloned());
assert!(!DeviceInfo::default().is_cloned()); // nothing cloned

CapsWhole clone, mouse and keyboard

Everything one caps() query returns. has_mouse() / has_keyboard() report which are bound. is_composite() is true when the clone has >1 HID interface.

FieldTypeMeaning
mouseMouseCapsThe mouse half (all-zero when no mouse is bound).
keyboardKbdCapsThe keyboard half (all-zero when no keyboard is bound).
mouse_change_drivenboolAlways false: mouse motion is continuous, so its Rate has a learned cadence.
kbd_change_drivenboolTrue when a keyboard is bound: it reports only on a key change, so its rate has no continuous cadence.
EXAMPLE
let caps = device.caps()?;
if caps.has_keyboard() && caps.keyboard.has_consumer {
    // media injection works on this clone
}
println!("{} mouse buttons", caps.mouse.n_buttons);

MouseCapsWhat the cloned mouse can do

Semantic capabilities from caps(). Every field is zero when no relative-axis mouse interface is bound. is_composite() is true when n_hid > 1.

FieldTypeMeaning
n_buttonsu8Buttons the mouse report carries.
has_xboolThe report carries an X axis.
has_yboolThe report carries a Y axis.
has_wheelboolThe report carries a wheel.
has_panboolThe report carries an AC Pan (horizontal scroll) axis.
has_report_idboolThe mouse report sits behind a HID report ID.
n_hidu8Cloned HID interfaces. >1 = composite. 0 with nothing cloned, or for a clone with no HID interface (DeviceInfo::is_cloned tells them apart).
EXAMPLE
use medius::MouseCaps;

let c = MouseCaps { n_buttons: 5, has_x: true, has_y: true, has_wheel: true, has_pan: false, has_report_id: false, n_hid: 1 };
assert!(!c.is_composite()); // single HID interface

RateNative report rate the box tracks

Live rate from query_rate(). native_hz() converts the period to hertz. It returns None while native_period_us is 0. On a change-driven input, poll_period_us is the only rate.

FieldTypeMeaning
native_period_usu16Realised native report period in µs; 0 = not learned, or change-driven.
poll_period_usu16Cloned inject-endpoint poll period in µs.
confidentboolThe estimator window is full.
change_drivenboolThe active input is event-driven (keyboard / media), with no continuous cadence.
EXAMPLE
use medius::Rate;

let r = Rate { native_period_us: 1000, poll_period_us: 1000, confident: true, change_driven: false };
assert_eq!(r.native_hz(), Some(1000.0));

StatsDelivery and telemetry counters

Delivery counters from query_stats(), decoded from the 31-byte RESP(STATS).

Nonzero tx_drops or tx_wedges means native input was lost on the way to the PC. Nonzero link_rx_drops or host_rx_drops means it was lost between the box's two chips. relay_drops is relayed traffic and commands that went no further, not lost native input. The narrowed fields saturate instead of wrapping. link_rx_drops, host_rx_drops and relay_drops are full width, and session wraps.

FieldTypeMeaning
inject_emitsu32Pure-injection reports emitted.
tx_dropsu16Reports the clone's IN queue could not hold, so the game PC never received them (should stay 0).
tx_mergesu16Backed-up reports merged instead of queued.
tx_maxdepthu8Deepest the TX queue has reached.
tx_wedgesu8Wedged-endpoint recoveries.
wakeupsu16Remote wakeups the clone signalled to the game PC, for the real device or an injection.
reset_countu16USB bus resets seen.
config_countu16SET_CONFIGURATION events (re-enumerations).
link_rx_dropsu32Input frames the device chip could not take off the link from the host chip (should stay 0).
host_rx_dropsu32The same count on the host chip, relayed over the link (should stay 0).
relay_dropsu32Relayed traffic and commands that went no further (none of it native input). Each of these counts: a vendor IN packet that found its endpoint's queue full of the control PC's RAW packets; a native zero-length packet answering a poll the PC didn't make (the one poll the box runs ahead of a suspended PC); an OUT packet the box had no room to queue, that failed three times on the real device's bus, or that a bus reset or SET_INTERFACE overtook; a RAW OUT packet with no room to wait; a relayed request the box couldn't queue; a flagged MOVE the motion batch couldn't take; any frame other than native input that either chip's link receive ring dropped; a frame from the control PC that its receive ring had no room for (one per frame); an overrun of the control port's UART (one per overrun, however many frames it cost).
sessionu16Releases of some or all of the session state a host set: held input, locks, subscriptions, rules, transforms, the clip, and an LED override. The library re-sends all but the LED override. 0 at boot. It wraps, so compare for inequality. The library watches it for session recovery. The native RESP(STATS) lists what counts.

LocksActive input scales

The active set from query_locks(), a list of LockEntry across every class, one per direction not passing untouched. The wire format is on the native LOCKS reply.

MethodReturnsMeaning
entries()&[LockEntry]Every weighed direction, one entry each, across specific targets and whole-class blankets.
scale_of(target, dir)i16Percent of the native value kept there. 100 when nothing weighs it. Where entries overlap, it reports the one that keeps least by magnitude, the negative one on a tie: a block outranks a reversal of any size. Both does the same across every direction. That isn't the figure a delta meets: a delta gets one scale from each pair, multiplied.
is_locked(target, dir)boolWhether it is blocked outright. A direction merely weighed is not locked. Both asks about the two fixed signs. Ask for a relative one by name.
from_entries(Vec<LockEntry>)LocksBuild one from entries, for tests and the MockBox.
CaseContents
A blanket key lockOne entry per blocked edge, never Both.
A media lock, blanket or specificDirection Both, always. Media has no edges.
A relative direction under BearingMode::VectorThe effective scale, the lower of X's and Y's, on both axes.
85 entries reachedThe rest is absent, with nothing marking it (native LOCKS budget).
EXAMPLE
use medius::{Axis, Button, Direction};

let locks = device.query_locks()?;
if locks.is_locked(Axis::X, Direction::Positive) {
    // native +X is zeroed
}
if locks.is_locked(Button::LEFT, Direction::Negative) {
    // a left-click is latched down: the release edge is blocked
}
// how much of a delta opposing the injection survives
println!("{}%", locks.scale_of(Axis::X, Direction::Against));

LockEntryOne entry in a Locks list

struct LockEntry { scope: LockScope, direction: Direction, scale: i16 }

One weighed direction in a Locks list. Entries mirror the LOCK frame field for field, so an entry is the frame that reproduces it.

FieldTypeMeaning
scopeLockScopeA specific axis or usage, or a whole-class blanket.
directionDirectionWhich direction of it this entry weighs.
scalei16Percent of the native value kept, signed: a negative scale reverses what it keeps. A momentary usage is one bit, so the box stores the block or pass it applies, never a value between them or a negative one.

is_block() is scale == 0: blocked outright rather than weighed.

BearingWhat With and Against are measured against

struct Bearing { window: Option<Duration>, mode: BearingMode }

The configured bearing from query_bearing(). The native bearing describes what it does.

FieldTypeMeaning
windowOption<Duration>How long an axis holds the direction of its last injected delta. None is off: With and Against then do nothing, whatever their scale.
modeBearingModePerAxis: each axis reads its sign. Vector: the native delta is projected onto the injected direction, and the relative scale weighs only the part along it.

is_live() reports whether a bearing is held.

CatchFilterWhat to catch, and how much

struct CatchFilter { /* private */ }

One entry in the table passed to catch_events or input_events, built with a constructor.

ConstructorAddresses
CatchFilter::watch(usage)One Usage: a button, a key, or a media usage, the same argument lock takes.
CatchFilter::watch_axis(axis)One Axis.
CatchFilter::watch_class(class)Every usage in one Class.
CatchFilter::watch_axes()Every axis.
CatchFilter::all_input()All four input classes, as a [CatchFilter; 4].
CatchFilter::traffic(class, id)One id in a TrafficClass: an interface or endpoint number.
CatchFilter::traffic_class(class)Every id in one traffic class, a blanket.
CatchFilter::everything()Every class, every id, both directions.
MethodReturnsMeaning
.on_press() / .on_release()CatchFilterOne edge, on the momentary classes.
.inbound() / .outbound()CatchFilterOne flow, on the traffic classes: IN is device to PC.
.with_direction(dir)CatchFilterThe Direction directly. Defaults to Both.
.with_capture(cap)CatchFilterA Capture. Defaults to Whole. Traffic classes only.
.class()Option<CatchClass>The class, or None for the wildcard.
.id()Option<u16>The class-specific id, or None for a blanket.
.direction() / .capture()Direction / CaptureWhat the filter was narrowed to.
.same_address(other)boolWhether both name the same box table entry, whatever their captures.

Matching order: an exact (class, id) before a class blanket, a class blanket before the wildcard, and a named direction before Both. The matching entry supplies the capture:

catch_events([
    CatchFilter::everything().with_capture(Capture::First(16)),   // everything, 16 bytes
    CatchFilter::traffic(TrafficClass::VendorInterrupt, 0x83),    // except 0x83, in full
])

  packet on 0x83  ->  traffic(VendorInterrupt, 0x83)  resolves  ->  whole packet
  packet on 0x84  ->  everything()                    resolves  ->  First(16)

Capture isn't part of a filter's address. Two filters naming one entry at different lengths are one box entry, at the wider length. same_address compares addresses only; == compares everything.

The table holds 32 entries. A larger subscription, an empty one, or a capture on an input class is refused before anything is sent (Error).

EXAMPLE
use medius::{CatchFilter, Class, TrafficClass};

// Press edges of every key, plus one control endpoint, plus bus context.
let stream = device.catch_events([
    CatchFilter::watch_class(Class::Key).on_press(),
    CatchFilter::traffic(TrafficClass::Control, 0),
    CatchFilter::traffic_class(TrafficClass::Bus),
])?;

// Dropping the stream clears the whole table.
drop(stream);

MotionEventOne native relative-axis event

The payload of a CatchEvent::Motion, read off an EventStream. It holds the native delta at the merge point, before lock suppression or injection, so a locked or injected axis still reports it.

FieldTypeMeaning
ts_usu32When the native report arrived, in box microseconds. Catch timestamps explains the clock.
clockClockDomainWhich chip's timer ts_us came from. Always HostChip here: native motion is stamped on the host chip as the real device's transfer completes.
dxi16X motion this report (right positive).
dyi16Y motion this report (down positive).
dzi16Wheel motion this report (up positive).
pani16AC Pan (horizontal scroll) this report (right positive).
EXAMPLE
use medius::{CatchEvent, CatchFilter};

let stream = device.catch_events([CatchFilter::watch_axes()])?;
if let CatchEvent::Motion(m) = stream.recv()? {
    println!("at {} us ({:?}): moved {} {}, wheel {}, pan {}", m.ts_us, m.clock, m.dx, m.dy, m.dz, m.pan);
}

UsageSnapshotOne native held-usage snapshot

The payload of a CatchEvent::Usages: every held Usage of one class (buttons, keys, or media, all one shape), captured before injection. Diff successive snapshots for press/release edges, or test one with is_held. A dropped frame self-corrects on the next.

FieldTypeMeaning
ts_usu32When the native report arrived, in box microseconds. Catch timestamps explains the clock.
clockClockDomainWhich chip's timer ts_us came from. Always HostChip: a held-usage snapshot is taken where the real device's report lands.
usagesVec<Usage>The held usages, one class per event.
MethodReturnsMeaning
is_held(usage)boolWhether usage is held. Takes any impl Into<Usage>.
class()Option<Class>The class of this snapshot, from its first usage, or None when empty.
EXAMPLE
use medius::{Button, CatchEvent, CatchFilter, Class};

let stream = device.catch_events([CatchFilter::watch_class(Class::Button)])?;
if let CatchEvent::Usages(s) = stream.recv()? {
    if s.is_held(Button::LEFT) {
        println!("left button held");
    }
}

InputEventDecoded input edge and its timestamp

struct InputEvent { ts_us: u32, clock: ClockDomain, input: Input }

What input_events yields. The Input is the edge, decoded from the held-usage snapshots the box sends.

FieldTypeMeaning
ts_usu32The report's arrival stamp, in the stamping chip's microseconds.
clockClockDomainAlways HostChip for native input.
inputInputThe decoded edge or motion.
EXAMPLE
use medius::{CatchFilter, Input};

for ev in device.input_events(CatchFilter::all_input())? {
    if let Input::Press(u) = ev.input {
        println!("{u:?} down at {}", ev.ts_us);
    }
}

TrafficEventBytes off one pipe, with what was cut

The payload of a CatchEvent::Traffic: one packet, one completed control transaction, or one bus event.

FieldTypeMeaning
ts_usu32When the transfer completed, in the microseconds of the chip named by clock.
clockClockDomainWhich chip stamped it. Varies by class and direction here, unlike the two input events.
classCatchClassWhich address space the event came from.
idu16The endpoint or interface number within that class.
directionDirectionPositive = IN (device to PC), Negative = OUT (PC to device).
flagsu8Class-specific (see below). 0 for the classes that define none.
true_lenu16The packet's length before the Capture cut it.
bytesVec<u8>What was captured, up to the entry's capture length.
MethodReturnsMeaning
truncated()boolWhether the capture or the frame ceiling cut this packet: bytes.len() < true_len.
rule_acted()boolWhether a rewrite rule at this event's class changed, dropped, answered or refused the packet. A Pass rule, or a Patch that changed nothing, leaves it false.
control_status()Option<ControlStatus>The handshake the game PC received, for a Control event.
zlp()boolWhether the event is a zero-length packet. An event is one packet, except a HidIn or Emit event on a HID endpoint, which carries a whole report of ≤64 bytes. On such an event with bytes, it means a zero-length packet ended the report after them.
bulk_end_of_transfer()boolWhether a VendorBulk event is the last of its transfer.

One event frame carries ≤180 bytes, so Capture::Whole still truncates a longer packet. truncated() reports it.

Classflags
HidIn, Emitb1 = a zero-length packet (on a HID endpoint, also one that ended the report after the event's bytes), read with zlp(). b7 = a rule acted on the packet, read with rule_acted().
HidOut, VendorInterruptb1 = a zero-length packet; b7 = a rule acted.
VendorBulkb0 = end of transfer, b1 = zero-length packet, b7 = a rule acted.
Controlb0-b1 = the handshake the game PC received, read with control_status() (see ControlStatus). b7 = a rule acted.
ClipTransferHow the clip's transfer ended, read with transfer_status() (see TransferStatus).
BusThe BusEvent kind.
everything else0.

A Control or ClipTransfer event is one completed transaction: bytes is the 8-byte SETUP packet then the data stage, and direction says which way that data went. A Control event is the transaction the game PC received, on every control endpoint. A request served from the box's value cache still raises one.

Control dataHolds
INThe reply the game PC received: after a ReplyPatch or ReplyReplace rule, or an Answer rule's payload.
OUTThe data stage the game PC sent, before a Patch or Replace rule rewrote it for the real device.
EXAMPLE
use medius::{Capture, CatchEvent, CatchFilter, TrafficClass};

let stream = device.catch_events([CatchFilter::traffic(TrafficClass::VendorInterrupt, 0x83)
    .with_capture(Capture::First(16))])?;
if let CatchEvent::Traffic(t) = stream.recv()? {
    println!("ep 0x{:02X} {:?}: {:02X?}", t.id, t.direction, t.bytes);
    if t.truncated() {
        println!("  cut: {} of {} bytes", t.bytes.len(), t.true_len);
    }
}

ButtonButton a command acts on

struct Button(pub u8)

The button an INJECT or LOCK command acts on, as an open numeric id. The five standard buttons are named constants, and the mouse may declare further ids (caps n_buttons). A Button converts Into<Usage> as class button, so inject takes one directly.

ConstantidMeaning
Button::LEFT0Left button.
Button::RIGHT1Right button.
Button::MIDDLE2Middle button.
Button::SIDE13First thumb button.
Button::SIDE24Second thumb button.
MethodReturnsMeaning
Button::new(id)ButtonAny 0-based id, named or not: Button::new(5) is the sixth button.
Button::from_id(id)ButtonThe same from a wire id byte. Total: every byte is a valid button.
.as_id()u8The wire id byte.
.0u8The id itself; the field is public.

Injecting a button the real device declares but never wires is descriptor-faithful: the box drives the full declared count, and one past it is a no-op.

UsageMomentary input: (class, id)

struct Usage { class: Class, id: u16 }

What inject drives and LockTarget locks. A Button, Key, and MediaKey each impl Into<Usage>, so any verb takes one directly. Usage::new(class, id) builds one by hand.

FieldTypeMeaning
classClassThe input class (button, key, or media).
idu16The class-specific id: a button id, a HID keycode, or a Consumer usage.
EXAMPLE
use medius::{Button, Class, Key, Usage};

let from_button: Usage = Button::LEFT.into();      // Class::Button, id 0
let from_key: Usage = Key::A.into();               // Class::Key, id 0x04
let by_hand = Usage::new(Class::Media, 0x00E9);    // volume up
device.press(from_button)?;                         // press takes any impl Into<Usage>

KeyHID keyboard keycode

A newtype over a HID keyboard/keypad usage. It converts Into<Usage>, so inject and press take one directly. Modifiers are the usages 0xE0-0xE7.

ItemReturnsMeaning
Key::A .. Key::LEFT_SHIFTKeyNamed consts for common keycodes and modifiers.
new(u8)KeyWrap any raw HID keycode.
usage()u8The raw keycode byte.
EXAMPLE
use medius::Key;

let a = Key::A;            // 0x04
let custom = Key::new(0x04);
assert_eq!(a.usage(), custom.usage());

MediaKey16-bit Consumer usage

A newtype over a 16-bit Consumer usage. It converts Into<Usage>, so inject and press take one directly.

ItemReturnsMeaning
MediaKey::VOLUME_UP .. MediaKey::MUTEMediaKeyNamed consts for common media usages.
new(u16)MediaKeyWrap any raw Consumer usage.
usage()u16The raw Consumer usage.
EXAMPLE
use medius::MediaKey;

let vol_up = MediaKey::VOLUME_UP;   // 0x00E9
let custom = MediaKey::new(0xE9);
assert_eq!(vol_up.usage(), custom.usage());

KbdCapsWhat the cloned keyboard can do

Semantic capabilities from caps(). Every field is zero when no keyboard is bound. has_consumer gates media injection.

FieldTypeMeaning
n_keysu8Keycode-array slots, or 0xFF for an NKRO bitmap.
nkroboolThe keyboard reports an NKRO bitmap.
has_consumerboolA Consumer collection is present (media keys injectable).
has_systemboolA system-control collection is present (relayed only).
has_report_idboolThe keyboard report sits behind a HID report ID.

CatchStateLive subscription table, read back

struct CatchState { table_full: bool, dropped: u32, clock: ClockEstimate, entries: Vec<CatchEntry> }

What query_catch() returns. Subscribing has no reply, so this is the only view of what the box accepted.

FieldTypeMeaning
table_fullboolAt least one entry was refused because the 32-slot table was full.
droppedu32Box-wide events shed under back-pressure, across every entry.
clockClockEstimateHow the host chip's and device chip's timers relate.
entriesVec<CatchEntry>The live subscription table, one entry per accepted CatchFilter. Empty = catching nothing.

An entry absent from entries was refused. If table_full is set, the reason was capacity rather than a malformed filter.

EXAMPLE
let c = device.query_catch()?;
if c.entries.is_empty() {
    println!("catching nothing");
}
if c.table_full {
    eprintln!("some filters were refused: the 32-entry table is full");
}
for e in &c.entries {
    println!("{:?} {:?} {:?} {:?} dropped={}",
             e.filter.class(), e.filter.id(), e.filter.direction(), e.filter.capture(), e.dropped);
}
println!("{} dropped box-wide", c.dropped);

CatchEntryOne accepted subscription, and what it lost

struct CatchEntry { filter: CatchFilter, dropped: u16 }

One row of the box's subscription table in a CatchState: the CatchFilter the box accepted, echoed back. A blanket comes back as one entry, not one row per id.

FieldTypeMeaning
filterCatchFilterThe subscription as the box holds it. Read it with class(), id(), direction(), capture().
droppedu16Events this entry could not queue.

The box-wide count on CatchState shows that events are being lost. This one shows which entry lost them. Vendor bulk drops first, because its queue drains last.

EXAMPLE
let c = device.query_catch()?;
for e in c.entries.iter().filter(|e| e.dropped > 0) {
    eprintln!("{:?} {:?} lost {} events", e.filter.class(), e.filter.id(), e.dropped);
}

ClockEstimateHow the two chips' timers relate

struct ClockEstimate { offset_us: i32, rate_ppb: Option<i32>, delay_us: u16, age: Option<Duration> }

The clock field of a CatchState, and the only thing that puts stamps from both clock domains on one timeline.

The box measures the difference with a four-timestamp exchange across the inter-chip link, stamping each frame as it reaches the wire rather than when it is queued. The two crystals drift by ≤20 µs/s.

FieldTypeMeaning
offset_usi32The host chip's clock minus the device chip's, in microseconds. Add it to a device-domain stamp to read it on the host's timeline. Subtract it to go the other way.
rate_ppbOption<i32>How fast the two are drifting apart, in parts per billion, or None when the box has fitted no rate. None isn't a fitted 0. On a link too busy for enough clean exchanges, no fit is made at all, and that is when assuming no drift costs the most.
delay_usu16The best round trip measured in the window. The offset is accurate to ~half of it.
ageOption<Duration>How long ago the exchange ran. None = no estimate yet.

error_bound_us() is half delay_us. drift_us_over(age) extrapolates rate_ppb, returning 0 for a None rate.

EXAMPLE
let clock = device.query_catch()?.clock;
match clock.age {
    None => println!("no cross-chip estimate yet: compare stamps within one domain only"),
    Some(age) => {
        let offset_now = clock.offset_us as i64 + clock.drift_us_over(age);
        println!("offset {offset_now} us, +/- {} us", clock.error_bound_us());
    }
}

TimelineBox stamps on this machine's clock

struct Timeline { /* private */ }

A catch stamp counts microseconds on a chip that booted before this process did. It wraps, restarts at zero on reboot, and has no relation to any clock here. Feed every event in as it arrives, in order. reset a domain whose chip restarted (a device-chip restart raises restarts).

&event is anything implementing Timestamped: an InputEvent, a CatchEvent, or one of the three frame structs. The decoded and raw paths share one timeline.

MethodReturnsMeaning
observe(&event)StampedPlace an event on this machine's clock, taking the arrival as now.
observe_at(&event, now)StampedThe same with the arrival supplied, for replaying a capture.
observe_stamp(ts_us, domain, now)StampedThe same from a stamp and domain held apart from an event.
box_us(&event)u64The stamp unwrapped past the rollover, monotonic within its domain.
reset(domain)()Forget one domain's rollover count and floor, for a chip that rebooted.
samples(domain)u64Events observed for a domain. The floor is a minimum over these.

Each domain is tracked separately, so both chips' stamps land on one comparable timeline.

The mapping keeps a per-domain minimum of (elapsed here minus elapsed on the box), not an average. The error is one-sided: an event can arrive late but never early. The mapping improves as it runs and never steps backwards.

EXAMPLE
use medius::{CatchFilter, Timeline};

let mut input = device.input_events(CatchFilter::all_input())?;
let mut time = Timeline::new();
for ev in input.by_ref().take(20) {
    println!("{:?} at {:?}", ev.input, time.observe(&ev).host);
}

StampedOne event placed on this machine's clock

struct Stamped { host: Instant, box_us: u64, excess: Duration }
FieldTypeMeaning
hostInstantWhen the event happened, on this machine's monotonic clock.
box_usu64The event's stamp, unwrapped past the 32-bit rollover.
excessDurationHow much later than the measured floor this event arrived. Jitter, not latency: the constant part of the delay is unknowable here.

TransformOne field operation and its two fields

struct Transform { op: TransformOp, source: LockTarget, dest: LockTarget }

One entry in the table passed to transform. The named constructors cover common cases. new is the general form.

MethodReturnsMeaning
Transform::new(op, source, dest)TransformAny TransformOp, from an explicit source and destination.
Transform::swap(a, b)TransformA Swap of two axes.
Transform::remap(source, dest)TransformA Remap. Any Axis, Button, Key, MediaKey or Usage converts into the LockTarget it takes.
.key()TransformKeyThe (source, dest) this entry is filed under.
FieldTypeMeaning
opTransformOpWhat the transform does. An op that can't address this source and destination pair returns Error::TransformOpFields.
sourceLockTargetThe field the transform reads: an axis, or a button. A key or media field can only be a destination, so naming one here returns Error::TransformOpFields. A field the clone's descriptor doesn't declare is refused box-side and is absent from the readback.
destLockTargetThe field the transform writes. Naming the same field as source returns Error::TransformOpFields. A cross-class remap with no destination collection is refused box-side.

The written value is clamped to the destination field's declared range. To weigh a field, or reverse it, use scale, which runs first.

EXAMPLE
use medius::{Axis, Button, Key, Transform, TransformOp};

let swap = Transform::swap(Axis::X, Axis::Y);
assert_eq!(swap.op, TransformOp::Swap);

let wheel_drives_y = Transform::remap(Axis::Wheel, Axis::Y); // the wheel drives Y
let side_key = Transform::remap(Button::new(4), Key::A);     // the fifth button types 'A'

TransformKey(source, dest) pair an entry is filed under

struct TransformKey { source: LockTarget, dest: LockTarget }

What Transform::key returns, and what untransform drops by. Two transforms sharing a key are one entry: setting the second overwrites the first's op in place, keeping its position in the table.

TransformsTransform table, read back

struct Transforms { table_full: bool, entries: Vec<Transform> }

What query_transforms returns: the held entries, as the commands that rebuild them. Every entry read back is a live one, and the table is re-asserted wholesale on reconnect.

FieldTypeMeaning
table_fullboolA further entry was, or would be, refused: the table holds Transforms::CAPACITY (32).
entriesVec<Transform>One per installed transform, in installation order (the order the box applies them). The order of two entries writing the same field matters.
EXAMPLE
use medius::Transforms;

let table = device.query_transforms()?;
println!("{}/{} transforms", table.entries.len(), Transforms::CAPACITY);
for t in &table.entries {
    println!("  {:?} {:?} -> {:?}", t.op, t.source, t.dest);
}

ImperfectStatusImperfect-clone state

The imperfect-clone state from query_imperfect().

FieldTypeMeaning
allowedboolThe opt-in toggle: a real device the box can't clone faithfully is cloned anyway.
over_capacityboolThe attached real device has more IN endpoints live at once than the box has transmit FIFOs (4), has >6 HID interfaces, or runs at high speed.
clone_imperfectboolThe live clone is imperfect: an opted-in real device the box can't clone faithfully, a forced rate, or an applied patch set.

EmitPaceStatusEmit-rate pacing and clone rate

The emit-rate pacing state from query_emit_pace().

FieldTypeMeaning
modeEmitPaceThe pace. Fixed carries the requested rate.
resolved_hzu16The ceiling in effect (Hz). 0 = learnt/adaptive, or no device yet in Interval. Once the renderer is on with a profile, 1000 in place of 0.
force_hzOption<u16>The forced wire rate requested. None keeps the native bInterval.
advertised_hzu16What the clone's input endpoints advertise now, forced or native. 0 = no clone.
force_activeboolWhether a forced interval is written into the descriptor being served.

RenderStatusMotion texture, and whether a profile armed

The render state from query_render().

FieldTypeMeaning
modeRenderModeRendering texture. Off is the paced fill.
fullboolWhether native motion is rendered instead of relayed.
readyboolWhether a profile has armed for the attached real device. Nothing is rendered until it has.

SpreadStatusSpread percent and interval in effect

The spread state from query_spread().

FieldTypeMeaning
percentu16Share of the command interval a delta is released across. 0 is the whole delta on the next report.
span_usu32The interval in effect, in microseconds. 0 whenever nothing is being spread.

LogLineOne line from the LOG stream

One line from the box's LOG stream, read off a LogStream.

FieldTypeMeaning
levelLogLevelSeverity tag.
textStringThe decoded message.

PortInfoDiscovered serial port

A serial port that looks like a Medius box, from find_medius(). serial is the CH343 adapter's serial string, part of the box identity.

FieldTypeMeaning
pathStringSerial port path.
vidu16USB vendor id (0x1A86).
pidu16USB product id (0x55D3).
serialOption<String>The CH343 adapter's serial string, when it serves one.

BoxInfoOne discovered box

One entry from Device::list(), and the value find_where's predicate receives.

FieldTypeMeaning
portPortInfoThe control port (path and CH343 serial).
versionVersionThe firmware version and control protocol, with the box MAC and name.
deviceOption<DeviceInfo>The real device it clones. None for a box whose version.proto_ver isn't PROTO_VER. Opening that box returns Error::BadProtoVer.
MethodReturnsMeaning
id()StringThe box identity: the MAC hex, as passed to open_by_id.
name()&strThe box's human-readable name (from version.name), a display label rather than an opener key.
serial()Option<&str>The CH343 adapter's serial, when it has one.

CountersSnapshotLink statistics snapshot

Five running link totals from counters().

FieldTypeMeaning
frames_txu64Frames sent to the box.
frames_rxu64Frames received from the box.
crc_dropsu64Inbound frames dropped on a bad checksum.
reconnectsu64Times the library reopened the port.
restartsu64Device-chip restarts the library recovered from by re-sending the state it holds. A session release, counted in Stats::session, leaves it alone.

LogStreamReceiver for the box LOG stream

Receives the box's LOG frames as LogLine values off a local channel, from device.logs(). No receive method touches the wire, and clones share the queue. Methods and an example are on Logs & counters.

ClipSettingsPersistent clip config, read back

A clip's configuration from ClipHandle::query_config(): the readback of the handle setters (set_autolock, set_loop, set_retain, set_ride, finalize, bind, bind_packet).

FieldTypeMeaning
autolockVec<Blanket>The Blanket groups auto-locked while playing (clip-owned, released on stop). Empty = no auto-lock.
loop_boolPlayback restarts from the top instead of stopping at the end.
retainboolThe buffered content survives a stop, so a restart replays it instead of needing a fresh append.
finalizedboolThe clip is sealed: no more appends, ready to replay as a fixed sequence.
rideboolThe clip's motion waits for native motion under movement riding. false (the default) plays it on the box's clock.
triggersVec<ClipTrigger>The bound input triggers (≤8), each firing a playback action on a native edge.
packet_triggersVec<ClipPacketTriggerEntry>The bound packet triggers (≤8), in the order the box holds them, each with its hit count.
EXAMPLE
let cfg = handle.query_config()?;
if cfg.loop_ && cfg.finalized {
    println!("sealed looping clip, {} triggers", cfg.triggers.len());
}
for e in &cfg.packet_triggers {
    println!("{:?} id {} -> {:?}, {} hits", e.trigger.class, e.trigger.id, e.trigger.action, e.hits);
}

ClipTriggerOne input binding that drives a clip

One native-input binding for a clip, handed to ClipHandle::bind. The box keeps ≤8, keyed by usage and edge.

Build one with the constructor, where consume defaults to false:

fn new(on: impl Into<Usage>, edge: Edge, action: ClipAction) -> ClipTrigger
FieldTypeMeaning
onUsageThe button, key, or media usage that fires the trigger.
edgeEdgeWhich edge fires it: Press, Release, or Both.
actionClipActionThe playback action to run (Start, Stop, Toggle, ...).
consumeboolLock the trigger usage while it is active, so its edge never reaches the PC. The .consume() builder sets it true.
EXAMPLE
use medius::{Button, ClipAction, ClipTrigger, Edge};

// Toggle the clip on a Side1 press, and suppress that press.
let trig = ClipTrigger::new(Button::SIDE1, Edge::Press, ClipAction::Toggle).consume();
handle.bind(trig)?;

ClipPacketTriggerOne packet binding that drives a clip

fn new(class: TrafficClass, id: u16, direction: Direction, action: ClipAction) -> ClipPacketTrigger
fn matching(self, match_bytes: impl Into<Vec<u8>>, mask: impl Into<Vec<u8>>) -> ClipPacketTrigger
fn consume(self) -> ClipPacketTrigger
fn once_per_run(self, selector_len: u8) -> ClipPacketTrigger

One packet binding for a clip, handed to ClipHandle::bind_packet. The box keeps ≤8 (CLIP_PKT_TRIG_MAX), keyed by (class, id, direction, match_bytes, mask). They share 112 match bytes (CLIP_PKT_MATCH_POOL).

FieldTypeMeaning
classTrafficClassThe surface the packet crosses: HidIn, HidOut, VendorInterrupt, VendorBulk, Control or Emit.
idu16The interface number for HidIn, the endpoint number for the rest, or ClipPacketTrigger::ANY_ID (0xFFFF) for every id of the class.
directionDirectionThe flow the packet travels in: IN, OUT or Both. With and Against return Error::RelativeDirection.
actionClipActionThe playback action to run, on the frame clock's next tick.
match_bytes, maskVec<u8>The head bytes and their mask, of one length, ≤16 bytes (PKT_MATCH_MAX). A packet matches when head[i] & mask[i] == match_bytes[i] for each byte. Empty matches every packet on the address. .matching() sets both.
consumeboolDrop every packet the trigger matches as the top-ranked trigger, ahead of the rewrite table. A consumed HidIn report is dropped whole. The box holds a consuming trigger only under the imperfect-clone opt-in. .consume() sets it true.
once_per_runboolRun the action on the first packet of a run of matching ones (a plain trigger runs it on each). .once_per_run(selector_len) sets it true.
selector_lenu8With once_per_run, how many leading match bytes select the run's stream within the address, such as a report ID. The rest are the condition. 0 without.
EXAMPLE
use medius::{ClipAction, ClipPacketTrigger, Direction, TrafficClass};

// Report ID 7 on interface 2 carries a button in bit 5 of its second byte. Start once per hold.
let held = ClipPacketTrigger::new(TrafficClass::HidIn, 2, Direction::IN, ClipAction::Start)
    .matching([0x07, 0x20], [0xFF, 0x20])
    .once_per_run(1);
handle.bind_packet(&held)?;

ClipPacketTriggerEntryOne packet trigger the box holds, read back

struct ClipPacketTriggerEntry { trigger: ClipPacketTrigger, hits: u16 }

One row of ClipSettings::packet_triggers.

FieldTypeMeaning
triggerClipPacketTriggerThe trigger, in the shape bind_packet takes, so a read entry replays as a bind.
hitsu16Packets the trigger has matched as the top-ranked trigger since it was bound or overwritten, saturating. A once_per_run trigger counts every packet of a run and runs its action on the first.

ClipStatusClip ring and playback state

The clip ring depth and playback counters from ClipHandle::query_status(). Pace top-ups off free. A ClipState::Faulted state means re-sync (clear, then rebuild).

FieldTypeMeaning
stateClipStateThe lifecycle state (idle / playing / paused / faulted).
freeu32Free bytes in the ring, the headroom for the next append.
totalu32The retained clip size in bytes. While streaming, the bytes buffered but not yet drained.
playedu32Bytes played from the clip start (retained progress; ~0 while streaming).
ticksu32Content frames played since the box booted (gap runs are not counted).
underrunsu16Underrun episodes (the ring ran dry mid-playback).
overrunsu16Appends dropped because the ring was full.
seq_gapsu16Append-sequence gaps seen (a dropped append frame).
xfersu16Clip transfers the real device completed.
xfer_errsu16Clip transfers that ended any other way: a refusal, no reply, no room in the box's queue, or dropped behind a transfer the real device didn't reply to.
gatedu16Raw reports and transfers the box discarded because the imperfect-clone opt-in was off.
heldVec<Usage>The usages the clip is holding down: buttons, keys, and media in one list, like a UsageSnapshot. Test one with is_held(usage).

ChipFirmwareWhat one chip is running

One chip's half of firmware_info(). Display renders it as major.minor.patch.

FieldTypeMeaning
major, minor, patchu8The firmware version this chip is running.
slotu8Which app slot it booted: 0 or 1.
stateImageStateWhether that image is confirmed, on probation, or rolled back.

FirmwareInfoBoth chips, and what is staged

Returned by firmware_info(). any_pending() is true while either chip is still on probation, which is when an update is refused.

FieldTypeMeaning
deviceChipFirmwareThe PC-facing chip.
hostOption<ChipFirmware>None when the host chip has not replied over the inter-chip link.
slot_sizeu32Usable bytes in a spare slot. The same on both chips.
device_stagedboolAn image is written and waiting to be activated.
host_stagedboolThe same, for the host chip.

UpdateProgressOne acknowledged window

Handed to the closure passed to stage_firmware(), once per acknowledged window rather than once per chunk.

FieldTypeMeaning
targetUpdateTargetThe chip being written.
sentusizeBytes the box has acknowledged.
totalusizeBytes in the whole image.
MethodReturnsMeaning
percent()u8sent as a percentage of total. 100 when total is 0.

SetupEight-byte USB setup packet

struct Setup { request_type: u8, request: u8, value: u16, index: u16, length: u16 }

What a transfer carries. It is the USB §9.3 setup packet: bmRequestType, bRequest, wValue, wIndex, wLength, little-endian on the wire.

FieldTypeMeaning
request_typeu8bmRequestType: direction in bit 7 (set is device-to-host / IN), then type and recipient.
requestu8bRequest: the request code.
valueu16wValue: request-specific.
indexu16wIndex: request-specific, often an interface or endpoint.
lengthu16wLength: the data-stage length. For an IN request, how many bytes to read back (the real device may return fewer). For an OUT request, the length of the data passed.
  byte   0            1          2   3     4   5     6   7
        +------------+----------+---------+---------+---------+
        |request_type| request  |  value  |  index  | length  |
        | bmRequest  | bRequest | wValue  | wIndex  | wLength |
        +------------+----------+---------+---------+---------+
                                 <-- LE -> <-- LE -> <-- LE ->

Setup::new builds one from its five fields, and is_in reads the direction bit. to_bytes gives the eight wire bytes (<BBHHH>, little-endian).

EXAMPLE
use medius::Setup;

// GET_DESCRIPTOR(Device): standard device-to-host request for the 18-byte device descriptor.
let setup = Setup::new(0x80, 0x06, 0x0100, 0x0000, 18);
assert!(setup.is_in());
assert_eq!(setup.to_bytes(), [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00]);

TransferOutcomeNative reply: status and any IN data

struct TransferOutcome { status: TransferStatus, data: Vec<u8> }

What transfer returns. A non-Ok status carries no data.

FieldTypeMeaning
statusTransferStatusHow the transfer ended.
dataVec<u8>The IN data the real device returned. Empty for an OUT transfer, a STALL, or no data.
MethodReturnsMeaning
.is_ok()boolWhether the transfer completed.
.data()&[u8]The returned data.
.ok_data()Option<Vec<u8>>The data if the transfer completed, else None.

RewriteRuleRule set_rewrite installs

fn new(class: RewriteClass, id: u16, direction: Direction, action: RewriteAction) -> RewriteRule
fn matching(self, match_bytes: impl Into<Vec<u8>>, mask: impl Into<Vec<u8>>) -> RewriteRule
fn at_offset(self, offset: u16) -> RewriteRule
fn with_payload(self, payload: impl Into<Vec<u8>>) -> RewriteRule

One entry in the table passed to set_rewrite, built by chaining onto new.

FieldTypeMeaning
classRewriteClassThe traffic class the rule addresses.
idu16The address within the class: an interface number or an endpoint number.
directionDirectionThe flow the rule matches: Both, Positive (IN) or Negative (OUT). With and Against resolve at emit time, after a rule is addressed, so either returns Error::RelativeDirection.
actionRewriteActionWhat the rule does to a matched packet. An action that doesn't fit the class returns Error::RewriteActionClass.
offsetu16Where a patching action writes. Other actions ignore it.
match_bytes, maskVec<u8>The head bytes and their mask, compared with the packet head byte for byte. They must be the same length, ≤16 bytes (REWRITE_MATCH_MAX), or the rule is an Error. Empty matches every packet on the address, a zero-length one included. A non-empty match never matches a zero-length packet.
payloadVec<u8>The bytes supplied by an action that carries one. A payload longer than the head the box holds for the class (64 bytes for a report class, an 8+2048-byte image for control) returns Error::RewritePayloadTooLarge.

RewriteTableRewrite table, read back

struct RewriteTable { table_full: bool, generation: u8, entries: Vec<RewriteEntry> }

What query_rewrite returns: a summary row per rule, without its match, mask, or payload bytes. query_rewrite_entry reads one in full.

FieldTypeMeaning
table_fullboolThe box refused the last new rule or overwrite for lack of room: all 32 entries in use (REWRITE_MAX_ENTRIES), or no space left in the 2048-byte payload pool (REWRITE_PAYLOAD_POOL). The next change to the table, or a clear, resets it. An identical re-send leaves it set. set_rewrite checks both limits before it sends.
generationu8Goes up by one on a change to the table, or a clear or silence that empties it. An identical re-send leaves it. reset, detach, link loss, re-clone and opt-in off return it to 0. The crate's keepalive re-sends every held rule, whatever generation reads (native lifecycle).
entriesVec<RewriteEntry>One row per rule, in table order, not the most-specific-first order the box selects a match by. An overwrite moves a rule to the end.

query_health reports a non-empty table in its rewrite_on flag.

RewriteEntryOne row in a RewriteTable

struct RewriteEntry { class: RewriteClass, id: u16, direction: Direction, action: RewriteAction, match_len: u8, offset: u16, payload_len: u16, hits: u16 }

One rule's summary in a RewriteTable: its address and action, the lengths of what it carries, and its hit count. The bytes come from query_rewrite_entry.

FieldTypeMeaning
class, id, direction, actionas setThe rule's address and action.
match_lenu8How many match and mask bytes the rule compares.
offsetu16The write offset for a patching action.
payload_lenu16How many payload bytes the rule carries.
hitsu16Packets the rule matched as the top-ranked rule since it was installed or last overwritten. A Pass rule counts too. Saturating.

PatchDescriptor overwrite set_patch stores

fn new(section: PatchSection, offset: u16, bytes: impl Into<Vec<u8>>) -> Patch
fn in_config(cfg: u8, offset: u16, bytes: impl Into<Vec<u8>>) -> Patch
fn in_interface(cfg: u8, interface: u8, offset: u16, bytes: impl Into<Vec<u8>>) -> Patch
fn in_string(index: u8, bytes: impl Into<Vec<u8>>) -> Patch

One entry in the set passed to set_patch, with a constructor per section.

FieldTypeMeaning
sectionPatchSectionThe descriptor this patch targets.
cfgu8The configuration index, for Config / Report. 0 is the first configuration, not bConfigurationValue.
indexu8The interface or string index, for Report / String.
offsetu16Where in the descriptor the overwrite starts. Ignored for String, which is replaced whole.
bytesVec<u8>The overwrite bytes. Empty removes the patch at this key.

PatchSetStored patch set and its apply state

struct PatchSet { applied: bool, pending: bool, refused: bool, table_full: bool, entries: Vec<PatchEntry> }

What query_patches returns, as four apply-state flags over a list of PatchEntry. The list is the stored set. query_health reports a patched clone in its patch_on flag.

FieldSet when
appliedThe clone serves a non-empty patched set: the one it was presented with. A later store doesn't change it until the next presentation.
pendingThe stored set differs from the one the clone serves, in its patches, bytes or order: not applied yet, changed or emptied since, refused, or held back because the opt-in is off.
refusedThe stored set failed a clone or consistency check when the clone was last presented with it, and is unchanged since. The clone serves the real device's descriptors unpatched, and the box log names the check. A set change, a clear, a presentation that passes, or a detach resets it. A real device that fails unpatched is refused with this flag clear.
table_fullThe box refused the last new patch or overwrite for lack of room: 16 patches in use (PATCH_MAX_ENTRIES), or no space left in the 1024-byte pool. The next change to the set, or a clear, resets it.
  set_patch --> stored set          (NVS, per VID:PID; pending while it differs from the served set)
                    |
                    |  apply_patch, the opt-in turning on, the real device attaching
                    v
                presented --- checks pass ---> applied   (the clone serves the patched set)
                    |
                    +-------- a check fails ---> refused   (served unpatched; the log names the check)

PatchEntryOne row in a PatchSet

struct PatchEntry { section: PatchSection, cfg: u8, index: u8, offset: u16, len: u16 }

One patch's key and length in a PatchSet, without its bytes. query_patch_entry reads one in full.

FieldTypeMeaning
section, cfg, index, offsetas setThe patch's key.
lenu16How many bytes the patch overwrites.