- 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>
251 lines
7.3 KiB
C
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;
|
|
}
|