SubscribeOpen an event or log stream

Both calls return a MediusStatus and write an opaque handle through an out-param. A catch subscription is an array of MediusCatchFilter entries from the medius_catch_filter_* helpers. The call doesn't retain the array, so it can live on the stack. Fields: Types & errors; classes: MediusCatchClass.

MediusStatus medius_device_catch_events(struct MediusDevice *dev,
                                        const MediusCatchFilter *filters,
                                        uintptr_t n_filters,
                                        struct MediusEventStream **out);

MediusStatus medius_device_logs(struct MediusDevice *dev,
                                struct MediusLogStream **out);
CallDoes
medius_device_catch_events(dev, filters, n_filters, &out)Subscribe to everything the filters address. See Catch.
medius_device_logs(dev, &out)Open the device LOG channel. See Logs.
medius_event_stream_clone(stream) / medius_log_stream_clone(stream)Another handle to the same subscription. Null in → null out.
medius_event_stream_free(stream) / medius_log_stream_free(stream)Release a handle (unsubscribes when the last clone drops). Null is a no-op.
medius_event_stream_close(stream) / medius_input_stream_close(stream) / medius_log_stream_close(stream)End the stream for every clone; see Close. Null is a no-op.
EXAMPLE
MediusCatchFilter filters[3] = {
    /* every native button and axis, in full */
    medius_catch_filter_watch_class(MEDIUS_CLASS_BUTTON),
    medius_catch_filter_watch_axes(),
    /* plus one vendor endpoint's IN traffic, first 16 bytes of each packet */
    medius_catch_filter_with_capture(
        medius_catch_filter_inbound(
            medius_catch_filter_traffic(MEDIUS_CATCH_CLASS_VENDOR_INTERRUPT, 0x83)),
        16),
};

MediusEventStream *events = NULL;
medius_device_catch_events(dev, filters, 3, &events);

The box's table holds MEDIUS_MAX_CATCH_ENTRIES (32) entries. More entries, or a filter the box cannot honour, fail the whole call with a distinct MediusStatus.

Every class at full length is more than the control link carries: under a busy mouse, bulk goes undrained. Lower capture for headroom. Queue ranking and link budget: Delivery.

ReceiveBlocking, polling, and timed reads

Loop a receive call until it returns MEDIUS_STATUS_ERR_DISCONNECTED (the stream closes after a reset, link loss or a close). Each call writes one event through *out.

FunctionReturnsBlocking
medius_event_stream_recv(stream, &out)MediusStatus (MEDIUS_STATUS_ERR_DISCONNECTED on close)Yes, until the next event
medius_event_stream_try_recv(stream, &out)bool (false if the queue is empty)No, returns at once
medius_event_stream_recv_timeout(stream, timeout_ms, &out)bool (false on timeout or close)Up to timeout_ms
medius_event_stream_is_connected(stream)bool (false once the stream has closed)No, returns at once
LOG STREAM
FunctionReturns
medius_log_stream_recv(stream, &out)MediusStatus (MEDIUS_STATUS_ERR_DISCONNECTED on close)
medius_log_stream_try_recv(stream, &out)bool (false if none queued)
medius_log_stream_recv_timeout(stream, timeout_ms, &out)bool (false on timeout or close)
medius_log_stream_is_connected(stream)bool (false once the stream has closed)

Event objectsFixed-size PODs, nothing to free per event

medius_event_stream_recv fills a MediusCatchEvent: a kind tag plus a union. A usage arm holds class-tagged MediusUsage values (a button, key, or media HID usage).

Variable-length payloads are inline arrays with a count, never pointers: usages cap at MEDIUS_MAX_USAGES (256), a captured packet at MEDIUS_MAX_TRAFFIC_BYTES (180). A copied event stays valid; nothing needs freeing.

