Calls & errors
The Python patterns: blocking, exceptions, lifecycle, buildersWhat every Device call looks like in Python: when it blocks, how a failure surfaces, when the handle is freed, and how the generic targets are built. The full call list is on API index; the value types on Types & errors. What each command does lives in the Rust Library and Native API sections.
Fire-and-forget vs blocking
What the two badges meanEvery Device call carries one of two badges, by whether it waits for the box to answer or fires and forgets.
| Badge | Means | Which calls |
|---|---|---|
| Fire-and-forget | Queues a frame and returns at once. No reply is read. | move_rel, wheel, inject, press, soft_release, lock/unlock, led, reset, reapply, set_movement_riding, set_emit_pace … |
| Blocks | Sends, then waits for the box's reply (or times out). | Device.open / find (the handshake), every query_* / caps / counters, and a stream recv() |
A Fire-and-forget call returning without raising means the frame was queued, not that the box acted on it. Confirm with a Blocks query like dev.query_health(). Blocking calls use the default reply wait, medius.default_query_timeout_ms() (1000 ms).
Errors
A failed call raises MediusErrorThere is no status return in Python: a failed call raises. The base type is MediusError (an Exception subclass); each status code (a Status IntEnum) maps to its own subclass, so you can catch the case you care about. Catch MediusError for all of them.
class MediusError(Exception):
status: Status # the failure code
message: str # the box's last error text, may be ""
proto_ver: int # offending byte for BadProtoVerError, else 0
str(err) # "ERR_NOT_FOUND: no medius port found" (or only the name)Status | Subclass raised | Typically means |
|---|---|---|
ERR_IO | IoError | serial read/write failed |
ERR_NOT_FOUND | NotFoundError | no box at that path / none present |
ERR_NO_REPLY | NoReplyError | box never answered |
ERR_BAD_PROTO_VER | BadProtoVerError | firmware protocol mismatch (see .proto_ver) |
ERR_QUERY_TIMEOUT | QueryTimeoutError | a query_* outran its wait |
ERR_DISCONNECTED | DisconnectedError | the link dropped (see below) |
ERR_FRAME_TOO_LONG | FrameTooLongError | payload over the frame limit |
ERR_FLASH_TOOL | FlashToolError | esptool flash failed |
ERR_INVALID_ARG | InvalidArgError | a bad argument value |
ERR_PANIC | PanicError | the native core panicked |
ERR_UNKNOWN | MediusError | anything unmapped |
from medius import Device, MediusError, DisconnectedError
try:
with Device.find() as dev:
dev.move_rel(10, 0)
print(dev.query_health())
except DisconnectedError:
print("the link dropped mid-session")
except MediusError as e:
print(e.status, "-", e.message) # e.g. Status.ERR_NOT_FOUND - no medius port foundA dropped link raises DisconnectedError from a normal call, but a stream iterator (for ev in dev.catch_events():) stops cleanly instead: it returns when the link drops rather than raising. Calling recv() directly still raises. Box-side telemetry behind these errors is on Diagnostics.
Lifecycle
Open, clone, and release the handleA Device owns a native handle. Open one, optionally clone() it, and release it three ways. Connection sharing is on Connection and Lifecycle.
| Open with | Does |
|---|---|
Device.find() | First box found + handshake. Blocks |
Device.open(path) | One serial path + handshake. Blocks |
dev.clone() | Another handle to the same link; the connection is shared. |
MockBox().open() | Open over an in-process mock box (needs the mock feature). |
Device.find() ──┐
├──▶ one USB-serial link (stays up while ANY handle is open)
dev.clone() ────┘
each handle is freed on its own; the link closes with the last one| Route | When the handle frees |
|---|---|
with Device.find() as dev: | When the context manager block exits (__exit__). Preferred. |
dev.close() | Immediately. Idempotent, so it's safe to call twice. |
| garbage collection | Best-effort via __del__. Don't rely on timing. |
# preferred: the context manager closes on exit
with Device.find() as dev:
dev.move_rel(5, 5)
# or hand a clone to a worker thread and close each when done
dev = Device.find()
worker = dev.clone()
worker.close()
dev.close()EventStream, LogStream, and MockBox follow the same pattern: a with block, .close(), or GC. Streams hold the device alive while open; see Streams.
Building targets
Usage · Motion · LockTargetThe inject / press / move_axis / lock calls take a target object, not a bare value. Build it with a classmethod, then pass it in. OneUsage (button, key, or media) feeds every inject verb.
| Builder | Feeds | What it makes |
|---|---|---|
Usage.button(button) | dev.inject(input, action), dev.press(input)see Inject | a mouse-button usage |
Usage.key(key) | a keyboard-key usage (keycodes) | |
Usage.media(media) | a consumer/media usage (usages) | |
Motion.cursor(dx, dy) | dev.move_axis(motion)see Move | a relative cursor nudge |
Motion.wheel(delta) | a wheel turn | |
LockTarget.x() / y() / wheel() | dev.lock(target, direction) / unlocksee Lock | an axis lock target |
LockTarget.usage(usage) (or button/key/media) | a usage lock target |
from medius import Device, Usage, Motion, LockTarget, Button, Action, LockDirection
with Device.find() as dev:
dev.inject(Usage.button(Button.LEFT), Action.PRESS) # generic inject
dev.move_axis(Motion.cursor(10, -4)) # generic move
dev.lock(LockTarget.button(Button.LEFT), LockDirection.BOTH)action is an Action (PRESS / SOFT_RELEASE / FORCE_RELEASE); the injection model defines what each does. Usage.button takes a Button; Usage.key/media accept a Key/MediaKey or a raw int.