ProgramFull listing

import time
from medius import Device, Usage, Button, CatchFilter, NotFoundError

try:
    with Device.find() as dev:                       # open first box + handshake
        v = dev.query_version()                      # blocks for one reply
        print(f"firmware {v.fw_major}.{v.fw_minor}.{v.fw_patch}, proto {v.proto_ver}")

        dev.move_rel(100, 0)                         # cursor 100 right
        dev.press(Usage.button(Button.LEFT))         # hold left down
        time.sleep(0.02)
        dev.soft_release(Usage.button(Button.LEFT))  # release it

        with dev.catch_events(CatchFilter.everything()) as stream:  # subscribe to everything
            event = stream.recv_timeout(5000)                      # one event, or None after 5 s
            if event is None:
                print("no native input within 5 s")
            elif event.motion:
                m = event.motion
                print(f"motion  dx={m.dx} dy={m.dy} wheel={m.dz}")
            elif event.usages and event.usages.is_held(Usage.button(Button.LEFT)):
                print("left button held")
            elif event.traffic:
                t = event.traffic
                print(f"traffic {t.catch_class.name} id={t.id:#04x} "
                      f"{len(t.bytes)} of {t.true_len} bytes")
            else:
                print(f"event  {event.kind.name}")
        # stream and link close on block exit
except NotFoundError:
    raise SystemExit("no medius box found: check the control-port cable")

Injection reaches only the game PC, through the clone port. The cursor and click don't show on the control PC running this script. Port map: Hardware.

WalkthroughEach call, its kind, and its concept page

CallKindDoes
Device.find()BlocksOpen the first box and run the handshake. See Connection.
dev.query_version()BlocksRead a Version. See Requests.
dev.move_rel(100, 0)Fire-and-forgetSee MOVE / Move.
dev.press(Usage.button(Button.LEFT))Fire-and-forgetSends Action.PRESS. See the injection model / Inject.
dev.soft_release(Usage.button(Button.LEFT))Fire-and-forgetSends Action.SOFT_RELEASE. See Injection.
dev.catch_events(CatchFilter.everything())Fire-and-forgetSubscribe with one CatchFilter (or an iterable); returns an EventStream. See Catch.
stream.recv_timeout(5000)BlocksWait up to 5 s for one CatchEvent, or None. See Streams.

Button ids are on Usage IDs; every call is on the API index.

Run itOne command, expected output

python first.py
# firmware 3.4.6, proto 9
# motion  dx=8 dy=-3 wheel=0

The last line needs a move or click on the real mouse within 5 s. CatchFilter.everything() covers every class, so a real device with busy vendor endpoints can print a traffic line first. Narrow the subscription with CatchFilter.watch_axes().

Reading eventsFour ways to pull from a stream

The program uses recv_timeout so it can't hang.

ReaderEmpty queueLink dropped
stream.recv()blocks foreverraises DisconnectedError
stream.recv_timeout(ms)returns None after msreturns None (same as a timeout)
stream.try_recv()returns None at oncereturns None (same as empty)
for ev in stream:blocks for eachloop ends without raising

A CatchEvent carries one of .motion / .usages / .traffic (the other two are None), plus .ts_us and the .clock domain that stamped it. A UsageSnapshot has is_held(usage) for any built Usage; a TrafficEvent has truncated(). Payload shapes are on Streams.

dev.input_events() yields press and release edges, already diffed from the snapshots.

ErrorsRaised exceptions, not return codes

Every Device and stream call raises on failure. Each exception is a MediusError subclass carrying .status, .message, and .proto_ver.

WhenRaises
find() with no box presentNotFoundError
a query_* gets no reply in timeQueryTimeoutError
the box runs a different protocol versionBadProtoVerError (check .proto_ver)
a stream read after the link dropsDisconnectedError
any other failurea MediusError subclass

Catch MediusError to handle them all. Full list and the Status codes on Types & errors; patterns on Calls & errors.

ClosingWith-block, close(), or garbage collection

A Device and each stream hold a live connection. Any of these closes it, and they are safe to combine:

MechanismFrees when
with Device.find() as dev:the context manager exits (used twice above, for the link and the stream)
dev.close()you call it; a second call is a no-op
garbage collectionthe object is collected, via __del__

dev.clone() returns another handle to the same link; the connection stays open until the last handle is freed. See Lifecycle and the keepalive thread.