Medius - BindingsCalls & errors

Calls & errors

The Python patterns: blocking, exceptions, lifecycle, builders

What 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 mean

Every Device call carries one of two badges, by whether it waits for the box to answer or fires and forgets.

BadgeMeansWhich calls
Fire-and-forgetQueues 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
BlocksSends, 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 MediusError

There 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 → EXCEPTION
StatusSubclass raisedTypically means
ERR_IOIoErrorserial read/write failed
ERR_NOT_FOUNDNotFoundErrorno box at that path / none present
ERR_NO_REPLYNoReplyErrorbox never answered
ERR_BAD_PROTO_VERBadProtoVerErrorfirmware protocol mismatch (see .proto_ver)
ERR_QUERY_TIMEOUTQueryTimeoutErrora query_* outran its wait
ERR_DISCONNECTEDDisconnectedErrorthe link dropped (see below)
ERR_FRAME_TOO_LONGFrameTooLongErrorpayload over the frame limit
ERR_FLASH_TOOLFlashToolErroresptool flash failed
ERR_INVALID_ARGInvalidArgErrora bad argument value
ERR_PANICPanicErrorthe native core panicked
ERR_UNKNOWNMediusErroranything unmapped
EXAMPLE
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 found

A 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 handle

A Device owns a native handle. Open one, optionally clone() it, and release it three ways. Connection sharing is on Connection and Lifecycle.

Open withDoes
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
THREE WAYS TO RELEASE
RouteWhen 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 collectionBest-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 · LockTarget

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

BuilderFeedsWhat 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) / unlock
see Lock
an axis lock target
LockTarget.usage(usage) (or button/key/media)a usage lock target
EXAMPLE
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.