First program
One file: find the box, move, click, read one native event. Install with pip (pip install medius, see Install). What each call does is in the Rust Library and Native API.
Fire-and-forget calls return once queued (the injection model); Blocks calls wait for the box's reply. Both raise a MediusError on failure.
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
| Call | Kind | Does |
|---|---|---|
Device.find() | Blocks | Open the first box and run the handshake. See Connection. |
dev.query_version() | Blocks | Read a Version. See Requests. |
dev.move_rel(100, 0) | Fire-and-forget | See MOVE / Move. |
dev.press(Usage.button(Button.LEFT)) | Fire-and-forget | Sends Action.PRESS. See the injection model / Inject. |
dev.soft_release(Usage.button(Button.LEFT)) | Fire-and-forget | Sends Action.SOFT_RELEASE. See Injection. |
dev.catch_events(CatchFilter.everything()) | Fire-and-forget | Subscribe with one CatchFilter (or an iterable); returns an EventStream. See Catch. |
stream.recv_timeout(5000) | Blocks | Wait 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 it One command, expected output
python first.py
# firmware 3.4.6, proto 9
# motion dx=8 dy=-3 wheel=0The 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 events Four ways to pull from a stream
The program uses recv_timeout so it can't hang.
| Reader | Empty queue | Link dropped |
|---|---|---|
stream.recv() | blocks forever | raises DisconnectedError |
stream.recv_timeout(ms) | returns None after ms | returns None (same as a timeout) |
stream.try_recv() | returns None at once | returns None (same as empty) |
for ev in stream: | blocks for each | loop 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.
| When | Raises |
|---|---|
find() with no box present | NotFoundError |
a query_* gets no reply in time | QueryTimeoutError |
| the box runs a different protocol version | BadProtoVerError (check .proto_ver) |
| a stream read after the link drops | DisconnectedError |
| any other failure | a 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:
| Mechanism | Frees 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 collection | the 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.