Medius - Rust LibraryCatch

Catch

Observe what passes through the box, addressed like a lock

input_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 edges
fn 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.

EXAMPLE
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 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 its own frame. The returned EventStream receives every event any of them matches.

ParameterTypeDescription
filtersanything iterable of CatchFilterThe subscription table: one filter, an array, or a Vec.
EXAMPLE
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` unsubscribes
LIFECYCLE

The 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 capture

The 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.

MOST-SPECIFIC-FIRST

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 64

InputStream

Receive decoded edges

The 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.

MethodReturnsDescription
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().awaitResult<InputEvent>Await the next edge (async feature).
is_connected()boolWhether 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()u64Events lost host-side because this consumer fell behind.

EventStream

Receive raw events

The handle catch_events returns. Cloning shares the queue. When the stream and all its clones drop, the subscription ends.

MethodReturnsDescription
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 IteratorBlocking, or drain what is buffered. The stream is itself an Iterator.
recv_async().awaitResult<CatchEvent>Await the next event (async feature), runtime-agnostic.
stream()impl StreamThe same queue as a futures stream (async feature).
is_connected()boolWhether the box is still delivering, which a None from the two above cannot tell you.
dropped()u64Events lost host-side because this consumer fell behind.
THE THREE VARIANTS
VariantCarriesRaised by
Motion(MotionEvent)the relative axes of one physical report, as a MotionEventan Axis filter
Usages(UsageSnapshot)the held usages of one class as a UsageSnapshot, a full snapshot rather than edgesa Button, Key, or Media filter
Traffic(TrafficEvent)bytes plus the address they came from, as a TrafficEventevery 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.

DELIVERY IS RANKED

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 events

A 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().

EXAMPLE
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 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 own domain.

DomainStampedCovers
ClockDomain::HostChipin USB interrupt context, the instant the real device's transfer completedmotion, usages, HidIn, vendor IN
ClockDomain::DeviceChipat the tap on the device chipHidOut, 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.

EXAMPLE
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.

CROSSING DOMAINS ON THE BOX

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 awaits

AsyncDevice keeps catch_events and input_events synchronous; the streams offer recv_async().await. query_catch is a future, like the other queries.

EXAMPLE
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