Files
video-setup/dev/cli/protocol_cli.c
mikael-lovqvists-claude-agent 62c25247ef 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>
2026-03-27 01:04:56 +00:00

251 lines
7.3 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "protocol.h"
#include "transport.h"
#include "error.h"
/* -- frame decoder --------------------------------------------------------- */
static const char *cmd_name(uint16_t cmd) {
switch (cmd) {
case PROTO_CMD_STREAM_OPEN: return "STREAM_OPEN";
case PROTO_CMD_STREAM_CLOSE: return "STREAM_CLOSE";
case PROTO_CMD_ENUM_DEVICES: return "ENUM_DEVICES";
case PROTO_CMD_ENUM_CONTROLS: return "ENUM_CONTROLS";
case PROTO_CMD_GET_CONTROL: return "GET_CONTROL";
case PROTO_CMD_SET_CONTROL: return "SET_CONTROL";
case PROTO_CMD_ENUM_MONITORS: return "ENUM_MONITORS";
default: return "unknown";
}
}
static const char *event_name(uint8_t code) {
switch (code) {
case PROTO_EVENT_INTERRUPTED: return "INTERRUPTED";
case PROTO_EVENT_RESUMED: return "RESUMED";
default: return "unknown";
}
}
static const char *status_name(uint16_t s) {
switch (s) {
case PROTO_STATUS_OK: return "OK";
case PROTO_STATUS_ERROR: return "ERROR";
case PROTO_STATUS_UNKNOWN_CMD: return "UNKNOWN_CMD";
case PROTO_STATUS_INVALID_PARAMS: return "INVALID_PARAMS";
case PROTO_STATUS_NOT_FOUND: return "NOT_FOUND";
default: return "unknown";
}
}
static void decode_and_print(struct Transport_Frame *frame) {
switch (frame->message_type) {
case PROTO_MSG_VIDEO_FRAME: {
struct Proto_Video_Frame vf;
struct App_Error e = proto_read_video_frame(
frame->payload, frame->payload_length, &vf);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf("VIDEO_FRAME stream_id=%u data_len=%u\n",
vf.stream_id, vf.data_len);
break;
}
case PROTO_MSG_STREAM_EVENT: {
struct Proto_Stream_Event ev;
struct App_Error e = proto_read_stream_event(
frame->payload, frame->payload_length, &ev);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf("STREAM_EVENT stream_id=%u event=%s\n",
ev.stream_id, event_name(ev.event_code));
break;
}
case PROTO_MSG_CONTROL_REQUEST: {
struct Proto_Request_Header hdr;
struct App_Error e = proto_read_request_header(
frame->payload, frame->payload_length, &hdr);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf("CONTROL_REQUEST request_id=%u command=%s\n",
hdr.request_id, cmd_name(hdr.command));
switch (hdr.command) {
case PROTO_CMD_STREAM_OPEN: {
struct Proto_Stream_Open so;
e = proto_read_stream_open(frame->payload, frame->payload_length, &so);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf(" stream_id=%u format=0x%04x pixel_format=0x%04x origin=0x%04x\n",
so.stream_id, so.format, so.pixel_format, so.origin);
break;
}
case PROTO_CMD_STREAM_CLOSE: {
struct Proto_Stream_Close sc;
e = proto_read_stream_close(frame->payload, frame->payload_length, &sc);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf(" stream_id=%u\n", sc.stream_id);
break;
}
case PROTO_CMD_ENUM_CONTROLS: {
struct Proto_Enum_Controls_Req req;
e = proto_read_enum_controls_req(frame->payload, frame->payload_length, &req);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf(" device_index=%u\n", req.device_index);
break;
}
case PROTO_CMD_GET_CONTROL: {
struct Proto_Get_Control_Req req;
e = proto_read_get_control_req(frame->payload, frame->payload_length, &req);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf(" device_index=%u control_id=%u\n",
req.device_index, req.control_id);
break;
}
case PROTO_CMD_SET_CONTROL: {
struct Proto_Set_Control_Req req;
e = proto_read_set_control_req(frame->payload, frame->payload_length, &req);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf(" device_index=%u control_id=%u value=%d\n",
req.device_index, req.control_id, req.value);
break;
}
default:
break;
}
break;
}
case PROTO_MSG_CONTROL_RESPONSE: {
struct Proto_Response_Header hdr;
struct App_Error e = proto_read_response_header(
frame->payload, frame->payload_length, &hdr);
if (!APP_IS_OK(e)) { app_error_print(&e); return; }
printf("CONTROL_RESPONSE request_id=%u status=%s\n",
hdr.request_id, status_name(hdr.status));
/* extra bytes are command-specific; caller must know the command */
break;
}
default:
printf("unknown message_type=0x%04x payload_length=%u\n",
frame->message_type, frame->payload_length);
break;
}
}
/* -- server mode ----------------------------------------------------------- */
static void server_on_frame(struct Transport_Conn *conn,
struct Transport_Frame *frame, void *ud)
{
(void)conn; (void)ud;
decode_and_print(frame);
free(frame->payload);
}
static void server_on_connect(struct Transport_Conn *conn, void *ud) {
(void)conn; (void)ud;
printf("client connected\n");
}
static void server_on_disconnect(struct Transport_Conn *conn, void *ud) {
(void)conn; (void)ud;
printf("client disconnected\n");
}
static int run_server(uint16_t port) {
struct Transport_Server_Config cfg = {
.port = port,
.max_connections = 8,
.max_payload = 16 * 1024 * 1024,
.on_frame = server_on_frame,
.on_connect = server_on_connect,
.on_disconnect = server_on_disconnect,
};
struct Transport_Server *server;
struct App_Error e = transport_server_create(&server, &cfg);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
e = transport_server_start(server);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
printf("listening on port %u\n", port);
pause();
return 0;
}
/* -- client mode ----------------------------------------------------------- */
static void client_on_frame(struct Transport_Conn *conn,
struct Transport_Frame *frame, void *ud)
{
(void)conn; (void)ud;
decode_and_print(frame);
free(frame->payload);
}
static void client_on_disconnect(struct Transport_Conn *conn, void *ud) {
(void)conn; (void)ud;
printf("disconnected\n");
}
static int run_client(const char *host, uint16_t port) {
struct Transport_Conn *conn;
struct App_Error e = transport_connect(&conn, host, port,
16 * 1024 * 1024,
client_on_frame, client_on_disconnect, NULL);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
printf("connected — sending STREAM_OPEN request\n");
e = proto_write_stream_open(conn,
/*request_id=*/1,
/*stream_id=*/0,
PROTO_FORMAT_MJPEG,
0,
PROTO_ORIGIN_DEVICE_NATIVE);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
printf("sent STREAM_OPEN; waiting for response (ctrl-c to exit)\n");
pause();
return 0;
}
/* -- usage ----------------------------------------------------------------- */
static void usage(void) {
fprintf(stderr,
"usage: protocol_cli --server [port]\n"
" protocol_cli --client host port\n"
"\n"
" --server [port] listen and decode incoming protocol frames\n"
" (default port 8000)\n"
" --client host port connect, send STREAM_OPEN, decode responses\n");
}
int main(int argc, char **argv) {
if (argc < 2) { usage(); return 1; }
if (strcmp(argv[1], "--server") == 0) {
uint16_t port = 8000;
if (argc >= 3) { port = (uint16_t)atoi(argv[2]); }
return run_server(port);
}
if (strcmp(argv[1], "--client") == 0) {
if (argc < 4) { usage(); return 1; }
uint16_t port = (uint16_t)atoi(argv[3]);
return run_client(argv[2], port);
}
usage();
return 1;
}