Medius - BindingsCalls & errors

Calls & errors

The C-specific shapes: status codes, handle lifecycle, builders

What each call does lives in the Rust Library and Native API sections. The full call list is on the API index; structs and enums are on Types & errors.

Fire-and-forget calls return as soon as the frame is queued; Blocks calls wait for the box's reply. Both return a MediusStatus.

Errors

MediusStatus + a thread-local last error

Every fallible call returns a MediusStatus and writes its real result through an out-param. MEDIUS_STATUS_OK is 0; anything else is a failure and the out-param is untouched. Fetch the human-readable detail separately. What each code means lives on Errors.

  call ──▶ MediusStatus
             │
             ├─ == OK ──▶ the out-param is valid, carry on
             └─ != OK ──▶ medius_last_error_message(buf, cap)   text (this thread)
                          medius_last_error_proto_ver()         byte (BadProtoVer only)
MediusStatusValueMeans
MEDIUS_STATUS_OK0Success; the out-param is written.
MEDIUS_STATUS_ERR_IO1Serial I/O failed on the link.
MEDIUS_STATUS_ERR_NOT_FOUND2No box found (open / find).
MEDIUS_STATUS_ERR_NO_REPLY3A query got no RESP frame.
MEDIUS_STATUS_ERR_BAD_PROTO_VER4Protocol mismatch at the handshake; read medius_last_error_proto_ver().
MEDIUS_STATUS_ERR_QUERY_TIMEOUT5The RESP wait elapsed.
MEDIUS_STATUS_ERR_DISCONNECTED6Link dropped or a stream closed (see below).
MEDIUS_STATUS_ERR_FRAME_TOO_LONG7Payload exceeded the wire limit.
MEDIUS_STATUS_ERR_UPDATE8The box refused a firmware update op.
MEDIUS_STATUS_ERR_INVALID_ARG9A bad argument (e.g. a null handle).
MEDIUS_STATUS_ERR_PANIC10An internal panic was caught at the boundary.
MEDIUS_STATUS_ERR_UNKNOWN11Unspecified, or a platform-gated call on an unsupported OS.
MEDIUS_STATUS_ERR_CATCH_TABLE_FULL12The subscription needs more entries than the box's table holds.
MEDIUS_STATUS_ERR_EMPTY_SUBSCRIPTION13A catch subscription with no filters, which would never yield an event.
MEDIUS_STATUS_ERR_CAPTURE_NOT_APPLICABLE14A non-zero capture on an input class, which carries no packet.
MEDIUS_STATUS_ERR_NOT_AN_INPUT_FILTER15A traffic class passed to medius_device_input_events.
MEDIUS_STATUS_ERR_WILDCARD_NOT_INPUT16The everything filter passed to medius_device_input_events; it covers traffic too.
MEDIUS_STATUS_ERR_HALF_EDGE_INPUT_FILTER17An input filter narrowed to one edge, which cannot be decoded into press and release.
MEDIUS_STATUS_ERR_RESERVED_ID18An exact id equal to the blanket sentinel, which would address the whole class.
MEDIUS_STATUS_ERR_RELATIVE_DIRECTION19MEDIUS_DIRECTION_WITH or _AGAINST where only a fixed sign or edge can be addressed.
READING THE DETAIL
uintptr_t medius_last_error_message(char *buf, uintptr_t cap);
uint8_t   medius_last_error_proto_ver(void);
MediusDevice *dev = NULL;
if (medius_device_find(&dev) != MEDIUS_STATUS_OK) {
    char buf[256];
    medius_last_error_message(buf, sizeof buf);   /* NUL-terminated, truncated to cap */
    fprintf(stderr, "open failed: %s\n", buf);
    return 1;
}

medius_last_error_message returns the full message length in bytes (excluding the NUL), so a caller can size a buffer and retry on truncation. medius_last_error_proto_ver returns the offending version byte after a BadProtoVer, else 0.

The last error is thread-local and overwritten by the next medius_* call on that thread. Read it right after the call that failed, before doing anything else on the same thread.

A device call returns MEDIUS_STATUS_ERR_DISCONNECTED once the link drops, and a stream's blocking recv returns it when the stream closes (after a reset or link loss). Recover with medius_device_reconnect or by re-opening (see Lifecycle).

Lifecycle

Opaque pointers, manual free, no RAII or GC

Handles are opaque pointers you own. There's no destructor or RAII: medius_*_clone makes another owner of the same underlying object (reference-counted), and you must call the matching medius_*_free on every handle you hold. See Connection and Lifecycle.

  medius_device_open / _find  ──▶  MediusDevice *    (you own it)
                                        │  medius_device_clone
                                        ▼
                                   MediusDevice *    (2nd owner, same link)

  medius_device_free(handle)  ──▶  drop one owner    (NULL = no-op)
  free the last owner         ──▶  joins the reader + keepalive threads
HandleCreateCloneFree
MediusDevicemedius_device_open · _find · _with_mockmedius_device_clonemedius_device_free
MediusEventStreammedius_device_catch_eventsmedius_event_stream_clonemedius_event_stream_free
MediusInputStreammedius_device_input_events-medius_input_stream_free
MediusTimelinemedius_timeline_new-medius_timeline_free
MediusLogStreammedius_device_logsmedius_log_stream_clonemedius_log_stream_free
MediusMockBoxmedius_mock_newmedius_mock_clonemedius_mock_free
MediusClipBuildermedius_clip_builder_new-medius_clip_builder_free
MediusClipmedius_device_clip-medius_clip_free
MediusDevice *dev = NULL;
if (medius_device_find(&dev) != MEDIUS_STATUS_OK) { return 1; }

