Medius - BindingsCalls & errors

Calls & errors

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

This page covers what's specific to C: how a failed call surfaces, who frees a handle, and how you build the generic inject / move / lock targets. 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_FLASH_TOOL8esptool failed (flash feature).
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.
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: a constructor hands you a pointer, medius_*_clone makes another owner of the same underlying object (reference-counted, like Device::clone in Rust), and you must call the matching medius_*_free on every handle you hold. See Connection and Lifecycle for what the link does between open and free.

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

Building targets

Usage, Motion, LockTarget for the generic verbs

Rust's generic inject / move_axis / lock targets are built structs in C: MediusUsage, MediusMotion, and MediusLockTarget, 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
/* 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);

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

MediusLockTarget side = medius_lock_target_usage(medius_usage_button(MEDIUS_BUTTON_SIDE1));
medius_device_lock(dev, side, MEDIUS_LOCK_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.

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