<!-- Source: https://medius.k4tech.net/bindings/python/streams -->
# Streams

_Consume live input and device logs_

[The box](/native/hardware.md) has two live channels: physical input it forwards to the PC ([Catch](/library/catch.md)) and its own log lines ([Logs & counters](/library/diagnostics.md)). Subscribe to each with a method on an open [Device](/bindings/python/api.md), then pull items off the returned stream. What an event _means_ lives on those pages; this page covers reading them in [Python](https://www.python.org).

```
  physical mouse / keyboard
            │   (also forwarded to the game PC)
            ▼
   ┌─────────────────┐                         ┌─────────────┐
   │   medius box    │  catch_events(mask)   ─▶│ EventStream │ ─▶ recv() ─▶ CatchEvent
   │                 │                         ├─────────────┤
   │                 │  logs()               ─▶│  LogStream  │ ─▶ recv() ─▶ LogLine
   └─────────────────┘                         └─────────────┘
```

> **Note**
>
> Both streams are [context managers](https://docs.python.org/3/reference/datamodel.html#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](/library/lifecycle.md).

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

| Call | Returns | Channel |
| --- | --- | --- |
| [`dev.catch_events(mask=CatchMask.ALL)`](/bindings/python/api.md#streams) | `EventStream` | physical mouse / key / media events (see [Catch](/library/catch.md)) |
| [`dev.logs()`](/bindings/python/api.md#streams) | `LogStream` | device log lines (see [Logs & counters](/library/diagnostics.md)) |

`mask` is a [`CatchMask`](/bindings/python/types.md#catchmask), an [`IntFlag`](https://docs.python.org/3/library/enum.html), 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](/library/catch.md).

## 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`](/bindings/python/types.md#catchevent)); `LogStream` is identical with [`LogLine`](/bindings/python/types.md#logline) in place of `CatchEvent`.

| Method | Returns | Behaviour |
| --- | --- | --- |
| `recv()` | `CatchEvent` | Blocks for the next item. Raises [`DisconnectedError`](/bindings/python/types.md#subclasses) 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 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](https://docs.python.org/3/library/dataclasses.html).

```
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`](/bindings/python/types.md#motionevent) | `dx: int`, `dy: int`, `dz: int` (the relative deltas at the merge point) | none |
| [`UsageSnapshot`](/bindings/python/types.md#usagesnapshot) | `usages: List[Usage]` (buttons, keys, and media, one shape) | `is_held(usage)`: the built [`Usage`](/bindings/python/types.md#input) is in the snapshot |
| [`LogLine`](/bindings/python/types.md#logline) | [`level: LogLevel`](/bindings/python/types.md#loglevel), `text: str` | none |

Field meanings and the full type tables are on [Types & errors](/bindings/python/types.md). Held [usage ids](/native/commands/usage.md) come from the [HID usage tables](https://www.usb.org/document-library/hid-usage-tables-14).

> **Note**
>
> `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_

```python
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

```python
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_

> **Warning**
>
> 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`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor)) and hand items to your loop. The pattern is the same in every binding; see [Async](/library/features/async.md).
