Streams
Read live input and logs in CThree live channels from the box: raw catch events, the same input decoded into edges, and device log lines (Logs & counters). Subscribe, then pull fixed-size POD events off the handle.
medius_device_catch_events(dev, filters, n, &stream) ── raw events, every class
medius_device_input_events(dev, filters, n, &stream) ── decoded press/release edges
medius_device_logs(dev, &stream) ── device log lines
│
▼ a background reader thread fills a host-side queue
medius_event_stream_recv(stream, &event) ── pull one (blocks)
medius_input_stream_recv(stream, &event) ── the same, decoded
medius_log_stream_recv(stream, &line) ── the same, for logs
│
▼ loop until MEDIUS_STATUS_ERR_DISCONNECTED
medius_event_stream_free(stream) ── unsubscribe
medius_input_stream_free(stream) ── the same, decoded
medius_log_stream_free(stream) ── the same, for logsSubscribe
Open an event or log streamBoth return a MediusStatus and write an opaque handle through an out-param. A catch subscription is an array of MediusCatchFilter entries, each built by a medius_catch_filter_* helper. The array is read during the call and not retained, so it can live on the stack.
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);| Call | Does |
|---|---|
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. |
| Field | Value | What it selects |
|---|---|---|
class_ | a MEDIUS_CATCH_CLASS_* | The address space, and which event arm the matches arrive on. |
id | class-specific, or MEDIUS_CATCH_ID_ANY | Which button, usage, axis, interface, or endpoint. ID_ANY is one blanket entry, not an expansion. |
direction | a MEDIUS_DIRECTION_* | The press/release edge for an input class; the transfer direction for a traffic class (POSITIVE = IN, NEGATIVE = OUT). |
capture | 0 to 255 | Bytes kept per event, 0 = the whole packet. Traffic classes only: an input class carries no packet. |
| Constant | Value | Subscribes to | Arrives as |
|---|---|---|---|
MEDIUS_CATCH_CLASS_BTN | 0 | mouse buttons | data.usages |
MEDIUS_CATCH_CLASS_KEY | 1 | keyboard keys and modifiers | |
MEDIUS_CATCH_CLASS_MEDIA | 2 | media (Consumer) usages | |
MEDIUS_CATCH_CLASS_AXIS | 3 | X, Y, and the wheel | data.motion |
MEDIUS_CATCH_CLASS_HID_IN | 4 | a cloned HID interface's reports | data.traffic |
MEDIUS_CATCH_CLASS_HID_OUT | 5 | an interrupt-OUT endpoint | |
MEDIUS_CATCH_CLASS_VENDOR_INTERRUPT | 6 | a vendor interrupt endpoint | |
MEDIUS_CATCH_CLASS_VENDOR_BULK | 7 | a vendor bulk endpoint | |
MEDIUS_CATCH_CLASS_CONTROL | 8 | a control endpoint, one event per completed transaction | |
MEDIUS_CATCH_CLASS_EMIT | 9 | what the clone actually put on the wire | |
MEDIUS_CATCH_CLASS_BUS | 10 | resets, suspends, configuration and attach changes | |
MEDIUS_CATCH_CLASS_ANY | 0xFF | every class at once (id must be MEDIUS_CATCH_ID_ANY) | any arm |
MediusCatchFilter filters[3] = {
/* every physical 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. Asking for more, or for a filter the box cannot honour, fails the whole call with its own MediusStatus rather than quietly narrowing the subscription.
The control link runs at 4 Mbaud and vendor bulk alone measures 250 KiB/s through the box, so subscribing to everything at full length cannot be delivered.
Delivery is four strict-priority queues (input and bus, then the other traffic classes, then control, then vendor bulk), and bulk can starve entirely under a busy mouse. Use capture to buy headroom.
Receive
Pull one event off the queueThere's no iterator. Loop a receive call until it returns MEDIUS_STATUS_ERR_DISCONNECTED (the stream closes after a reset or link loss). Each writes one event through *out.
| Function | Returns | Blocks? |
|---|---|---|
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 |
| Function | Returns |
|---|---|
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) |
Event objects
Fixed-size PODs, nothing to free per eventmedius_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).
Every variable-length payload is an inline array with a count beside it, never a pointer: the usage list caps at MEDIUS_MAX_USAGES (256), a captured packet at MEDIUS_MAX_TRAFFIC_BYTES (180). An event you copy stays valid, and 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; }; // cursor + wheel 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 address / 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 is | Read | Fields |
|---|---|---|
MEDIUS_CATCH_EVENT_KIND_MOTION | data.motion | dx, dy, dz (cursor and wheel deltas) |
MEDIUS_CATCH_EVENT_KIND_USAGES | data.usages | usages[0..n], each a class-tagged MediusUsage |
MEDIUS_CATCH_EVENT_KIND_TRAFFIC | data.traffic | class_, id, direction, flags, and bytes[0..len] of a true_len-byte packet |
Host-chip stamps cover motion, usages, HID_IN and IN traffic; device-chip stamps cover HID_OUT, OUT traffic, CONTROL, EMIT and BUS. The two chips boot independently, so put both on your own clock with a MediusTimeline.
| Helper | Does |
|---|---|
medius_usage_event_is_held(&ev.data.usages, usage) | bool: true if that MediusUsage usage (button, key, or media) is held. |
medius_traffic_event_truncated(&ev.data.traffic) | bool: true if len < true_len, so the packet was cut at the matching entry's capture. Without it a cut packet and a genuinely short one read identically. |
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. What the box shed before it ever reached the wire is on MediusCatchState: a box-wide total, plus a per-entry count so you can tell which subscription is the expensive one.
Consume loop
Subscribe, drain until disconnect, free#include <medius.h>
#include <stdio.h>
/* a blanket over 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 input
Press and release edges, not held setsThe box reports held-usage snapshots. medius_device_input_events diffs them into edges, so nothing on your side has to remember what was down last report.
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.
| Function | Does |
|---|---|
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 (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 currently held usages into out[0..cap]; returns how many there are. A return above cap means the buffer was short. |
medius_input_stream_dropped(stream) | uint64_t: events the subscription dropped behind a slow consumer. |
medius_input_stream_free(stream) | Release the handle. Null is a no-op; this stream has no clone, because it owns the held sets it diffs. |
| Refused with | Because the filter was |
|---|---|
MEDIUS_STATUS_ERR_NOT_AN_INPUT_FILTER | A traffic class, which arrives as bytes and decodes into no edge. |
MEDIUS_STATUS_ERR_WILDCARD_NOT_INPUT | medius_catch_filter_everything, which covers traffic too. |
MEDIUS_STATUS_ERR_HALF_EDGE_INPUT_FILTER | Narrowed with _on_press or _on_release: one edge cannot be diffed into two. |
MEDIUS_STATUS_ERR_INVALID_ARG | Carrying a class_ or a direction byte no constant names. medius_device_catch_events refuses the same two. |
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);Timeline
Put box stamps on your own clockA catch stamp is microseconds on a chip that booted before your process did: it wraps every ~71.6 minutes and relates to nothing here. A MediusTimeline maps it.
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);
void medius_timeline_reset(struct MediusTimeline *t, MediusClockDomain domain);
uint64_t medius_timeline_samples(struct MediusTimeline *t, MediusClockDomain domain);Feed every event in as it arrives, in order, with your own monotonic reading as now_ns. A MediusStamped comes back on that same scale.
| Call | Does |
|---|---|
medius_timeline_observe(t, &ev, now_ns, &out) | Place one event; false on a null argument. |
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. |
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, which no bus event announces:
call medius_timeline_reset(tl, domain) when you know one happened. */
}
medius_timeline_free(tl);