Structs
Public fields, returned by queries and discovery.
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.
| Field | Type | Meaning |
|---|---|---|
proto_ver | u8 | Wire-protocol version the firmware speaks (9 here). |
fw_major | u8 | Firmware major version. |
fw_minor | u8 | Firmware minor version. |
fw_patch | u8 | Firmware patch version. |
mac | [u8; 6] | The device chip's base MAC, a stable per-box id. |
name | String | The box's human-readable name (a synthesised default when unset), set via set_name. |
| Method | Returns | Meaning |
|---|---|---|
mac_hex() | String | The MAC as 12 lowercase hex digits, the id used by open_by_id. |
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.
| Field | Type | True when |
|---|---|---|
link_up | bool | The link to the host chip is up. |
mouse_attached | bool | A real mouse is plugged in. |
clone_configured | bool | The game PC has configured the clone. |
injection_active | bool | The box is holding at least one injected button or move. |
rate_confident | bool | A mouse is bound and the native-rate estimator window is full, so Rate is trustworthy. |
lock_on | bool | At least one input is off a full pass, whether locked or merely scaled. |
catch_on | bool | The catch table holds at least one CatchFilter, whatever class it addresses. |
kbd_attached | bool | A keyboard is attached on the host chip, cloned and injectable. |
rewrite_on | bool | The rewrite-rule table is non-empty (v3.4.0). |
patch_on | bool | The clone serves a descriptor-patch set (v3.4.0). |
transform_on | bool | A field transform is active (v3.4.0). |
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 wordDeviceInfo Clone'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.
| Field | Type | Meaning |
|---|---|---|
vid | u16 | USB vendor id (idVendor). |
pid | u16 | USB product id (idProduct). |
bcd_device | u16 | Device release (bcdDevice). |
bcd_usb | u16 | USB version (bcdUSB), e.g. 0x0200. |
has_serial | bool | The clone serves a serial string. |
has_bos | bool | The clone serves a BOS descriptor. |
kind | DeviceKind | The real device's primary kind, from its HID report descriptors. |
product | String | The product string the real device serves, in ASCII with ? for each character outside it (empty when it serves none). |
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 clonedCapsWhole 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.
| Field | Type | Meaning |
|---|---|---|
mouse | MouseCaps | The mouse half (all-zero when no mouse is bound). |
keyboard | KbdCaps | The keyboard half (all-zero when no keyboard is bound). |
mouse_change_driven | bool | Always false: mouse motion is continuous, so its Rate has a learned cadence. |
kbd_change_driven | bool | True when a keyboard is bound: it reports only on a key change, so its rate has no continuous cadence. |
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);MouseCaps What 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.
| Field | Type | Meaning |
|---|---|---|
n_buttons | u8 | Buttons the mouse report carries. |
has_x | bool | The report carries an X axis. |
has_y | bool | The report carries a Y axis. |
has_wheel | bool | The report carries a wheel. |
has_pan | bool | The report carries an AC Pan (horizontal scroll) axis. |
has_report_id | bool | The mouse report sits behind a HID report ID. |
n_hid | u8 | Cloned HID interfaces. >1 = composite. 0 with nothing cloned, or for a clone with no HID interface (DeviceInfo::is_cloned tells them apart). |
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 interfaceRateNative 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.
| Field | Type | Meaning |
|---|---|---|
native_period_us | u16 | Realised native report period in µs; 0 = not learned, or change-driven. |
poll_period_us | u16 | Cloned inject-endpoint poll period in µs. |
confident | bool | The estimator window is full. |
change_driven | bool | The active input is event-driven (keyboard / media), with no continuous cadence. |
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.
| Field | Type | Meaning |
|---|---|---|
inject_emits | u32 | Pure-injection reports emitted. |
tx_drops | u16 | Reports the clone's IN queue could not hold, so the game PC never received them (should stay 0). |
tx_merges | u16 | Backed-up reports merged instead of queued. |
tx_maxdepth | u8 | Deepest the TX queue has reached. |
tx_wedges | u8 | Wedged-endpoint recoveries. |
wakeups | u16 | Remote wakeups the clone signalled to the game PC, for the real device or an injection. |
reset_count | u16 | USB bus resets seen. |
config_count | u16 | SET_CONFIGURATION events (re-enumerations). |
link_rx_drops | u32 | Input frames the device chip could not take off the link from the host chip (should stay 0). |
host_rx_drops | u32 | The same count on the host chip, relayed over the link (should stay 0). |
relay_drops | u32 | Relayed 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). |
session | u16 | Releases 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.
| Method | Returns | Meaning |
|---|---|---|
entries() | &[LockEntry] | Every weighed direction, one entry each, across specific targets and whole-class blankets. |
scale_of(target, dir) | i16 | Percent 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) | bool | Whether 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>) | Locks | Build one from entries, for tests and the MockBox. |
| Case | Contents |
|---|---|
| A blanket key lock | One entry per blocked edge, never Both. |
| A media lock, blanket or specific | Direction Both, always. Media has no edges. |
A relative direction under BearingMode::Vector | The effective scale, the lower of X's and Y's, on both axes. |
| 85 entries reached | The rest is absent, with nothing marking it (native LOCKS budget). |
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));LockEntry One 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.
| Field | Type | Meaning |
|---|---|---|
scope | LockScope | A specific axis or usage, or a whole-class blanket. |
direction | Direction | Which direction of it this entry weighs. |
scale | i16 | Percent 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.
| Field | Type | Meaning |
|---|---|---|
window | Option<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. |
mode | BearingMode | PerAxis: 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.
CatchFilter What to catch, and how much
struct CatchFilter { /* private */ }One entry in the table passed to catch_events or input_events, built with a constructor.
| Constructor | Addresses |
|---|---|
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. |
| Method | Returns | Meaning |
|---|---|---|
.on_press() / .on_release() | CatchFilter | One edge, on the momentary classes. |
.inbound() / .outbound() | CatchFilter | One flow, on the traffic classes: IN is device to PC. |
.with_direction(dir) | CatchFilter | The Direction directly. Defaults to Both. |
.with_capture(cap) | CatchFilter | A 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 / Capture | What the filter was narrowed to. |
.same_address(other) | bool | Whether 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).
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);MotionEvent One 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.
| Field | Type | Meaning |
|---|---|---|
ts_us | u32 | When the native report arrived, in box microseconds. Catch timestamps explains the clock. |
clock | ClockDomain | Which 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. |
dx | i16 | X motion this report (right positive). |
dy | i16 | Y motion this report (down positive). |
dz | i16 | Wheel motion this report (up positive). |
pan | i16 | AC Pan (horizontal scroll) this report (right positive). |
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);
}UsageSnapshot One 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.
| Field | Type | Meaning |
|---|---|---|
ts_us | u32 | When the native report arrived, in box microseconds. Catch timestamps explains the clock. |
clock | ClockDomain | Which chip's timer ts_us came from. Always HostChip: a held-usage snapshot is taken where the real device's report lands. |
usages | Vec<Usage> | The held usages, one class per event. |
| Method | Returns | Meaning |
|---|---|---|
is_held(usage) | bool | Whether usage is held. Takes any impl Into<Usage>. |
class() | Option<Class> | The class of this snapshot, from its first usage, or None when empty. |
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");
}
}InputEvent Decoded 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.
| Field | Type | Meaning |
|---|---|---|
ts_us | u32 | The report's arrival stamp, in the stamping chip's microseconds. |
clock | ClockDomain | Always HostChip for native input. |
input | Input | The decoded edge or motion. |
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);
}
}TrafficEvent Bytes off one pipe, with what was cut
The payload of a CatchEvent::Traffic: one packet, one completed control transaction, or one bus event.
| Field | Type | Meaning |
|---|---|---|
ts_us | u32 | When the transfer completed, in the microseconds of the chip named by clock. |
clock | ClockDomain | Which chip stamped it. Varies by class and direction here, unlike the two input events. |
class | CatchClass | Which address space the event came from. |
id | u16 | The endpoint or interface number within that class. |
direction | Direction | Positive = IN (device to PC), Negative = OUT (PC to device). |
flags | u8 | Class-specific (see below). 0 for the classes that define none. |
true_len | u16 | The packet's length before the Capture cut it. |
bytes | Vec<u8> | What was captured, up to the entry's capture length. |
| Method | Returns | Meaning |
|---|---|---|
truncated() | bool | Whether the capture or the frame ceiling cut this packet: bytes.len() < true_len. |
rule_acted() | bool | Whether 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() | bool | Whether 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() | bool | Whether 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.
| Class | flags |
|---|---|
HidIn, Emit | b1 = 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, VendorInterrupt | b1 = a zero-length packet; b7 = a rule acted. |
VendorBulk | b0 = end of transfer, b1 = zero-length packet, b7 = a rule acted. |
Control | b0-b1 = the handshake the game PC received, read with control_status() (see ControlStatus). b7 = a rule acted. |
ClipTransfer | How the clip's transfer ended, read with transfer_status() (see TransferStatus). |
Bus | The BusEvent kind. |
| everything else | 0. |
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 data | Holds |
|---|---|
| IN | The reply the game PC received: after a ReplyPatch or ReplyReplace rule, or an Answer rule's payload. |
| OUT | The data stage the game PC sent, before a Patch or Replace rule rewrote it for the real device. |
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.
| Constant | id | Meaning |
|---|---|---|
Button::LEFT | 0 | Left button. |
Button::RIGHT | 1 | Right button. |
Button::MIDDLE | 2 | Middle button. |
Button::SIDE1 | 3 | First thumb button. |
Button::SIDE2 | 4 | Second thumb button. |
| Method | Returns | Meaning |
|---|---|---|
Button::new(id) | Button | Any 0-based id, named or not: Button::new(5) is the sixth button. |
Button::from_id(id) | Button | The same from a wire id byte. Total: every byte is a valid button. |
.as_id() | u8 | The wire id byte. |
.0 | u8 | The 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.
| Field | Type | Meaning |
|---|---|---|
class | Class | The input class (button, key, or media). |
id | u16 | The class-specific id: a button id, a HID keycode, or a Consumer usage. |
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.
| Item | Returns | Meaning |
|---|---|---|
Key::A .. Key::LEFT_SHIFT | Key | Named consts for common keycodes and modifiers. |
new(u8) | Key | Wrap any raw HID keycode. |
usage() | u8 | The raw keycode byte. |
use medius::Key;
let a = Key::A; // 0x04
let custom = Key::new(0x04);
assert_eq!(a.usage(), custom.usage());MediaKey 16-bit Consumer usage
A newtype over a 16-bit Consumer usage. It converts Into<Usage>, so inject and press take one directly.
| Item | Returns | Meaning |
|---|---|---|
MediaKey::VOLUME_UP .. MediaKey::MUTE | MediaKey | Named consts for common media usages. |
new(u16) | MediaKey | Wrap any raw Consumer usage. |
usage() | u16 | The raw Consumer usage. |
use medius::MediaKey;
let vol_up = MediaKey::VOLUME_UP; // 0x00E9
let custom = MediaKey::new(0xE9);
assert_eq!(vol_up.usage(), custom.usage());KbdCaps What the cloned keyboard can do
Semantic capabilities from caps(). Every field is zero when no keyboard is bound. has_consumer gates media injection.
| Field | Type | Meaning |
|---|---|---|
n_keys | u8 | Keycode-array slots, or 0xFF for an NKRO bitmap. |
nkro | bool | The keyboard reports an NKRO bitmap. |
has_consumer | bool | A Consumer collection is present (media keys injectable). |
has_system | bool | A system-control collection is present (relayed only). |
has_report_id | bool | The keyboard report sits behind a HID report ID. |
CatchState Live 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.
| Field | Type | Meaning |
|---|---|---|
table_full | bool | At least one entry was refused because the 32-slot table was full. |
dropped | u32 | Box-wide events shed under back-pressure, across every entry. |
clock | ClockEstimate | How the host chip's and device chip's timers relate. |
entries | Vec<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.
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);CatchEntry One 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.
| Field | Type | Meaning |
|---|---|---|
filter | CatchFilter | The subscription as the box holds it. Read it with class(), id(), direction(), capture(). |
dropped | u16 | Events 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.
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);
}ClockEstimate How 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.
| Field | Type | Meaning |
|---|---|---|
offset_us | i32 | The 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_ppb | Option<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_us | u16 | The best round trip measured in the window. The offset is accurate to ~half of it. |
age | Option<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.
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.
| Method | Returns | Meaning |
|---|---|---|
observe(&event) | Stamped | Place an event on this machine's clock, taking the arrival as now. |
observe_at(&event, now) | Stamped | The same with the arrival supplied, for replaying a capture. |
observe_stamp(ts_us, domain, now) | Stamped | The same from a stamp and domain held apart from an event. |
box_us(&event) | u64 | The 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) | u64 | Events 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.
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 }| Field | Type | Meaning |
|---|---|---|
host | Instant | When the event happened, on this machine's monotonic clock. |
box_us | u64 | The event's stamp, unwrapped past the 32-bit rollover. |
excess | Duration | How 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.
| Method | Returns | Meaning |
|---|---|---|
Transform::new(op, source, dest) | Transform | Any TransformOp, from an explicit source and destination. |
Transform::swap(a, b) | Transform | A Swap of two axes. |
Transform::remap(source, dest) | Transform | A Remap. Any Axis, Button, Key, MediaKey or Usage converts into the LockTarget it takes. |
.key() | TransformKey | The (source, dest) this entry is filed under. |
| Field | Type | Meaning |
|---|---|---|
op | TransformOp | What the transform does. An op that can't address this source and destination pair returns Error::TransformOpFields. |
source | LockTarget | The 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. |
dest | LockTarget | The 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.
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.
| Field | Type | Meaning |
|---|---|---|
table_full | bool | A further entry was, or would be, refused: the table holds Transforms::CAPACITY (32). |
entries | Vec<Transform> | One per installed transform, in installation order (the order the box applies them). The order of two entries writing the same field matters. |
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);
}ImperfectStatus Imperfect-clone state
The imperfect-clone state from query_imperfect().
| Field | Type | Meaning |
|---|---|---|
allowed | bool | The opt-in toggle: a real device the box can't clone faithfully is cloned anyway. |
over_capacity | bool | The 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_imperfect | bool | The live clone is imperfect: an opted-in real device the box can't clone faithfully, a forced rate, or an applied patch set. |
EmitPaceStatus Emit-rate pacing and clone rate
The emit-rate pacing state from query_emit_pace().
| Field | Type | Meaning |
|---|---|---|
mode | EmitPace | The pace. Fixed carries the requested rate. |
resolved_hz | u16 | The 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_hz | Option<u16> | The forced wire rate requested. None keeps the native bInterval. |
advertised_hz | u16 | What the clone's input endpoints advertise now, forced or native. 0 = no clone. |
force_active | bool | Whether a forced interval is written into the descriptor being served. |
RenderStatus Motion texture, and whether a profile armed
The render state from query_render().
| Field | Type | Meaning |
|---|---|---|
mode | RenderMode | Rendering texture. Off is the paced fill. |
full | bool | Whether native motion is rendered instead of relayed. |
ready | bool | Whether a profile has armed for the attached real device. Nothing is rendered until it has. |
SpreadStatus Spread percent and interval in effect
The spread state from query_spread().
| Field | Type | Meaning |
|---|---|---|
percent | u16 | Share of the command interval a delta is released across. 0 is the whole delta on the next report. |
span_us | u32 | The interval in effect, in microseconds. 0 whenever nothing is being spread. |
LogLine One line from the LOG stream
One line from the box's LOG stream, read off a LogStream.
| Field | Type | Meaning |
|---|---|---|
level | LogLevel | Severity tag. |
text | String | The decoded message. |
PortInfo Discovered 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.
| Field | Type | Meaning |
|---|---|---|
path | String | Serial port path. |
vid | u16 | USB vendor id (0x1A86). |
pid | u16 | USB product id (0x55D3). |
serial | Option<String> | The CH343 adapter's serial string, when it serves one. |
BoxInfo One discovered box
One entry from Device::list(), and the value find_where's predicate receives.
| Field | Type | Meaning |
|---|---|---|
port | PortInfo | The control port (path and CH343 serial). |
version | Version | The firmware version and control protocol, with the box MAC and name. |
device | Option<DeviceInfo> | The real device it clones. None for a box whose version.proto_ver isn't PROTO_VER. Opening that box returns Error::BadProtoVer. |
| Method | Returns | Meaning |
|---|---|---|
id() | String | The box identity: the MAC hex, as passed to open_by_id. |
name() | &str | The 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. |
CountersSnapshot Link statistics snapshot
Five running link totals from counters().
| Field | Type | Meaning |
|---|---|---|
frames_tx | u64 | Frames sent to the box. |
frames_rx | u64 | Frames received from the box. |
crc_drops | u64 | Inbound frames dropped on a bad checksum. |
reconnects | u64 | Times the library reopened the port. |
restarts | u64 | Device-chip restarts the library recovered from by re-sending the state it holds. A session release, counted in Stats::session, leaves it alone. |
LogStream Receiver 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.
ClipSettings Persistent 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).
| Field | Type | Meaning |
|---|---|---|
autolock | Vec<Blanket> | The Blanket groups auto-locked while playing (clip-owned, released on stop). Empty = no auto-lock. |
loop_ | bool | Playback restarts from the top instead of stopping at the end. |
retain | bool | The buffered content survives a stop, so a restart replays it instead of needing a fresh append. |
finalized | bool | The clip is sealed: no more appends, ready to replay as a fixed sequence. |
ride | bool | The clip's motion waits for native motion under movement riding. false (the default) plays it on the box's clock. |
triggers | Vec<ClipTrigger> | The bound input triggers (≤8), each firing a playback action on a native edge. |
packet_triggers | Vec<ClipPacketTriggerEntry> | The bound packet triggers (≤8), in the order the box holds them, each with its hit count. |
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);
}ClipTrigger One 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
| Field | Type | Meaning |
|---|---|---|
on | Usage | The button, key, or media usage that fires the trigger. |
edge | Edge | Which edge fires it: Press, Release, or Both. |
action | ClipAction | The playback action to run (Start, Stop, Toggle, ...). |
consume | bool | Lock the trigger usage while it is active, so its edge never reaches the PC. The .consume() builder sets it true. |
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)?;ClipPacketTrigger One 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).
| Field | Type | Meaning |
|---|---|---|
class | TrafficClass | The surface the packet crosses: HidIn, HidOut, VendorInterrupt, VendorBulk, Control or Emit. |
id | u16 | The interface number for HidIn, the endpoint number for the rest, or ClipPacketTrigger::ANY_ID (0xFFFF) for every id of the class. |
direction | Direction | The flow the packet travels in: IN, OUT or Both. With and Against return Error::RelativeDirection. |
action | ClipAction | The playback action to run, on the frame clock's next tick. |
match_bytes, mask | Vec<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. |
consume | bool | Drop 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_run | bool | Run 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_len | u8 | With 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. |
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)?;ClipPacketTriggerEntry One packet trigger the box holds, read back
struct ClipPacketTriggerEntry { trigger: ClipPacketTrigger, hits: u16 }One row of ClipSettings::packet_triggers.
| Field | Type | Meaning |
|---|---|---|
trigger | ClipPacketTrigger | The trigger, in the shape bind_packet takes, so a read entry replays as a bind. |
hits | u16 | Packets 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. |
ClipStatus Clip 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).
| Field | Type | Meaning |
|---|---|---|
state | ClipState | The lifecycle state (idle / playing / paused / faulted). |
free | u32 | Free bytes in the ring, the headroom for the next append. |
total | u32 | The retained clip size in bytes. While streaming, the bytes buffered but not yet drained. |
played | u32 | Bytes played from the clip start (retained progress; ~0 while streaming). |
ticks | u32 | Content frames played since the box booted (gap runs are not counted). |
underruns | u16 | Underrun episodes (the ring ran dry mid-playback). |
overruns | u16 | Appends dropped because the ring was full. |
seq_gaps | u16 | Append-sequence gaps seen (a dropped append frame). |
xfers | u16 | Clip transfers the real device completed. |
xfer_errs | u16 | Clip 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. |
gated | u16 | Raw reports and transfers the box discarded because the imperfect-clone opt-in was off. |
held | Vec<Usage> | The usages the clip is holding down: buttons, keys, and media in one list, like a UsageSnapshot. Test one with is_held(usage). |
ChipFirmware What one chip is running
One chip's half of firmware_info(). Display renders it as major.minor.patch.
| Field | Type | Meaning |
|---|---|---|
major, minor, patch | u8 | The firmware version this chip is running. |
slot | u8 | Which app slot it booted: 0 or 1. |
state | ImageState | Whether that image is confirmed, on probation, or rolled back. |
FirmwareInfo Both 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.
| Field | Type | Meaning |
|---|---|---|
device | ChipFirmware | The PC-facing chip. |
host | Option<ChipFirmware> | None when the host chip has not replied over the inter-chip link. |
slot_size | u32 | Usable bytes in a spare slot. The same on both chips. |
device_staged | bool | An image is written and waiting to be activated. |
host_staged | bool | The same, for the host chip. |
UpdateProgress One acknowledged window
Handed to the closure passed to stage_firmware(), once per acknowledged window rather than once per chunk.
| Field | Type | Meaning |
|---|---|---|
target | UpdateTarget | The chip being written. |
sent | usize | Bytes the box has acknowledged. |
total | usize | Bytes in the whole image. |
| Method | Returns | Meaning |
|---|---|---|
percent() | u8 | sent 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.
| Field | Type | Meaning |
|---|---|---|
request_type | u8 | bmRequestType: direction in bit 7 (set is device-to-host / IN), then type and recipient. |
request | u8 | bRequest: the request code. |
value | u16 | wValue: request-specific. |
index | u16 | wIndex: request-specific, often an interface or endpoint. |
length | u16 | wLength: 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).
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]);TransferOutcome Native reply: status and any IN data
struct TransferOutcome { status: TransferStatus, data: Vec<u8> }What transfer returns. A non-Ok status carries no data.
| Field | Type | Meaning |
|---|---|---|
status | TransferStatus | How the transfer ended. |
data | Vec<u8> | The IN data the real device returned. Empty for an OUT transfer, a STALL, or no data. |
| Method | Returns | Meaning |
|---|---|---|
.is_ok() | bool | Whether the transfer completed. |
.data() | &[u8] | The returned data. |
.ok_data() | Option<Vec<u8>> | The data if the transfer completed, else None. |
RewriteRule Rule 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.
| Field | Type | Meaning |
|---|---|---|
class | RewriteClass | The traffic class the rule addresses. |
id | u16 | The address within the class: an interface number or an endpoint number. |
direction | Direction | The 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. |
action | RewriteAction | What the rule does to a matched packet. An action that doesn't fit the class returns Error::RewriteActionClass. |
offset | u16 | Where a patching action writes. Other actions ignore it. |
match_bytes, mask | Vec<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. |
payload | Vec<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. |
RewriteTable Rewrite 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.
| Field | Type | Meaning |
|---|---|---|
table_full | bool | The 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. |
generation | u8 | Goes 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). |
entries | Vec<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.
RewriteEntry One 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.
| Field | Type | Meaning |
|---|---|---|
class, id, direction, action | as set | The rule's address and action. |
match_len | u8 | How many match and mask bytes the rule compares. |
offset | u16 | The write offset for a patching action. |
payload_len | u16 | How many payload bytes the rule carries. |
hits | u16 | Packets 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.
| Field | Type | Meaning |
|---|---|---|
section | PatchSection | The descriptor this patch targets. |
cfg | u8 | The configuration index, for Config / Report. 0 is the first configuration, not bConfigurationValue. |
index | u8 | The interface or string index, for Report / String. |
offset | u16 | Where in the descriptor the overwrite starts. Ignored for String, which is replaced whole. |
bytes | Vec<u8> | The overwrite bytes. Empty removes the patch at this key. |
PatchSet Stored 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.
| Field | Set when |
|---|---|
applied | The clone serves a non-empty patched set: the one it was presented with. A later store doesn't change it until the next presentation. |
pending | The 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. |
refused | The 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_full | The 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)PatchEntry One 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.
| Field | Type | Meaning |
|---|---|---|
section, cfg, index, offset | as set | The patch's key. |
len | u16 | How many bytes the patch overwrites. |