Streams
Consume live input and device logsThe box has two live channels: physical input it forwards to the PC (Catch) and its own log lines (Logs & counters). Subscribe to each with a method on an open Device, then pull items off the returned stream. What an event means lives on those pages; this page covers reading them in Python.
physical mouse / keyboard
│ (also forwarded to the game PC)
▼
┌─────────────────┐ ┌─────────────┐
│ medius box │ catch_events(mask) ─▶│ EventStream │ ─▶ recv() ─▶ CatchEvent
│ │ ├─────────────┤
│ │ logs() ─▶│ LogStream │ ─▶ recv() ─▶ LogLine
└─────────────────┘ └─────────────┘Both streams are context managers and iterable. Use with so the subscription is released on exit, and for item in stream: to drain it until the link drops.
Subscribe
Open a stream from a DeviceBoth calls live on the Device. They send a subscribe request to the box and hand back a stream object.
| Call | Returns | Channel |
|---|---|---|
dev.catch_events(mask=CatchMask.ALL) | EventStream | physical mouse / key / media events (see Catch) |
dev.logs() | LogStream | device log lines (see Logs & counters) |
mask is a CatchMask, an IntFlag, so OR the categories you want (CatchMask.MOTION | CatchMask.BUTTONS). It defaults to ALL.
| CatchMask member | Value | Selects |
|---|---|---|
CatchMask.MOTION | 1 | cursor movement (dx / dy) |
CatchMask.WHEEL | 2 | wheel ticks |
CatchMask.BUTTONS | 4 | mouse buttons |
CatchMask.KEYS | 8 | keyboard keys |
CatchMask.MEDIA | 16 | media keys |
CatchMask.ALL | 31 | every category (the default) |
Exactly what each bit selects is on Catch.
Receive
Block, poll, time out, or iterateFour read methods (recv, try_recv, recv_timeout, iterate) are on both streams, plus clone() and close() for lifecycle. The table shows EventStream (yielding CatchEvent); LogStream is identical with LogLine in place of CatchEvent.
| Method | Returns | Behaviour |
|---|---|---|
recv() | CatchEvent | Blocks for the next item. Raises DisconnectedError when the link drops. |
try_recv() | Optional[CatchEvent] | Returns immediately; None if nothing is queued. |
recv_timeout(ms) | Optional[CatchEvent] | Waits up to ms milliseconds; None on timeout. |
for ev in stream: | yields each item | Loops on recv(); ends cleanly when the link drops (no exception). |
clone() | EventStream | A second handle to the same subscription; the queue is shared. |
close() / with stream: | none | Release the subscription. Automatic on with exit and GC. |
Event objects
What recv() hands backA CatchEvent carries a kind and one payload. Read the payload by kind, or use the typed accessors that return None for the wrong kind. Every object here is a dataclass.
CatchEvent ├─ kind : CatchEventKind (MOTION = 0 · USAGES = 1) ├─ payload : MotionEvent | UsageSnapshot ├─ .motion → MotionEvent | None (None unless kind == MOTION) └─ .usages → UsageSnapshot | None (None unless kind == USAGES)
| Payload | Fields | Held test |
|---|---|---|
MotionEvent | dx: int, dy: int, dz: int (the relative deltas at the merge point) | none |
UsageSnapshot | usages: List[Usage] (buttons, keys, and media, one shape) | is_held(usage): the built Usage is in the snapshot |
LogLine | level: LogLevel, text: str | none |
Field meanings and the full type tables are on Types & errors. Held usage ids come from the HID usage tables.
EventStream has a dropped property (an int): events the box queued that you didn't recv() fast enough, so the queue shed them. Read it to tell when you fell behind. LogStream has no such counter.
Consume loop
Subscribe, iterate, reactfrom medius import Device, CatchMask, CatchEventKind, Usage, Button
with Device.find() as dev:
with dev.catch_events(CatchMask.MOTION | CatchMask.BUTTONS) as events:
for ev in events: # ends when the link drops
if ev.kind == CatchEventKind.MOTION:
m = ev.motion
print(f"moved {m.dx},{m.dy} wheel {m.dz}")
elif ev.kind == CatchEventKind.USAGES:
if ev.usages.is_held(Usage.button(Button.LEFT)):
print("left held")
if events.dropped:
print("fell behind:", events.dropped, "dropped")events = dev.catch_events()
while running:
ev = events.recv_timeout(50) # wake every 50 ms to do other work
if ev is None:
continue
handle(ev)No async
Build it on the timeout / non-blocking readsThe streams are synchronous. There are no async def or await methods. To feed an event loop, drive it yourself: run recv_timeout(ms) or try_recv() on a worker thread (or in run_in_executor) and hand items to your loop. The pattern is the same in every binding; see Async.