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

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, 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_eventsSubscribe 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.

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

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

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

MethodReturnsDescription
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().awaitResult<InputEvent>Await the next edge (async feature). Takes &mut self.
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)Vec<Usage>Usages held in one class, as this stream tracked them.
dropped()u64Events 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()StreamCloserA handle that closes the stream from another thread.

EventStreamReceive raw events

Returned by catch_events. Clones share the queue. The subscription ends when the stream and every clone drop, or on close().

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. A None from try_recv or recv_timeout can't tell.
dropped()u64Events 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()StreamCloserA 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

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

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

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);
        }
    }
}

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.

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

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);
    }
}

query_catch returns a ClockEstimate: the box's offset between its two chips, the drift rate, and the round trip bounding the error.

On AsyncDeviceSubscribing fires, the stream awaits

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

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