Add protocol module, video-node binary, query/web CLI tools

- Protocol module: framed binary encoding for control requests/responses
  (ENUM_DEVICES, ENUM_CONTROLS, GET/SET_CONTROL, STREAM_OPEN/CLOSE)
- video-node: scans /dev/media* and /dev/video*, serves V4L2 device
  topology and controls over TCP; uses UDP discovery for peer announce
- query_cli: auto-discovers a node, queries devices and controls
- protocol_cli: low-level protocol frame decoder for debugging
- dev/web: Express 5 ESM web inspector — live SSE discovery picker,
  REST bridge to video-node, controls UI with sliders/selects/checkboxes
- Makefile: sequential module builds before cli/node to fix make -j races
- common.mk: add DEPFLAGS (-MMD -MP) for automatic header dependencies
- All module Makefiles: split compile/link, generate .d dependency files
- discovery: replace 100ms poll loop with pthread_cond_timedwait;
  respond to all announcements (not just new peers) for instant re-discovery
- ENUM_DEVICES response: carry device_caps (V4L2_CAP_*) per video node
  so clients can distinguish capture nodes from metadata nodes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 01:04:56 +00:00
parent 34386b635e
commit 62c25247ef
32 changed files with 3998 additions and 81 deletions

View File

@@ -40,7 +40,9 @@ struct Discovery {
pthread_t announce_thread;
pthread_t receive_thread;
atomic_int running;
atomic_int early_announce; /* set when a new peer is seen */
pthread_mutex_t announce_mutex;
pthread_cond_t announce_cond; /* signaled to wake announce thread early */
pthread_mutex_t peers_mutex;
struct Peer_Entry peers[DISCOVERY_MAX_PEERS];
@@ -131,23 +133,26 @@ static void *announce_thread_fn(void *arg) {
send_announcement(d);
pthread_mutex_lock(&d->announce_mutex);
while (atomic_load(&d->running)) {
/* sleep in 100 ms increments; breaks early if a new peer is detected
* or if destroy is called */
uint32_t elapsed = 0;
while (atomic_load(&d->running) && elapsed < d->config.interval_ms) {
if (atomic_load(&d->early_announce)) { break; }
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100 * 1000000L };
nanosleep(&ts, NULL);
elapsed += 100;
struct timespec abs;
clock_gettime(CLOCK_REALTIME, &abs);
uint32_t ms = d->config.interval_ms;
abs.tv_sec += ms / 1000u;
abs.tv_nsec += (long)(ms % 1000u) * 1000000L;
if (abs.tv_nsec >= 1000000000L) {
abs.tv_sec++;
abs.tv_nsec -= 1000000000L;
}
/* blocks until signaled (new peer / shutdown) or interval elapses */
pthread_cond_timedwait(&d->announce_cond, &d->announce_mutex, &abs);
if (!atomic_load(&d->running)) { break; }
atomic_store(&d->early_announce, 0);
send_announcement(d);
check_timeouts(d);
}
pthread_mutex_unlock(&d->announce_mutex);
return NULL;
}
@@ -228,11 +233,14 @@ static void *receive_thread_fn(void *arg) {
pthread_mutex_unlock(&d->peers_mutex);
if (is_new) {
atomic_store(&d->early_announce, 1);
if (d->config.on_peer_found) {
d->config.on_peer_found(&peer_copy, d->config.userdata);
}
/* respond to every announcement — the sender may be a fresh instance
* that doesn't know about us yet even if we already have it in our table */
pthread_mutex_lock(&d->announce_mutex);
pthread_cond_signal(&d->announce_cond);
pthread_mutex_unlock(&d->announce_mutex);
if (is_new && d->config.on_peer_found) {
d->config.on_peer_found(&peer_copy, d->config.userdata);
}
}
@@ -262,7 +270,8 @@ struct App_Error discovery_create(struct Discovery **out,
inet_pton(AF_INET, DISCOVERY_MULTICAST_GROUP, &d->mcast_addr.sin_addr);
atomic_init(&d->running, 0);
atomic_init(&d->early_announce, 0);
pthread_mutex_init(&d->announce_mutex, NULL);
pthread_cond_init(&d->announce_cond, NULL);
pthread_mutex_init(&d->peers_mutex, NULL);
*out = d;
@@ -326,9 +335,15 @@ struct App_Error discovery_start(struct Discovery *d) {
void discovery_destroy(struct Discovery *d) {
atomic_store(&d->running, 0);
/* wake announce thread so it exits without waiting for the full interval */
pthread_mutex_lock(&d->announce_mutex);
pthread_cond_signal(&d->announce_cond);
pthread_mutex_unlock(&d->announce_mutex);
close(d->sock);
pthread_join(d->announce_thread, NULL);
pthread_join(d->receive_thread, NULL);
pthread_cond_destroy(&d->announce_cond);
pthread_mutex_destroy(&d->announce_mutex);
pthread_mutex_destroy(&d->peers_mutex);
free(d);
}