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:
250
dev/cli/protocol_cli.c
Normal file
250
dev/cli/protocol_cli.c
Normal file
@@ -0,0 +1,250 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user