Fire-and-forget vs blockingCall-kind badges

Both kinds return a MediusStatus; the API index badges each call.

BadgeBehaviourFails with
Fire-and-forgetReturns once the frame is queued, without awaiting a reply. 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.

MEDIUS_STATUS_OK from a Fire-and-forget call means the writer took the frame, not that the box acted on it. Default reply wait: medius_default_query_timeout_ms(); held-override keepalive cadence: medius_default_keepalive_cadence_ms() (see keepalive).

ErrorsMediusStatus + a thread-local last error

Every fallible call returns a MediusStatus and writes its result through an out-param. MEDIUS_STATUS_OK is 0; on anything else the out-param is untouched. Types & errors lists every enumerator; Errors is the canonical mapping.

  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)
                          medius_last_error_update(&out)        op + status (ErrUpdate only)
ERROR DETAIL
uintptr_t medius_last_error_message(char *buf, uintptr_t cap);
uint8_t   medius_last_error_proto_ver(void);
bool      medius_last_error_update(MediusUpdateError *out);
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); retry with a larger buffer on truncation. medius_last_error_proto_ver returns the offending version byte after a BadProtoVer, else 0. medius_last_error_update writes the refused update op and status (MediusUpdateError) after MEDIUS_STATUS_ERR_UPDATE, else returns false.

The last error is thread-local and overwritten by the next medius_* call on that thread; read it right after the failing call.

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

LifecycleOpaque pointers, manual free, no RAII or GC

Handles are opaque, reference-counted pointers with no destructor or RAII. medius_*_clone adds an owner of the same object; call the matching medius_*_free on every handle. 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
MediusClipFramemedius_clip_frame_new-medius_clip_frame_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 */

A clip builder, clip frame or timeline takes calls from one thread at a time. Every other handle takes calls from several threads at once. Free a handle only when no other thread is using it. To stop a thread blocked in a receive: close the stream, let the receive return, then free the stream.

clone(NULL) returns NULL and every *_free(NULL) is a no-op, so cleanup needs no null checks. A stream unsubscribes when its last handle is freed, or at once on medius_*_stream_close.

Catch events and log lines are fixed-size structs written into your buffer, with nothing to free. A MediusTrafficEvent holds captured bytes inline (bytes[MEDIUS_MAX_TRAFFIC_BYTES] plus len), so a copy owns nothing and outlives its stream.

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

Rust's generic inject / move_axis / lock targets are structs in C (MediusUsage, MediusMotion, MediusLockTarget, plus MediusCatchFilter), each with helper constructors.

A MediusUsage holds a button id, keycode, or Consumer usage; one value serves inject, lock and catch.

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 / all helpers
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);

Buttons, keys and media lock the same way: medius_lock_target_usage(medius_usage_key(...)) locks a key, medius_lock_target_axis(...) an axis or the wheel. Fields: Types & errors.

A catch filter starts from a base that names what to observe; each modifier returns a narrowed copy, so one blanket can seed several entries. Fields: 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 */

One usage can address both a lock and a catch entry. Catch covers where each class is tapped and which ones a lock reaches; Streams covers what comes back.