typedef struct MediusCatchEvent {
    MediusCatchEventKind kind;          // MOTION=0, USAGES=1, TRAFFIC=2
    uint32_t ts_us;                     // box microseconds, wraps every ~71.6 min
    MediusClockDomain clock;            // HOST_CHIP=0, DEVICE_CHIP=1: which chip stamped ts_us
    union MediusCatchEventData data;    // read the arm for kind
} MediusCatchEvent;

struct MediusMotionEvent { int16_t dx, dy, dz, pan; };   // cursor, wheel and pan deltas

struct MediusUsageEvent {               // one class's held usages
    MediusClass class_;                 // BUTTON / KEY / MEDIA
    uint8_t direction;                  // POSITIVE = the set grew, NEGATIVE = it shrank
    uint16_t n;
    MediusUsage usages[256];
};

struct MediusTrafficEvent {             // one captured packet
    MediusCatchClass class_;            // which class matched
    uint16_t id;                        // endpoint no. / interface no.
    uint8_t direction;                  // POSITIVE = IN, NEGATIVE = OUT
    uint8_t flags;                      // class-specific; the kind, for BUS
    uint16_t true_len;                  // length on the bus, before capture
    uint16_t len;                       // bytes kept in bytes[]
    uint8_t bytes[180];                 // valid over bytes[0..len]
};

typedef struct MediusLogLine {          // from medius_log_stream_recv
    MediusLogLevel level;               // ERROR=0, WARN=1, INFO=2, DEBUG=3, VERBOSE=4
    char text[512];                     // NUL-terminated
} MediusLogLine;
When kind isReadFields
MEDIUS_CATCH_EVENT_KIND_MOTIONdata.motiondx, dy, dz, pan (cursor, wheel and AC Pan deltas)
MEDIUS_CATCH_EVENT_KIND_USAGESdata.usagesusages[0..n], each a class-tagged MediusUsage
MEDIUS_CATCH_EVENT_KIND_TRAFFICdata.trafficclass_, id, direction, flags, and bytes[0..len] of a true_len-byte packet

Host-chip stamps cover motion, usages, HID_IN and native IN traffic. Device-chip stamps cover HID_OUT, OUT traffic, CONTROL, EMIT, BUS and CLIP_TRANSFER. The chips boot independently; a MediusTimeline puts both on your clock.

INSPECTORS
HelperDoes
medius_usage_event_is_held(&ev.data.usages, usage)bool: true if that usage (button, key, or media) is held.
medius_traffic_event_truncated(&ev.data.traffic)bool: true if len < true_len, meaning the matching entry's capture cut the packet.
medius_event_stream_dropped(stream)uint64_t: events dropped because the consumer fell behind (host-side back-pressure).

medius_event_stream_dropped counts what your consumer lost. MediusCatchState counts what the box dropped before sending: a box-wide total and a per-entry count, which names the expensive subscription.

Consume loopSubscribe, drain until disconnect, free

#include <medius.h>
#include <stdio.h>

/* every class at 16 bytes a packet, plus one vendor endpoint's IN traffic in full */
MediusCatchFilter filters[2] = {
    medius_catch_filter_with_capture(medius_catch_filter_everything(), 16),
    medius_catch_filter_inbound(
        medius_catch_filter_traffic(MEDIUS_CATCH_CLASS_VENDOR_INTERRUPT, 0x83)),
};

MediusEventStream *events = NULL;
if (medius_device_catch_events(dev, filters, 2, &events) != MEDIUS_STATUS_OK) {
    char msg[256];
    medius_last_error_message(msg, sizeof msg);
    fprintf(stderr, "subscribe failed: %s\n", msg);
    return 1;
}

