Medius - BindingsCalls & errors

Calls & errors

The Python patterns: blocking, exceptions, lifecycle, builders

What every Device call looks like in Python. 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.

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, scale/lock/unlock, led, reset, reapply, set_movement_riding, set_bearing, 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. 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_UPDATEUpdateErrorthe box refused a firmware update op
ERR_INVALID_ARGInvalidArgErrora bad argument value
ERR_PANICPanicErrorthe native core panicked
ERR_UNKNOWNMediusErroranything unmapped
ERR_CATCH_TABLE_FULLCatchTableFullErrorthe subscription needs more than the box's 32 entries
ERR_EMPTY_SUBSCRIPTIONEmptySubscriptionErrora subscription with no filters
ERR_CAPTURE_NOT_APPLICABLECaptureNotApplicableErrora Capture on an input class
ERR_NOT_AN_INPUT_FILTERNotAnInputFilterErrora traffic class passed to input_events
ERR_WILDCARD_NOT_INPUTWildcardNotInputErrorCatchFilter.everything() passed to input_events
ERR_HALF_EDGE_INPUT_FILTERHalfEdgeInputFilterErroran input filter narrowed to one edge
ERR_RESERVED_IDReservedIdErroran exact id equal to the blanket sentinel
ERR_RELATIVE_DIRECTIONRelativeDirectionErrorWITH or AGAINST where only a fixed sign or edge fits

The last six are subscription refusals, raised before a frame reaches the box.

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(CatchFilter.everything()):) ends cleanly instead of 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, InputStream, LogStream, Timeline, and MockBox follow the same pattern: a with block, .close(), or GC. Streams keep the device open while open; see Streams.

Building targets

Usage · Motion · LockTarget · CatchFilter

The inject, move, lock, and catch calls take a target object, not a bare value. Build it with a classmethod, then pass it in.

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, timing, pending)
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
CatchFilter.watch(usage) / .watch_axis(axis) / .watch_class(cls) / .watch_axes() / .all_input()dev.catch_events(filters), dev.input_events(filters)
see Catch
one subscription entry on an input class, addressed exactly as a lock is
.traffic(tc, id) / .traffic_class(tc) / .everything()one entry on a TrafficClass, or the wildcard across every class
.with_direction(direction) / .with_capture(n) / .on_press() / .inbound()a refinement of one, returned as a new filter
EXAMPLE
from medius import (Device, Usage, Motion, LockTarget, CatchFilter, TrafficClass,
                    Button, Action, Direction)

with Device.find() as dev:
    dev.inject(Usage.button(Button.LEFT), Action.PRESS)        # generic inject
    dev.move_axis(Motion.cursor(10, -4))                       # generic move (Ride / Keep by default)
    dev.lock(LockTarget.button(Button.LEFT), Direction.BOTH)

    stream = dev.catch_events([                                # generic subscribe
        CatchFilter.watch_axes(),
        CatchFilter.traffic(TrafficClass.VENDOR_INTERRUPT, 0x83).with_capture(16),
    ])

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.

A filter's direction is the same Direction: on an input class it picks the press or release edge (PRESS / RELEASE), on a traffic class the flow (IN / OUT).