Catch
Observe what passes through the box, addressed like a lockinput_events gives you press and release edges; catch_events gives you the raw frames underneath. Input is observed before any lock suppression or injection. Drop the stream to unsubscribe.
A subscription is a table of CatchFilter entries. Addressing doubles as the filter: the control link runs at 4 Mbaud and vendor bulk alone measures ~250 KiB/s, so a subscription has to be able to name one endpoint.
input_events
Subscribe to decoded input edgesfn input_events(&self, filters: impl IntoIterator<Item = CatchFilter>) -> Result<InputStream>
Fire-and-forget
The box sends held-usage snapshots; this diffs them into edges, so watching a key is a match rather than a set difference.
use medius::{Device, CatchFilter, Input, Key};
let device = Device::find()?;
for ev in device.input_events([CatchFilter::watch(Key::F)])? {
match ev.input {
Input::Press(u) => println!("down {u:?}"),
Input::Release(u) => println!("up {u:?}"),
Input::Motion { dx, dy, dz } => println!("moved {dx},{dy},{dz}"),
}
}Every filter must name an input class and cover both edges; anything else is refused. Without the release edge a fresh press cannot be told from a chord, so match on Input::Press instead.
catch_events
Subscribe to the raw event streamfn catch_events(&self, filters: impl IntoIterator<Item = CatchFilter>) -> Result<EventStream>
Fire-and-forget
Each CatchFilter becomes one entry in the box's 32-entry table, sent as its own frame. The returned EventStream receives every event any of them matches.
| Parameter | Type | Description |
|---|---|---|
filters | anything iterable of CatchFilter | The subscription table: one filter, an array, or a Vec. |
use medius::{Capture, CatchEvent, CatchFilter, Device, TrafficClass};
let device = Device::find()?;
let events = device.catch_events([
CatchFilter::everything().with_capture(Capture::First(16)),
CatchFilter::traffic(TrafficClass::VendorInterrupt, 0x83),
])?;
while let Ok(CatchEvent::Traffic(t)) = events.recv() {
println!("{:?} {:#06x}: {} of {} bytes", t.class, t.id, t.bytes.len(), t.true_len);
}
// dropping `events` unsubscribesThe keepalive re-asserts the table, and it survives a reconnect. It clears on control-PC silence, on reset (which ends the stream), or on link loss. See the native CATCH command for the wire layout.
CatchFilter
One table entry: an address, a direction, a captureThe input constructors take what lock takes, so hiding an input from the game and watching it are written alike. Every constructor, modifier and accessor is on CatchFilter.
The box resolves to the most specific match: an exact (class, id) before a class blanket, a class blanket before everything(), and a named direction before Both. That entry supplies the capture.
CatchFilter::everything().with_capture(Capture::First(16))
CatchFilter::traffic_class(TrafficClass::VendorInterrupt).with_capture(Capture::First(32))
CatchFilter::traffic(TrafficClass::VendorInterrupt, 0x83)
a 64-byte report on vendor interrupt endpoint 0x83
+- exact (class, id)? HIT --> whole packet --> all 64 bytes
the same report on endpoint 0x81
+- exact (class, id)? miss
+- class blanket? HIT --> First(32) --> 32 bytes, true_len 64InputStream
Receive decoded edgesThe handle input_events returns. It holds the per-class held sets it diffs, so it takes &mut self and one report can yield several Input values. It is an Iterator, so a for loop over it works.
It reports only the usages this subscription addressed. The box holds one table, the union of every subscription in the process, so its snapshots widen as soon as unrelated code subscribes. The decoder filters them back down.
| Method | Returns | Description |
|---|---|---|
recv() | Result<InputEvent> | Block until the next edge. |
try_recv() | Option<InputEvent> | The next decoded edge, or None (never blocks). |
recv_timeout(dur) | Option<InputEvent> | Block up to dur; None on timeout. |
recv_async().await | Result<InputEvent> | Await the next edge (async feature). |
is_connected() | bool | Whether the box is still delivering; try_recv and recv_timeout return None for both "nothing yet" and "nothing ever again". |
held(class) | &[Usage] | What this stream currently has held for one class. |
dropped() | u64 | Events lost host-side because this consumer fell behind. |
EventStream
Receive raw eventsThe handle catch_events returns. Cloning shares the queue. When the stream and all its clones drop, the subscription ends.
| Method | Returns | Description |
|---|---|---|
recv() | Result<CatchEvent> | Block until the next event. |
try_recv() | Option<CatchEvent> | The next buffered event, or None (never blocks). |
recv_timeout(dur) | Option<CatchEvent> | Block up to dur; None on timeout. |
iter() / try_iter() | impl Iterator | Blocking, or drain what is buffered. The stream is itself an Iterator. |
recv_async().await | Result<CatchEvent> | Await the next event (async feature), runtime-agnostic. |
stream() | impl Stream | The same queue as a futures stream (async feature). |
is_connected() | bool | Whether the box is still delivering, which a None from the two above cannot tell you. |
dropped() | u64 | Events lost host-side because this consumer fell behind. |
| Variant | Carries | Raised by |
|---|---|---|
Motion(MotionEvent) | the relative axes of one physical report, as a MotionEvent | an Axis filter |
Usages(UsageSnapshot) | the held usages of one class as a UsageSnapshot, a full snapshot rather than edges | a Button, Key, or Media filter |
Traffic(TrafficEvent) | bytes plus the address they came from, as a TrafficEvent | every other class |
class(), id(), direction(), ts_us(), clock() and bytes() read the same fields on any variant.
The buffer is bounded and lossy: a slow consumer drops the oldest events. The box's own drop counts are on query_catch, box-wide and per entry.
The box drains through strict-priority queues. Vendor bulk can go entirely undrained under a busy mouse: bulk-plus-input is what the control link cannot carry.
Button Key Media Axis Bus --> [ queue 0 ] --+ HidIn HidOut | VendorInterrupt Emit --> [ queue 1 ] --+---> control link, 4 Mbaud Control --> [ queue 2 ] --+ VendorBulk --> [ queue 3 ] --+ each queue drains fully before the next
Reading traffic
Truncation, control transactions, bus eventsA TrafficEvent carries the address, the bytes, and true_len: the length before the capture cut it. A trimmed packet and a genuinely short one are otherwise identical, so check truncated().
use medius::{Capture, CatchEvent, CatchFilter, TrafficClass};
let filter = CatchFilter::traffic_class(TrafficClass::VendorInterrupt)
.with_capture(Capture::First(16));
for event in &device.catch_events([filter])? {
if let CatchEvent::Traffic(t) = event {
if t.truncated() {
println!("ep {:#06x}: {} of {} bytes", t.id, t.bytes.len(), t.true_len);
}
}
}What flags carries per class is on TrafficEvent.
A Control event is one completed transaction, not one stage: bytes is [setup 8][data…], split by setup() and data(). A Bus event carries a BusEvent kind and its operands.
Timestamps
Two chips, two clocks, one host timelineEvery event carries ts_us and the ClockDomain that produced it. The two chips boot independently, so a stamp is only meaningful within its own domain.
| Domain | Stamped | Covers |
|---|---|---|
ClockDomain::HostChip | in USB interrupt context, the instant the real device's transfer completed | motion, usages, HidIn, vendor IN |
ClockDomain::DeviceChip | at the tap on the device chip | HidOut, every OUT direction, Control, Emit, Bus |
Stamps are u32 microseconds from that chip's boot: they wrap every ~71.6 minutes and restart at zero on reboot. Timeline handles all of it and returns an Instant. It takes an InputEvent or a raw CatchEvent alike.
use medius::{CatchFilter, Input, Key, Timeline};
let mut input = device.input_events([CatchFilter::watch(Key::F)])?;
let mut time = Timeline::new();
for ev in input.by_ref().take(20) {
let at = time.observe(&ev);
if let Input::Press(u) = ev.input {
println!("{u:?} down at {:?}, {:?} above the floor", at.host, at.excess);
}
}It keeps a per-domain minimum of (elapsed here minus elapsed on the box) rather than an average, because the error is one-sided: an event can arrive late but never early.
query_catch returns a ClockEstimate: the box's own offset between its two chips, its drift rate, and the round trip bounding the error.
On AsyncDevice
Subscribing fires, the stream awaitsAsyncDevice keeps catch_events and input_events synchronous; the streams offer recv_async().await. query_catch is a future, like the other queries.
use medius::{AsyncDevice, CatchFilter, Key};
let device = AsyncDevice::open("/dev/ttyACM0")?;
let mut input = device.input_events([CatchFilter::watch(Key::F)])?; // sync, no await
let edge = input.recv_async().await?; // stream awaits