MediusCatchEvent ev;
while (medius_event_stream_recv(events, &ev) == MEDIUS_STATUS_OK) {
    const char *dom = ev.clock == MEDIUS_CLOCK_DOMAIN_HOST_CHIP ? "host" : "device";
    switch (ev.kind) {
    case MEDIUS_CATCH_EVENT_KIND_MOTION:
        printf("[%s %u] motion dx=%d dy=%d dz=%d\n", dom, ev.ts_us,
               ev.data.motion.dx, ev.data.motion.dy, ev.data.motion.dz);
        break;
    case MEDIUS_CATCH_EVENT_KIND_USAGES:
        printf("[%s %u] held usages=%u  LMB=%d  W=%d\n", dom, ev.ts_us, ev.data.usages.n,
               medius_usage_event_is_held(&ev.data.usages, medius_usage_button(MEDIUS_BUTTON_LEFT)),
               medius_usage_event_is_held(&ev.data.usages, medius_usage_key(MEDIUS_KEY_W)));
        break;
    case MEDIUS_CATCH_EVENT_KIND_TRAFFIC:
        printf("[%s %u] class=%u id=0x%02X %u/%u bytes%s\n", dom, ev.ts_us,
               ev.data.traffic.class_, ev.data.traffic.id,
               ev.data.traffic.len, ev.data.traffic.true_len,
               medius_traffic_event_truncated(&ev.data.traffic) ? " (cut)" : "");
        break;
    }
}
/* recv returned MEDIUS_STATUS_ERR_DISCONNECTED: the box reset or the link dropped */
printf("dropped while behind: %llu\n",
       (unsigned long long)medius_event_stream_dropped(events));
medius_event_stream_free(events);

Decoded inputPress and release edges, not snapshots

The box reports held-usage snapshots; medius_device_input_events diffs them into edges, so your code tracks nothing between reports.

MediusStatus medius_device_input_events(struct MediusDevice *dev,
                                        const MediusCatchFilter *filters,
                                        uintptr_t n_filters,
                                        struct MediusInputStream **out);

Every filter must name an input class and cover both edges. Build them with medius_catch_filter_watch*, or take all four from medius_catch_filter_all_input.

FunctionDoes
medius_input_stream_recv(stream, &out)Block for the next MediusInputEvent; MEDIUS_STATUS_ERR_DISCONNECTED on close.
medius_input_stream_try_recv(stream, &out)bool: the next queued event, or false if none is queued or another thread is receiving (never blocks).
medius_input_stream_recv_timeout(stream, timeout_ms, &out)bool: false on timeout or close.
medius_input_stream_held(stream, class_, out, cap)Write that class's held usages into out[0..cap]; returns the count, which exceeds cap when the buffer was short.
medius_input_stream_dropped(stream)uint64_t: events the subscription dropped behind a slow consumer.
medius_input_stream_is_connected(stream)bool: false once the stream has closed.
medius_input_stream_close(stream)End the stream; a receive blocked on it returns at once. See Close.
medius_input_stream_free(stream)Release the handle; null is a no-op. No clone: the stream owns the held sets it diffs.
Refused withBecause the filter was
MEDIUS_STATUS_ERR_NOT_AN_INPUT_FILTERA traffic class, which arrives as bytes and decodes into no edge.
MEDIUS_STATUS_ERR_WILDCARD_NOT_INPUTmedius_catch_filter_everything, which covers traffic too.
MEDIUS_STATUS_ERR_HALF_EDGE_INPUT_FILTERNarrowed with _on_press or _on_release: one edge cannot be diffed into two.
MEDIUS_STATUS_ERR_INVALID_ARGCarrying a class_ or a direction byte no constant names. medius_device_catch_events refuses the same two.
EXAMPLE
MediusCatchFilter filters[4];
medius_catch_filter_all_input(filters);        /* buttons, keys, media, axes */

MediusInputStream *input = NULL;
if (medius_device_input_events(dev, filters, 4, &input) != MEDIUS_STATUS_OK)
    return 1;

