Structs
Values the box reports backPlain value types you get back from queries and discovery. Their fields are public.
Version
Firmware identity and box idFirmware identity from query_version(). Display prints fw M.m.p and omits proto_ver; read it from the field.
| Field | Type | Meaning |
|---|---|---|
proto_ver | u8 | Wire-protocol version the firmware speaks (5 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 synthesized 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: 5, fw_major: 3, fw_minor: 2, fw_patch: 0, mac: [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], name: "Loki".into() };
assert_eq!(v.to_string(), "fw 3.2.0"); // Display omits proto_ver
assert_eq!(v.mac_hex(), "123456789abc");
println!("{v} (protocol {}, box {}, name {})", v.proto_ver, v.mac_hex(), v.name);Health
Box readiness flagsBox readiness from query_health(), one bool per bit. from_flags(u8) and to_flags() convert the byte.
| 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 PC has set up the cloned mouse. |
injection_active | bool | The box is holding at least one injected button or move. |
rate_confident | bool | 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. |
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);
assert_eq!(h.to_flags(), 0b0000_0011); // round-trips to the same byteDeviceInfo
The cloned device's USB identity, kind, and productUSB identity from device_info(). Every field is zero/empty when nothing is cloned. 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 device's primary kind, from its Boot-interface protocol. |
product | String | The product string the device serves (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 productCaps
The whole device, mouse and keyboardEverything one caps() query returns. has_mouse() / has_keyboard() tell you which are bound; is_composite() is true when the device has more than one 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 is real on this board
}
println!("{} mouse buttons", caps.mouse.n_buttons);MouseCaps
What the cloned mouse can doSemantic 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_report_id | bool | The mouse report sits behind a HID report ID. |
n_hid | u8 | Cloned HID interfaces; >1 = composite. |
use medius::MouseCaps;
let c = MouseCaps { n_buttons: 5, has_x: true, has_y: true, has_wheel: true, has_report_id: false, n_hid: 1 };
assert!(!c.is_composite()); // single HID interfaceRate
The native report rate the box tracksLive rate from query_rate(). native_hz() converts the period to a frequency, returning None while native_period_us is still 0. On a change-driven input, poll_period_us is the only rate there is.
| 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 and the value is trustworthy. |
change_driven | bool | The active input is event-driven (keyboard / media), so there is 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));Stats
Delivery and telemetry countersDelivery counters from query_stats(). A nonzero tx_drops or tx_wedges means delivery degraded under load. The narrowed fields saturate instead of wrapping.
| Field | Type | Meaning |
|---|---|---|
inject_emits | u32 | Pure-injection reports emitted. |
tx_drops | u16 | Reports dropped on TX-queue overflow (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 issued. |
reset_count | u16 | USB bus resets seen. |
config_count | u16 | SET_CONFIGURATION events (re-enumerations). |
Locks
The active input scalesThe active set from query_locks(), a list of LockEntry across every class, one per direction that is not passing untouched. See the native LOCKS reply for the wire format.
| Method | Returns | Meaning |
|---|---|---|
entries() | &[LockEntry] | Every weighed direction, one entry each, across specific targets and whole-class blankets. |
scale_of(target, dir) | u8 | Percent of the physical value kept there; 100 when nothing weighs it, and where entries overlap it reports the lowest. Both reports the lowest across every direction, which is not the figure a delta meets: it picks up one 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 | What the list holds |
|---|---|
| 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. |
| 96 entries reached | The rest is absent, with nothing marking it. See the native LOCKS budget. |
use medius::{Axis, Button, Direction};
let locks = device.query_locks()?;
if locks.is_locked(Axis::X, Direction::Positive) {
// physical +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 liststruct LockEntry { scope: LockScope, direction: Direction, scale: u8 }One weighed direction in a Locks list. Entries mirror the LOCK frame field for field, so what comes back is what you would send to reproduce 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 | u8 | Percent of the physical value kept. A momentary usage carries one bit, so the box stores the block or pass it renders and this never reads between them. |
is_block() is scale == 0: blocked outright rather than weighed.
Bearing
What With and Against are measured againststruct Bearing { window: Option<Duration>, mode: BearingMode }The configured bearing from query_bearing(). See the native bearing for what it does.
| Field | Type | Meaning |
|---|---|---|
window | Option<Duration> | How long an axis holds the direction of its last injected delta. None is off, so With and Against are inert whatever their scale. |
mode | BearingMode | PerAxis: each axis reads its own sign. Vector: the physical delta is projected onto the injected direction, and the relative scale weighs only the part along it. |
is_live() is whether a bearing is held at all.
CatchFilter
One subscription entry: what to catch, and how much of itstruct CatchFilter { /* private */ }One entry in the table you hand to catch_events or input_events. Built with a constructor rather than by hand.
| 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 endpoint, an interface, an 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. |
An exact (class, id) is matched before a class blanket, that before the wildcard, and a named direction before Both. That 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 is not part of a filter's address, so two filters naming one entry at different lengths are one box entry at the wider of the two. same_address is that comparison; == compares everything.
The table holds 32 entries, and a subscription that would exceed it is refused before anything is sent. See Error. So is an empty subscription, and a capture on an input class.
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 physical relative-axis eventThe payload of a CatchEvent::Motion, read off an EventStream. The real hand motion at the merge point, before lock suppression or injection, so a locked or injected axis still reports the true delta.
| Field | Type | Meaning |
|---|---|---|
ts_us | u32 | When the device's report arrived, in box microseconds. See Catch timestamps for what the clock means. |
clock | ClockDomain | Which chip's timer ts_us came from. Always HostChip here: physical motion is stamped on the host chip as the real device's transfer completes. |
dx | i16 | X movement this report (right positive). |
dy | i16 | Y movement this report (down positive). |
dz | i16 | Wheel movement this report (up 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 {}", m.ts_us, m.clock, m.dx, m.dy, m.dz);
}UsageSnapshot
One physical held-usage snapshotThe 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 device's report arrived, in box microseconds. See Catch timestamps for what the clock means. |
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 currently-held usages, all of 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
One decoded input edge, and when it happenedstruct 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 physical input. |
input | Input | What happened. |
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 cutThe 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 address, endpoint number, or interface number inside 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 actually captured, at most the entry's capture length. |
| Method | Returns | Meaning |
|---|---|---|
truncated() | bool | Whether the capture or the frame ceiling cut this packet: bytes.len() < true_len. |
One event frame carries at most 180 bytes, so Capture::Whole still truncates a longer packet, and still says so.
| Class | flags |
|---|---|
VendorBulk | b0 = end of transfer, b1 = zero-length packet. |
Control | How the proxied transfer ended: 0 completed, 0xFD the device STALLed, 0xFE it NAKed until the transfer timed out. |
Bus | The BusEvent kind. |
| everything else | 0. |
bytes is the 8-byte SETUP packet then the data stage, and direction says which way that data went. Requests answered from the box's descriptor cache still raise events.
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);
}
}Key
A HID keyboard keycodeA newtype over a HID keyboard/keypad usage. It converts Into<Usage>, so you pass one straight to inject or press. 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
A 16-bit Consumer usageA newtype over a 16-bit Consumer usage. It converts Into<Usage>, so you pass one straight to inject or press.
| 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 doSemantic 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 (passthrough-only). |
has_report_id | bool | The keyboard report sits behind a HID report ID. |
CatchState
The live subscription table, read backstruct CatchState { table_full: bool, dropped: u32, clock: ClockEstimate, entries: Vec<CatchEntry> }What query_catch() returns. Subscribing has no reply of its own, 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 | The measured relationship between the host chip's and device chip's timers. |
entries | Vec<CatchEntry> | The live subscription table, one entry per accepted CatchFilter. Empty = catching nothing. |
An entry absent from entries was refused; table_full says 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 loststruct 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 says you are losing events; this one says which. Vendor bulk drops first, by design.
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 relatestruct 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 up to 20 µs per second.
| 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. Not the same as a fitted 0: on a link too busy for enough clean exchanges no fit is made at all, which is when assuming no drift costs the most. |
delay_us | u16 | The best round trip measured in the window; the offset is good to about half of it. |
age | Option<Duration> | How long ago the exchange ran. None = no estimate yet. |
error_bound_us() halves delay_us for you; 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());
}
}Timeline
Box stamps on this machine's clockstruct Timeline { /* private */ }A catch stamp is microseconds on a chip that booted before this process did: it wraps every ~71.6 minutes, restarts at zero on reboot, and has no relation to any clock here. Feed every event in as it arrives, in order.
&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 on their own. |
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. It 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);
}Stamped
One event placed on this machine's clockstruct 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 own stamp, unwrapped past the 32-bit rollover. |
excess | Duration | How much later than the measured floor this event reached you. Jitter, not latency: the constant part of the delay is unknowable from here. |
ImperfectStatus
The imperfect-clone stateThe imperfect-clone state from query_imperfect().
| Field | Type | Meaning |
|---|---|---|
allowed | bool | The opt-in toggle; cloning an over-capacity device is allowed. |
over_capacity | bool | The attached device needs an interrupt-IN endpoint the box can't service. |
clone_imperfect | bool | The live clone is over-capacity and was cloned anyway, so one interface is dead. |
EmitPaceStatus
The emit-rate pacing state and the rate the clone runs atThe emit-rate pacing state from query_emit_pace().
| Field | Type | Meaning |
|---|---|---|
mode | EmitPace | The selected mode; Fixed carries the requested rate. |
resolved_hz | u16 | The ceiling in effect (Hz); 0 = learnt/adaptive, or no device yet in Interval. |
force_hz | Option<u16> | The forced wire rate requested; None leaves the device's own. |
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. |
PortInfo
A discovered serial portA 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. |
CountersSnapshot
Link statistics snapshotFour 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. |
LogStream
Receiver for the device LOG streamReceives the box's LOG frames as LogLine values off a local channel, from device.logs(). No receive method touches the wire, so cloning shares the queue. The methods and an example are on Logs & counters.
ClipSettings
A clip's persistent config, read backA clip's configuration from ClipHandle::query_config(). You set these with the handle setters (set_autolock, set_loop, set_retain, set_ride, finalize, bind); this is the readback.
| 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 a real move under movement riding; false (the default) plays it on the box's own clock. |
triggers | Vec<ClipTrigger> | The bound input triggers (up to 8), each firing a playback action on a physical edge. |
let cfg = handle.query_config()?;
if cfg.loop_ && cfg.finalized {
println!("sealed looping clip, {} triggers", cfg.triggers.len());
}ClipTrigger
One input binding that drives a clipOne physical-input binding for a clip, handed to ClipHandle::bind. The box keeps up to 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
No round-trip
| 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)?;ClipStatus
The buffered-clip ring and playback stateThe clip ring depth and playback counters from ClipHandle::query_status(). Pace top-ups off free; a ClipState::Faulted state means re-sync (stop, 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 buffered-but-undrained bytes. |
played | u32 | Bytes played from the clip start (retained progress; ~0 while streaming). |
ticks | u32 | Content frames drained since the last start (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). |
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 runningOne 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 stagedReturned 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 answered 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 windowHanded 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. |