Calls & errors
The C-specific shapes: status codes, handle lifecycle, buildersThis 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 errorEvery 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)| MediusStatus | Value | Means |
|---|---|---|
MEDIUS_STATUS_OK | 0 | Success; the out-param is written. |
MEDIUS_STATUS_ERR_IO | 1 | Serial I/O failed on the link. |
MEDIUS_STATUS_ERR_NOT_FOUND | 2 | No box found (open / find). |
MEDIUS_STATUS_ERR_NO_REPLY | 3 | A query got no RESP frame. |
MEDIUS_STATUS_ERR_BAD_PROTO_VER | 4 | Protocol mismatch at the handshake; read medius_last_error_proto_ver(). |
MEDIUS_STATUS_ERR_QUERY_TIMEOUT | 5 | The RESP wait elapsed. |
MEDIUS_STATUS_ERR_DISCONNECTED | 6 | Link dropped or a stream closed (see below). |
MEDIUS_STATUS_ERR_FRAME_TOO_LONG | 7 | Payload exceeded the wire limit. |
MEDIUS_STATUS_ERR_FLASH_TOOL | 8 | esptool failed (flash feature). |
MEDIUS_STATUS_ERR_INVALID_ARG | 9 | A bad argument (e.g. a null handle). |
MEDIUS_STATUS_ERR_PANIC | 10 | An internal panic was caught at the boundary. |
MEDIUS_STATUS_ERR_UNKNOWN | 11 | Unspecified, or a platform-gated call on an unsupported OS. |
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 GCHandles 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| Handle | Create | Clone | Free |
|---|---|---|---|
MediusDevice | medius_device_open · _find · _with_mock | medius_device_clone | medius_device_free |
MediusEventStream | medius_device_catch_events | medius_event_stream_clone | medius_event_stream_free |
MediusLogStream | medius_device_logs | medius_log_stream_clone | medius_log_stream_free |
MediusMockBox | medius_mock_new | medius_mock_clone | medius_mock_free |
MediusClipBuilder | medius_clip_builder_new | - | medius_clip_builder_free |
MediusClip | medius_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 verbsRust'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.
| Builder | Returns | For |
|---|---|---|
medius_usage_button(MediusButton) | MediusUsage | inject / injection model |
medius_usage_key(MediusKey) | MediusUsage | |
medius_usage_media(MediusMediaKey) | MediusUsage | |
medius_motion_cursor(dx, dy) | MediusMotion | move / MOVE |
medius_motion_wheel(delta) | MediusMotion | |
medius_lock_target_axis(MediusLockTargetKind) | MediusLockTarget | lock / 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 badgesTwo return shapes, both yielding a MediusStatus. The badge on each row of the API index says which is which.
| Badge | Behaviour | Fails with |
|---|---|---|
| Fire-and-forget | Returns 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. |
| Blocks | Sends 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).