MediusInputEvent ev;
while (medius_input_stream_recv(input, &ev) == MEDIUS_STATUS_OK) {
    switch (ev.kind) {
    case MEDIUS_INPUT_KIND_PRESS:
    case MEDIUS_INPUT_KIND_RELEASE:
        printf("[%u] %s usage %u:%u\n", ev.ts_us,
               ev.kind == MEDIUS_INPUT_KIND_PRESS ? "down" : "up  ",
               (unsigned)ev.usage.kind, (unsigned)ev.usage.id);
        break;
    case MEDIUS_INPUT_KIND_MOTION:
        printf("[%u] move dx=%d dy=%d dz=%d\n", ev.ts_us, ev.dx, ev.dy, ev.dz);
        break;
    }
}
medius_input_stream_free(input);

TimelinePut box stamps on this machine's clock

A catch stamp is microseconds on a chip's clock. It wraps every ~71.6 minutes and relates to no clock on this machine. A MediusTimeline maps it onto this machine's clock.

struct MediusTimeline *medius_timeline_new(void);
void     medius_timeline_free(struct MediusTimeline *t);
bool     medius_timeline_observe(struct MediusTimeline *t, const MediusCatchEvent *ev,
                                 uint64_t now_ns, MediusStamped *out);
bool     medius_timeline_observe_input(struct MediusTimeline *t, const MediusInputEvent *ev,
                                       uint64_t now_ns, MediusStamped *out);
void     medius_timeline_reset(struct MediusTimeline *t, MediusClockDomain domain);
uint64_t medius_timeline_samples(struct MediusTimeline *t, MediusClockDomain domain);

Feed every event in arrival order, with a monotonic reading as now_ns; the MediusStamped comes back on that scale.

CallDoes
medius_timeline_observe(t, &ev, now_ns, &out)Place one event; false on a null argument.
medius_timeline_observe_input(t, &ev, now_ns, &out)The same for a MediusInputEvent. One timeline orders events from both kinds of stream.
medius_timeline_reset(t, domain)Forget that domain's rollover count and measured floor, for a chip that rebooted.
medius_timeline_samples(t, domain)Events observed for a domain. The floor is a minimum over these, so a handful is a loose estimate.
EXAMPLE
MediusTimeline *tl = medius_timeline_new();

MediusCatchEvent ev;
while (medius_event_stream_recv(events, &ev) == MEDIUS_STATUS_OK) {
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    uint64_t now_ns = (uint64_t)now.tv_sec * 1000000000ull + (uint64_t)now.tv_nsec;

    MediusStamped at;
    if (medius_timeline_observe(tl, &ev, now_ns, &at))
        printf("%llu ns  (box %llu us, %llu ns of jitter)\n",
               (unsigned long long)at.host_ns,
               (unsigned long long)at.box_us,
               (unsigned long long)at.excess_ns);

    /* a chip reboot restarts its clock at zero (a device-chip restart raises
       MediusCountersSnapshot.restarts): call medius_timeline_reset(tl, domain) */
}
medius_timeline_free(tl);

CloseEnd a stream another thread is receiving on

A close ends the stream for every clone. A receive blocked on it in another thread returns at once. Free the handle after that thread is done with it. Closing one log stream leaves the others reading.

void medius_event_stream_close(struct MediusEventStream *stream);
void medius_input_stream_close(struct MediusInputStream *stream);
void medius_log_stream_close(struct MediusLogStream *stream);
EXAMPLE
#include <pthread.h>

static void *reader(void *arg) {
    MediusEventStream *events = arg;
    MediusCatchEvent ev;
    while (medius_event_stream_recv(events, &ev) == MEDIUS_STATUS_OK) {
        /* ... */
    }
    return NULL;                        /* MEDIUS_STATUS_ERR_DISCONNECTED */
}

pthread_t t;
pthread_create(&t, NULL, reader, events);
/* ... */
medius_event_stream_close(events);      /* the blocked recv returns at once */
pthread_join(t, NULL);
medius_event_stream_free(events);       /* no thread is using it now */

No asyncBuild async on the non-blocking reads

The C ABI is synchronous, with no async API. Poll with medius_event_stream_try_recv, bound a block with medius_event_stream_recv_timeout, or run the blocking recv loop on a separate thread. Catch and log handles clone; an input stream does not.