First program
Connect, move, click, read one eventOne file that finds the box, reads its version, moves the cursor, clicks the left button, reads one physical event, then cleans up. Install it with pip (pip install medius, see Install). For what each call does, follow the links to the Rust Library and Native API.
Fire-and-forget calls return the moment they're queued (the injection model); Blocks calls wait for the box's reply. Either kind raises a MediusError on failure.
The program
The full listingimport time
from medius import Device, Usage, Button, CatchMask, 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) # nudge cursor 100 right
dev.press(Usage.button(Button.LEFT)) # hold left down
time.sleep(0.02)
dev.soft_release(Usage.button(Button.LEFT)) # let it back up
with dev.catch_events(CatchMask.ALL) as stream: # subscribe to physical input
event = stream.recv_timeout(5000) # one event, or None after 5 s
if event is None:
print("no physical 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")
else:
print(f"event {event.kind.name}")
# stream + link are closed here, on block exit
except NotFoundError:
raise SystemExit("no medius box found: check the control-port cable")The cursor won't move and the click won't land on this machine, and that's by design: injection only reaches the game PC through the clone port, not the control PC running this script. See Hardware for the port map.
Walkthrough
What each call is and where its meaning lives| Call | Kind | Does (follow the link) |
|---|---|---|
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 | Relative cursor move. See MOVE / Move. |
dev.press(Usage.button(Button.LEFT)) | Fire-and-forget | Hold a button. See the injection model / Inject. |
dev.soft_release(Usage.button(Button.LEFT)) | Fire-and-forget | Release, unless the user is physically holding it. See Injection. |
dev.catch_events(CatchMask.ALL) | Fire-and-forget | Subscribe; returns an EventStream. See Catch. |
stream.recv_timeout(5000) | Blocks | Wait up to 5 s for one CatchEvent, or None. More on Streams. |
Button ids are on Usage IDs; the full call surface is the API index.
Run it
One command, expected outputpython first.py
# firmware 3.0.0, proto 3
# motion dx=8 dy=-3 wheel=0The motion line appears once you move or click the real mouse within the 5-second window; otherwise you get no physical input within 5 s.
Reading events
Four ways to pull from a streamThe program uses recv_timeout so it can't hang. The readers differ in how they handle an empty queue and a dropped link:
| 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 cleanly |
A CatchEvent carries one of .motion / .usages (the other is None); a UsageSnapshot has is_held(usage) for any built Usage. Full payload shapes on Streams.
Errors
Failures raise; they're never a return codeEvery Device and stream call raises on failure; a plain call site is the success path. 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 speaks a different protocol | BadProtoVerError (check .proto_ver) |
| a stream read after the link drops | DisconnectedError |
| any other failure | a MediusError subclass |
Catch the base MediusError to handle them all at once. Full list and the Status codes on Types & errors; patterns on Calls & errors.
Closing
A with-block, close(), or garbage collectionA Device and each stream hold a live connection. Close it three ways, all 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; idempotent, a second call is a no-op |
| garbage collection | the object is collected, via __del__, if you forgot |
dev.clone() returns another handle to the same link; the connection lives until the last handle is freed. See Lifecycle and the keepalive thread.
Next steps
Further reading| Page | For |
|---|---|
| Calls & errors | how calls, enums, and error handling work in Python |
| Streams | consuming catch events and device logs |
| API index | every call, one line each, linked to what it does |
| Types & errors | the dataclasses, enums, and exception tree |