MediusDevice *worker = medius_device_clone(dev);  /* same link, ref-counted */
/* ... use either handle from either thread ... */

medius_device_free(worker);   /* drop one owner */
medius_device_free(dev);      /* last owner -> joins the background threads */

clone(NULL) returns NULL and every *_free(NULL) is a no-op, so cleanup paths don't need null checks. Freeing a stream unsubscribes when its last handle drops.

Catch events and log lines are fixed-size structs written into your buffer, so there's nothing to free per event. A MediusTrafficEvent holds captured bytes inline in bytes[MEDIUS_MAX_TRAFFIC_BYTES] with a len beside it, so a copied event owns nothing and outlives its stream.

Building targets

Usage, Motion, LockTarget, CatchFilter for the generic verbs

Rust's generic inject / move_axis / lock targets are built structs in C: MediusUsage, MediusMotion, and MediusLockTarget, plus MediusCatchFilter, each with a helper constructor.

A MediusUsage holds a button id, keycode, or Consumer usage, and the same value drives an inject, a lock, or a catch test.

BuilderReturnsFor
medius_usage_button(MediusButton)MediusUsageinject / injection model
medius_usage_key(MediusKey)MediusUsage
medius_usage_media(MediusMediaKey)MediusUsage
medius_motion_cursor(dx, dy)MediusMotionmove / MOVE
medius_motion_wheel(delta)MediusMotion
medius_lock_target_axis(MediusLockTargetKind)MediusLockTargetlock / LOCK
medius_lock_target_usage(MediusUsage)MediusLockTarget
medius_catch_filter_watch(MediusUsage)MediusCatchFiltercatch / the whole helper set
medius_catch_filter_watch_axis(MediusAxis)MediusCatchFilter
medius_catch_filter_traffic(MediusCatchClass, uint16_t)MediusCatchFilter
/* inject: build a usage, then apply an Action */
MediusUsage lmb = medius_usage_button(MEDIUS_BUTTON_LEFT);
medius_device_inject(dev, lmb, MEDIUS_ACTION_PRESS);
medius_device_press(dev, medius_usage_key(MEDIUS_KEY_W));   /* keys and media inject the same way */

/* move: build a motion arm */
MediusMotion m = medius_motion_cursor(100, -50);
medius_device_move_axis(dev, m, MEDIUS_MOVE_TIMING_RIDE, MEDIUS_PENDING_MOTION_KEEP);

/* lock: an axis, or any usage */
MediusLockTarget x = medius_lock_target_axis(MEDIUS_LOCK_TARGET_KIND_X);
medius_device_lock(dev, x, MEDIUS_DIRECTION_BOTH);

MediusLockTarget side = medius_lock_target_usage(medius_usage_button(MEDIUS_BUTTON_SIDE1));
medius_device_lock(dev, side, MEDIUS_DIRECTION_BOTH);

A button, key, and media usage all lock the same way: medius_lock_target_usage(medius_usage_key(...)) locks a key, medius_lock_target_axis(...) an axis or the wheel. The struct fields are on Types & errors.

A catch filter narrows the same way: a base names what to observe, a modifier returns a narrowed copy. Nothing is mutated, so one blanket can be the base for several entries. Fields are on Types & errors.

EXAMPLE
/* every key edge, whole report */
MediusCatchFilter keys = medius_catch_filter_watch_class(MEDIUS_CLASS_KEY);

/* one vendor endpoint's IN packets, first 16 bytes each */
MediusCatchFilter ep = medius_catch_filter_with_capture(
    medius_catch_filter_inbound(
        medius_catch_filter_traffic(MEDIUS_CATCH_CLASS_VENDOR_INTERRUPT, 0x83)),
    16);

MediusCatchFilter filters[2] = { keys, ep };
MediusEventStream *events = NULL;
medius_device_catch_events(dev, filters, 2, &events);   /* the array is not retained */

Locking and catching are separate subscriptions over the same address vocabulary, so the same usage can be in both. Catch is sampled before lock suppression, so a locked input still reports.

The classes above the four input ones have no lock equivalent. See Streams for what comes back.

Fire-and-forget vs blocking

The two call-kind badges

Two return shapes, both yielding a MediusStatus. The badge on each row of the API index says which is which.

BadgeBehaviourFails with
Fire-and-forgetReturns once the frame is queued for the wire; no reply is awaited. Move, inject, lock, LED, options, clip playback. See fire-and-forget.MEDIUS_STATUS_ERR_IO / MEDIUS_STATUS_ERR_DISCONNECTED if the link is down.
BlocksSends a QUERY and waits for the box's RESP, up to the query timeout. The medius_device_query_* reads and the open/handshake calls. See Requests.MEDIUS_STATUS_ERR_NO_REPLY / MEDIUS_STATUS_ERR_QUERY_TIMEOUT.

A Fire-and-forget call returning MEDIUS_STATUS_OK means the frame was handed to the writer, not that the box acted on it; there's no acknowledgement. The default reply wait is medius_default_query_timeout_ms(); the held-override keepalive cadence is medius_default_keepalive_cadence_ms() (see keepalive).