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

297
dev/cli/query_cli.c Normal file
View File

@@ -0,0 +1,297 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <semaphore.h>
#include <stdatomic.h>
#include <pthread.h>
#include "discovery.h"
#include "transport.h"
#include "protocol.h"
#include "error.h"
/* -------------------------------------------------------------------------
* Discovery — wait for first matching node
* ------------------------------------------------------------------------- */
struct Discovery_Wait {
struct Discovery_Peer peer;
atomic_int found;
};
static void on_peer_found(const struct Discovery_Peer *peer, void *userdata) {
struct Discovery_Wait *w = userdata;
if (atomic_load(&w->found)) { return; }
w->peer = *peer;
atomic_store(&w->found, 1);
}
static int wait_for_node(struct Discovery_Peer *peer_out,
const char *self_name, int timeout_ms)
{
struct Discovery_Wait w;
memset(&w, 0, sizeof(w));
atomic_init(&w.found, 0);
struct Discovery_Config cfg = {
.site_id = 0,
.tcp_port = 0,
.function_flags = 0,
.name = self_name,
.interval_ms = 2000,
.timeout_intervals= 3,
.on_peer_found = on_peer_found,
.userdata = &w,
};
struct Discovery *disc;
struct App_Error e = discovery_create(&disc, &cfg);
if (!APP_IS_OK(e)) { app_error_print(&e); return -1; }
e = discovery_start(disc);
if (!APP_IS_OK(e)) { app_error_print(&e); discovery_destroy(disc); return -1; }
printf("waiting for a node (timeout %d ms)...\n", timeout_ms);
int elapsed = 0;
while (!atomic_load(&w.found) && elapsed < timeout_ms) {
usleep(100000);
elapsed += 100;
}
discovery_destroy(disc);
if (!atomic_load(&w.found)) {
fprintf(stderr, "no node found within %d ms\n", timeout_ms);
return -1;
}
*peer_out = w.peer;
return 0;
}
/* -------------------------------------------------------------------------
* Response handling — semaphore-based synchronisation
* ------------------------------------------------------------------------- */
struct Query_State {
sem_t sem;
uint16_t last_request_id;
int done;
};
/* -------------------------------------------------------------------------
* ENUM_DEVICES callbacks
* ------------------------------------------------------------------------- */
static void on_media_device(
const char *path, uint8_t path_len,
const char *driver, uint8_t driver_len,
const char *model, uint8_t model_len,
const char *bus_info, uint8_t bus_info_len,
uint8_t vcount,
void *ud)
{
(void)ud;
printf(" media %.*s driver=%.*s model=%.*s bus=%.*s (%u video node(s))\n",
(int)path_len, path,
(int)driver_len, driver,
(int)model_len, model,
(int)bus_info_len, bus_info,
(unsigned)vcount);
}
static void caps_str(uint32_t caps, char *buf, size_t len) {
/* Build a compact comma-separated list of the relevant V4L2_CAP_* bits. */
static const struct { uint32_t bit; const char *name; } flags[] = {
{ 0x00000001u, "video-capture" },
{ 0x00000002u, "video-output" },
{ 0x00000010u, "vbi-capture" },
{ 0x00800000u, "meta-capture" },
{ 0x04000000u, "streaming" },
};
buf[0] = '\0';
size_t pos = 0;
for (size_t i = 0; i < sizeof(flags)/sizeof(flags[0]); i++) {
if (!(caps & flags[i].bit)) { continue; }
int n = snprintf(buf + pos, len - pos, "%s%s",
pos ? "," : "", flags[i].name);
if (n < 0 || (size_t)n >= len - pos) { break; }
pos += (size_t)n;
}
}
static void on_video_node(
const char *path, uint8_t path_len,
const char *ename, uint8_t ename_len,
uint32_t etype, uint32_t eflags,
uint32_t dcaps,
uint8_t pflags, uint8_t is_capture,
void *ud)
{
(void)eflags; (void)pflags; (void)ud;
char caps[128];
caps_str(dcaps, caps, sizeof(caps));
printf(" video %.*s entity=%.*s type=0x%08x caps=[%s]%s\n",
(int)path_len, path,
(int)ename_len, ename,
etype, caps,
is_capture ? " [capture]" : "");
}
static void on_standalone(
const char *path, uint8_t path_len,
const char *name, uint8_t name_len,
void *ud)
{
(void)ud;
printf(" standalone %.*s card=%.*s\n",
(int)path_len, path,
(int)name_len, name);
}
/* -------------------------------------------------------------------------
* ENUM_CONTROLS callbacks
* ------------------------------------------------------------------------- */
static void on_control(
uint32_t id, uint8_t type, uint32_t flags,
const char *name, uint8_t name_len,
int32_t min, int32_t max, int32_t step,
int32_t default_val, int32_t current_val,
uint8_t menu_count, void *ud)
{
(void)flags; (void)ud;
printf(" ctrl id=0x%08x type=%u %.*s"
" min=%d max=%d step=%d default=%d current=%d",
id, type,
(int)name_len, name,
min, max, step, default_val, current_val);
if (menu_count) { printf(" (%u menu items)", (unsigned)menu_count); }
printf("\n");
}
/* -------------------------------------------------------------------------
* Frame handler
* ------------------------------------------------------------------------- */
struct Frame_State {
struct Query_State *q;
uint16_t pending_cmd; /* command we expect a response for */
uint16_t device_count; /* flat video node count from ENUM_DEVICES */
};
static void on_frame(struct Transport_Conn *conn,
struct Transport_Frame *frame, void *userdata)
{
(void)conn;
struct Frame_State *fs = userdata;
if (frame->message_type == PROTO_MSG_CONTROL_RESPONSE) {
switch (fs->pending_cmd) {
case PROTO_CMD_ENUM_DEVICES: {
struct Proto_Response_Header hdr;
struct App_Error e = proto_read_enum_devices_response(
frame->payload, frame->payload_length, &hdr,
on_media_device, on_video_node, on_standalone, NULL);
if (!APP_IS_OK(e)) { app_error_print(&e); }
else if (hdr.status != PROTO_STATUS_OK) {
fprintf(stderr, "ENUM_DEVICES failed: status=%u\n", hdr.status);
}
break;
}
case PROTO_CMD_ENUM_CONTROLS: {
struct Proto_Response_Header hdr;
struct App_Error e = proto_read_enum_controls_response(
frame->payload, frame->payload_length, &hdr,
on_control, NULL, NULL);
if (!APP_IS_OK(e)) { app_error_print(&e); }
else if (hdr.status != PROTO_STATUS_OK) {
fprintf(stderr, "ENUM_CONTROLS failed: status=%u\n", hdr.status);
}
break;
}
default:
break;
}
sem_post(&fs->q->sem);
}
free(frame->payload);
}
static void on_disconnect(struct Transport_Conn *conn, void *userdata) {
(void)conn; (void)userdata;
printf("disconnected\n");
}
/* -------------------------------------------------------------------------
* Entry point
* ------------------------------------------------------------------------- */
static void usage(void) {
fprintf(stderr,
"usage: query_cli [--timeout ms] [--controls device_index]\n"
"\n"
" Discovers a video node on the LAN and queries its devices.\n"
" --timeout ms discovery timeout in ms (default 5000)\n"
" --controls idx also enumerate controls for device at index idx\n");
}
int main(int argc, char **argv) {
int timeout_ms = 5000;
int query_controls = -1; /* -1 = don't query controls */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--timeout") == 0 && i + 1 < argc) {
timeout_ms = atoi(argv[++i]);
} else if (strcmp(argv[i], "--controls") == 0 && i + 1 < argc) {
query_controls = atoi(argv[++i]);
} else if (strcmp(argv[i], "--help") == 0) {
usage(); return 0;
}
}
/* Discover a node */
struct Discovery_Peer peer;
if (wait_for_node(&peer, "query_cli:0", timeout_ms) != 0) { return 1; }
char addr_str[INET_ADDRSTRLEN];
struct in_addr in = { .s_addr = peer.addr };
inet_ntop(AF_INET, &in, addr_str, sizeof(addr_str));
printf("found node: %s addr=%s port=%u\n",
peer.name, addr_str, peer.tcp_port);
/* Set up connection state */
struct Query_State q;
sem_init(&q.sem, 0, 0);
struct Frame_State fs = { .q = &q, .pending_cmd = 0, .device_count = 0 };
struct Transport_Conn *conn;
struct App_Error e = transport_connect(&conn, addr_str, peer.tcp_port,
16 * 1024 * 1024, on_frame, on_disconnect, &fs);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
/* ENUM_DEVICES */
printf("\ndevices:\n");
fs.pending_cmd = PROTO_CMD_ENUM_DEVICES;
e = proto_write_enum_devices(conn, 1);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
sem_wait(&q.sem);
/* ENUM_CONTROLS for a specific device if requested */
if (query_controls >= 0) {
printf("\ncontrols for device %d:\n", query_controls);
fs.pending_cmd = PROTO_CMD_ENUM_CONTROLS;
e = proto_write_enum_controls(conn, 2, (uint16_t)query_controls);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
sem_wait(&q.sem);
}
transport_conn_close(conn);
sem_destroy(&q.sem);
return 0;
}