Medius - BindingsStreams

Streams

Consume live input and device logs

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

Both calls live on the Device. They send a subscribe request to the box and hand back a stream object.

CallReturnsChannel
dev.catch_events(mask=CatchMask.ALL)EventStreamphysical mouse / key / media events (see Catch)
dev.logs()LogStreamdevice 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 memberValueSelects
CatchMask.MOTION1cursor movement (dx / dy)
CatchMask.WHEEL2wheel ticks
CatchMask.BUTTONS4mouse buttons
CatchMask.KEYS8keyboard keys
CatchMask.MEDIA16media keys
CatchMask.ALL31every category (the default)

Exactly what each bit selects is on Catch.

Receive

Block, poll, time out, or iterate

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

MethodReturnsBehaviour
recv()CatchEventBlocks 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 itemLoops on recv(); ends cleanly when the link drops (no exception).
clone()EventStreamA second handle to the same subscription; the queue is shared.
close() / with stream:noneRelease the subscription. Automatic on with exit and GC.

Event objects

What recv() hands back

A 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)
PayloadFieldsHeld test
MotionEventdx: int, dy: int, dz: int (the relative deltas at the merge point)none
UsageSnapshotusages: List[Usage] (buttons, keys, and media, one shape)is_held(usage): the built Usage is in the snapshot
LogLinelevel: LogLevel, text: strnone

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, react
from 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")
NON-BLOCKING POLL
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 reads

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