A robot in C
The C ABI is the boundary every non-Rust form factor sits on (ADR 0004). sdks/c/include/seyd.h is generated by cbindgen from packages/seyd-ffi and is checked in, so a C consumer needs no Rust toolchain once the library is built. Any language with a C FFI can use the header today; the Python SDK is one such consumer. C++ and ROS 2 wrappers are planned, not built.
Build and link
Section titled “Build and link”cargo build -p seyd-ffi --release # target/release/libseyd.{dylib,so,a}make -C sdks/c check # builds and runs the ABI conformance testmake -C sdks/c # builds every example into sdks/c/build/Link against libseyd (cdylib) or libseyd.a (staticlib). The library pulls in the platform’s TLS, threading and network libraries, and the Makefile is the authoritative per-platform list:
CFLAGS += -std=c11 -Wall -Wextra -O2 -I includeLDFLAGS += -L $(TARGET) -lseydifeq ($(UNAME),Darwin) LDFLAGS += -framework CoreFoundation -framework Security -framework SystemConfigurationelse LDFLAGS += -lpthread -ldl -lmendifAt run time the dynamic library must be findable: the Makefile sets DYLD_LIBRARY_PATH or LD_LIBRARY_PATH to the target directory when it runs the check.
The lifecycle
Section titled “The lifecycle”The header’s own summary, in order:
seyd_agent_create -> seyd_channel_add* -> seyd_agent_start -> seyd_push_frame / seyd_push_message -> seyd_agent_stop -> seyd_agent_destroyseyd_agent_createtakes aseyd_configand aseyd_callbacksand does no network work. Strings are borrowed for the call only; Seyd copies what it keeps.seyd_channel_addis valid only before start. Channels are numbered from 1 in the order added, and that numbering is what the pilot sees.seyd_agent_startbinds, discovers candidates and announces. It blocks until the robot is announced, up to a second or two. Callbacks begin after it returnsSEYD_OK.seyd_agent_stoptears down sessions and blocks until the callback thread has finished, so no callback runs after it returns.seyd_agent_destroystops if running and frees the handle.
Every fallible call returns a seyd_status; SEYD_OK is zero.
The example: a sensor robot in one file
Section titled “The example: a sensor robot in one file”sdks/c/examples/sensor-robot.c announces itself, publishes a counter on a sensor channel at 10 Hz and prints whatever a pilot sends on the command channel. It is built by make -C sdks/c, so the page cannot show code that does not compile.
// Copyright 2026 Anton Gravestam// SPDX-License-Identifier: Apache-2.0/* sensor-robot.c — a complete Seyd robot in C, in one file. * * Announces itself to the signal cloud, publishes a counter on a sensor * channel at 10 Hz, and prints whatever a pilot sends on the command channel. * It is the C twin of sim/sensor-source.py, and the smallest honest answer to * "what does integrating Seyd look like?". * * ./sensor-robot <robot_id> <signal_url> [credential_path] * * Add video by calling seyd_push_frame with encoded access units from * whatever encoder the robot already has — Seyd never decodes or re-encodes. */
#include <signal.h>#include <stdatomic.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <time.h>
#include "seyd.h"
static volatile atomic_bool running = true;
static void on_sigint(int _sig) { (void)_sig; atomic_store(&running, false);}
/* Callbacks arrive on one Seyd thread and must not block: print and return. */
static void on_session_started(void *user, const char *session_id, seyd_role role, const char *path_label) { (void)user; printf("session %s started (%s over %s)\n", session_id, role == SEYD_ROLE_DRIVER ? "driver" : "observer", path_label);}
static void on_session_ended(void *user, const char *session_id, const char *reason) { (void)user; /* Park actuators here — this fires whether the pilot said goodbye or * simply vanished. */ printf("session %s ended (%s)\n", session_id, reason);}
static void on_command(void *user, uint8_t channel, const uint8_t *data, size_t len) { (void)user; printf("command on channel %u: %.*s\n", channel, (int)len, (const char *)data);}
static void on_requested_config(void *user, const char *json) { (void)user; /* A real robot reconfigures its encoder here. */ printf("requested config: %s\n", json);}
static void on_signal_state(void *user, seyd_signal_state state, const char *detail) { (void)user; const char *name = state == SEYD_SIGNAL_CONNECTED ? "connected" : state == SEYD_SIGNAL_DISCONNECTED ? "disconnected" : "denied"; printf("signal %s%s%s\n", name, detail ? ": " : "", detail ? detail : "");}
static uint64_t now_us(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (uint64_t)ts.tv_sec * 1000000u + (uint64_t)ts.tv_nsec / 1000u;}
int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "usage: %s <robot_id> <signal_url> [credential_path]\n", argv[0]); return 2; } signal(SIGINT, on_sigint); seyd_init_logging("info");
seyd_config cfg = { .robot_id = argv[1], .signal_url = argv[2], .credential_path = argc > 3 ? argv[3] : "./robot.key", .quic_port = 4433, .ipv6 = true, .port_mapping = true, .qos_profile = "latency", .max_sessions = 4, }; seyd_callbacks cbs = { .on_session_started = on_session_started, .on_session_ended = on_session_ended, .on_command = on_command, .on_requested_config = on_requested_config, .on_signal_state = on_signal_state, };
seyd_agent *agent = NULL; if (seyd_agent_create(&cfg, &cbs, &agent) != SEYD_OK) { fprintf(stderr, "create: %s\n", seyd_last_error()); return 1; }
uint8_t telemetry = 0, drive = 0; seyd_channel_config sensor = {.kind = SEYD_CHANNEL_SENSOR, .name = "telemetry"}; seyd_channel_config command = {.kind = SEYD_CHANNEL_COMMAND, .name = "drive"}; if (seyd_channel_add(agent, &sensor, &telemetry) != SEYD_OK || seyd_channel_add(agent, &command, &drive) != SEYD_OK) { fprintf(stderr, "channel_add: %s\n", seyd_last_error()); seyd_agent_destroy(agent); return 1; }
/* Blocks while candidates are discovered and the robot is announced. */ if (seyd_agent_start(agent) != SEYD_OK) { fprintf(stderr, "start: %s\n", seyd_last_error()); seyd_agent_destroy(agent); return 1; } printf("robot %s online; pilots may connect\n", cfg.robot_id);
for (uint64_t tick = 0; atomic_load(&running); tick++) { char json[128]; int n = snprintf(json, sizeof json, "{\"seq\":%llu,\"t_us\":%llu}", (unsigned long long)tick, (unsigned long long)now_us()); seyd_push_message(agent, telemetry, (const uint8_t *)json, (size_t)n); struct timespec hz10 = {.tv_sec = 0, .tv_nsec = 100 * 1000 * 1000}; nanosleep(&hz10, NULL); }
printf("\nstopping\n"); seyd_counters_t c = {0}; if (seyd_counters(agent, &c) == SEYD_OK) { printf("sent %llu chunks, %llu bytes\n", (unsigned long long)c.chunks_sent, (unsigned long long)c.bytes_sent); } seyd_agent_destroy(agent); return 0;}Walking through it:
- Callbacks come first. Each prints and returns;
on_session_endedis where a real robot parks its actuators,on_requested_configwhere it reconfigures its encoder.on_signal_statereports connected, disconnected or denied, and the robot is unreachable while signaling is down. seyd_init_logging("info")installs a stderr log filtered by a directive such asinfoorseyd_core=debug. Optional, at most once, before creating an agent.seyd_configsets the robot id, the signal URL and the credential path fromargv; a zeroquic_portormax_sessionsand a NULLqos_profiletake the same defaults asseydd.toml(4433, 4,balanced).seyd_channel_addtwice: atelemetrysensor channel and adrivecommand channel. The id written toout_channel_idis whatseyd_push_messagetakes.seyd_agent_start, then the main loop pushes a JSON counter once every 100 ms until SIGINT.seyd_counterson the way out snapshots the engine counters;seyd_agent_destroyreleases everything.
Adding video
Section titled “Adding video”Add a video channel with a WebCodecs codec string and the nominal frame rate, then hand it one encoded access unit per call:
uint8_t video;seyd_channel_config vc = {.kind = SEYD_CHANNEL_VIDEO, .name = "main", .codec = "avc1.42001f", .fps = 25};seyd_channel_add(agent, &vc, &video);
seyd_agent_start(agent);for (;;) seyd_push_frame(agent, video, au, au_len, is_idr, capture_ts_us);seyd_push_frame never blocks. A frame that arrives while the previous one is still going out is dropped by admission control and counted in frames_dropped_backlog; keyframes are never dropped. capture_ts_us is the source’s capture clock and travels to the pilot, which paces presentation on it (ADR 0005), so pass the encoder’s timestamp rather than “now”. Seyd is handed encoded access units and relays them; it never decodes, re-encodes or inspects them, and whatever your encoder produced is what the pilot decodes. What the encoder must do is on The publisher contract and Encoder setup. Where the publisher can encode the same picture more than once, declare layers; see Simulcast.
Set on_requested_config and on_recovery_request. The first carries Seyd’s targets as the video-config JSON (maxBitrateKbps, latencyBudgetMs, maxGopMs, preferIntraRefresh, suggestedFps, reason); the second names the recovery point a pilot needs (ltr, intra_refresh or idr). Keyframes are on demand (ADR 0009): honour the request and treat maxGopMs as a long ceiling, not a cadence.
Threading and error rules
Section titled “Threading and error rules”- Callbacks run on one dedicated Seyd thread and must not block. A slow callback delays every later event. Queue the work and return. Pointers passed to a callback are valid only for the duration of the call.
seyd_push_*never blocks and may be called from any thread.- Check
seyd_last_error()after a non-SEYD_OKstatus. It is thread-local and valid until that thread’s next failing call; never free it. SEYD_ERR_STATEmeans a call in the wrong lifecycle state, such asseyd_channel_addafter start.SEYD_ERR_BACKPRESSUREis reserved and not returned: the engine drops under pressure by design and reports it through the counters.- The ABI is versioned. Compare
seyd_abi_version()with theSEYD_ABI_VERSIONyou compiled against. Additions are appended, never reordered, until the version is bumped.
Checking the ABI
Section titled “Checking the ABI”make -C sdks/c checkexamples/abi-smoke.c is the conformance test: header matches library, channel numbering, lifecycle enforcement, argument validation. No network. Run it whenever the library or the header changes.
The C ABI reference lists every status code, struct, callback and function, generated from the header.