Catch
input_events yields press and release edges. catch_events yields the raw frames underneath. Input is observed before any lock suppression or injection. Dropping the stream, or close(), unsubscribes.
A subscription is a table of CatchFilter entries, each addressed like a lock. Vendor bulk alone measures ~250 KiB/s on a 6 Mbaud control link, so a filter can narrow to one endpoint.
input_events Subscribe to decoded input edges
fn input_events(&self, filters: impl IntoIterator<Item = CatchFilter>) -> Result<InputStream>
Fire-and-forget
The box sends held-usage snapshots. input_events diffs them into edges.
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, pan } => println!("moved {dx},{dy},{dz},{pan}"),
}
}Every filter must name an input class and cover both edges. Anything else is refused. Without the release edge, a fresh press can't be told from a chord. To watch presses, match on Input::Press.
catch_events Subscribe to the raw event stream
fn 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 a separate 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. It survives a reconnect and a session recovery. The box clears it on control-PC silence, reset (which ends the stream), inter-chip link loss, the real device detaching, and a re-clone. The wire layout is on the native CATCH command.
InputStream Receive decoded edges
Returned by input_events. One report can yield several Input values, so receives run one at a time. held, dropped and is_connected never wait for a receive. It is Sync and an Iterator.
It reports only the usages this subscription addressed. The box's one table is the union of every subscription in the process, and the decoder filters the wider snapshots back down.
| Method | Returns | Description |
|---|---|---|
recv() | Result<InputEvent> | Block until the next edge. |
try_recv() | Option<InputEvent> | The next decoded edge, or None if none is queued or another thread is receiving (never blocks). |
recv_timeout(dur) | Option<InputEvent> | Block up to dur, including any wait for another thread's receive. None on timeout. |
recv_async().await | Result<InputEvent> | Await the next edge (async feature). Takes &mut self. |
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) | Vec<Usage> | Usages held in one class, as this stream tracked them. |
dropped() | u64 | Events lost host-side because this consumer fell behind. |
close() | () | End the subscription. Every blocked receive returns at once, and so does every later one. Queued events are dropped. |
closer() | StreamCloser | A handle that closes the stream from another thread. |
EventStream Receive raw events
Returned by catch_events. Clones share the queue. The subscription ends when the stream and every clone drop, or on close().
| 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. A None from try_recv or recv_timeout can't tell. |
dropped() | u64 | Events lost host-side because this consumer fell behind. |
close() | () | End the subscription for this stream and every clone. Every blocked receive returns at once, and so does every later one. Queued events are dropped. |
closer() | StreamCloser | A handle that closes the stream from another thread. |
Each event is a CatchEvent variant. class(), id(), direction(), ts_us(), clock() and bytes() read the same fields on any of them.
The buffer is bounded and lossy: a slow consumer drops the oldest events. The box's drop counts, box-wide and per entry, are on query_catch.
The box drains through strict-priority queues. Under a busy mouse, vendor bulk can go entirely undrained, since the control link can't carry bulk plus input.
Button Key Media Axis Bus --> [ queue 0 ] --+ HidIn HidOut | VendorInterrupt Emit --> [ queue 1 ] --+---> control link, 6 Mbaud Control ClipTransfer --> [ queue 2 ] --+ VendorBulk --> [ queue 3 ] --+ each queue drains fully before the next
StreamCloser Close a stream from another thread
fn closer(&self) -> StreamCloser
fn close(&self)
No round-trip
Get one from InputStream, EventStream or LogStream, for a thread that holds none of the stream's handles. Its close() works like the stream's close(). It holds the stream weakly, so it keeps no subscription alive and does nothing once every handle has dropped. It is Clone.
use medius::{CatchFilter, Device};
let device = Device::find()?;
let input = device.input_events(CatchFilter::all_input())?;
let closer = input.closer();
let reader = std::thread::spawn(move || {
for ev in input {
println!("{ev:?}");
}
});
closer.close(); // the loop ends at once
reader.join().unwrap();TrafficTruncation, control transactions, bus events
A TrafficEvent carries the address, the bytes, and true_len (the length before capture cut it). Only truncated() tells a trimmed packet from a short one.
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);
}
}
}TrafficEvent lists what flags carries per class. control_status(), rule_acted(), transfer_status(), zlp(), bulk_end_of_transfer() and bus_event() read it.
A Control or ClipTransfer event is one completed transaction: bytes is [setup 8][data...], split by setup() and data(). A Control event is the transaction the game PC received, on every control endpoint.
TimestampsTwo chips, two clocks, one host timeline
Every event carries ts_us and the ClockDomain that produced it. The two chips boot independently, so a stamp is only meaningful within its domain.
| Domain | Stamped | Covers |
|---|---|---|
ClockDomain::HostChip | in USB interrupt context, the instant the real device's transfer completed | motion, usages, HidIn, native vendor IN |
ClockDomain::DeviceChip | at the tap on the device chip | HidOut, every OUT direction, a raw vendor IN packet, Control, Emit, Bus, ClipTransfer |
Stamps are u32 microseconds from that chip's boot, so they wrap and restart at zero on reboot. Timeline handles domains, wrap and reboot, and turns an InputEvent or a raw CatchEvent into an Instant.
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);
}
}query_catch returns a ClockEstimate: the box's offset between its two chips, the drift rate, and the round trip bounding the error.
On AsyncDevice Subscribing fires, the stream awaits
AsyncDevice keeps catch_events and input_events synchronous, and the streams offer recv_async().await. query_catch is a future.
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