Fire-and-forget vs blockingCall-kind badges

BadgeMeaningCalls
Fire-and-forgetQueues a frame and returns at once; no reply is read (fire-and-forget).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()

When a Fire-and-forget call returns without raising, the frame was queued. That does not mean the box acted on it. Confirm with a Blocks query such as dev.query_health(). Default reply wait: medius.default_query_timeout_ms() (1000 ms).

ErrorsMediusError and its subclasses

A failed call raises. The base type is MediusError (an Exception subclass); each status code (a Status IntEnum) maps to a distinct subclass. Catch MediusError for all of them; full mapping on Types & errors.

class MediusError(Exception):
    status:    Status   # the failure code
    message:   str      # the last error text, may be ""
    proto_ver: int      # offending byte for BadProtoVerError, else 0

str(err)   # "ERR_NOT_FOUND: no medius device found"  (or only the name)
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.name, "-", e.message)   # e.g. ERR_NOT_FOUND - no medius device found

A dropped link raises DisconnectedError from a call and from recv(), but a stream iterator (for ev in dev.catch_events(CatchFilter.everything()):) ends without raising. Box-side telemetry behind these errors is on Diagnostics.

LifecycleHandle ownership and release

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.
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 separately; the link closes with the last one
RELEASE
RouteFreed
with Device.find() as dev:When the context manager block exits (__exit__). Preferred.
dev.close()Immediately; a second call is a no-op.
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. An open stream keeps the device open; see Streams.

Building targetsUsage, Motion, LockTarget, CatchFilter for the generic verbs

The inject, move, lock, and catch calls take a target object built with a classmethod, not a bare value.

BuilderFeedsResult
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 move
Motion.wheel(delta)a wheel turn
LockTarget.x() / y() / wheel() / pan()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 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 narrowed copy of one
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).