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:
@@ -9,47 +9,79 @@ SERIAL_OBJ = $(BUILD)/serial/serial.o
|
||||
TRANSPORT_OBJ = $(BUILD)/transport/transport.o
|
||||
DISCOVERY_OBJ = $(BUILD)/discovery/discovery.o
|
||||
CONFIG_OBJ = $(BUILD)/config/config.o
|
||||
PROTOCOL_OBJ = $(BUILD)/protocol/protocol.o
|
||||
|
||||
.PHONY: all clean modules
|
||||
CLI_SRCS = \
|
||||
media_ctrl_cli.c \
|
||||
v4l2_ctrl_cli.c \
|
||||
transport_cli.c \
|
||||
discovery_cli.c \
|
||||
config_cli.c \
|
||||
protocol_cli.c \
|
||||
query_cli.c
|
||||
|
||||
all: modules \
|
||||
CLI_OBJS = $(CLI_SRCS:%.c=$(CLI_BUILD)/%.o)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: \
|
||||
$(CLI_BUILD)/media_ctrl_cli \
|
||||
$(CLI_BUILD)/v4l2_ctrl_cli \
|
||||
$(CLI_BUILD)/transport_cli \
|
||||
$(CLI_BUILD)/discovery_cli \
|
||||
$(CLI_BUILD)/config_cli
|
||||
$(CLI_BUILD)/config_cli \
|
||||
$(CLI_BUILD)/protocol_cli \
|
||||
$(CLI_BUILD)/query_cli
|
||||
|
||||
modules:
|
||||
$(MAKE) -C $(ROOT)/src/modules/common
|
||||
$(MAKE) -C $(ROOT)/src/modules/media_ctrl
|
||||
$(MAKE) -C $(ROOT)/src/modules/v4l2_ctrl
|
||||
$(MAKE) -C $(ROOT)/src/modules/serial
|
||||
$(MAKE) -C $(ROOT)/src/modules/transport
|
||||
$(MAKE) -C $(ROOT)/src/modules/discovery
|
||||
$(MAKE) -C $(ROOT)/src/modules/config
|
||||
# Module objects delegate to their sub-makes.
|
||||
$(COMMON_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/common
|
||||
$(MEDIA_CTRL_OBJ):; $(MAKE) -C $(ROOT)/src/modules/media_ctrl
|
||||
$(V4L2_CTRL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/v4l2_ctrl
|
||||
$(SERIAL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/serial
|
||||
$(TRANSPORT_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/transport
|
||||
$(DISCOVERY_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/discovery
|
||||
$(CONFIG_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/config
|
||||
$(PROTOCOL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/protocol
|
||||
|
||||
$(CLI_BUILD)/media_ctrl_cli: media_ctrl_cli.c $(COMMON_OBJ) $(MEDIA_CTRL_OBJ) | $(CLI_BUILD)
|
||||
# Compile each CLI source to its own .o (generates .d alongside).
|
||||
$(CLI_BUILD)/%.o: %.c | $(CLI_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
# Link rules.
|
||||
$(CLI_BUILD)/media_ctrl_cli: $(CLI_BUILD)/media_ctrl_cli.o $(COMMON_OBJ) $(MEDIA_CTRL_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^
|
||||
|
||||
$(CLI_BUILD)/v4l2_ctrl_cli: v4l2_ctrl_cli.c $(COMMON_OBJ) $(V4L2_CTRL_OBJ) | $(CLI_BUILD)
|
||||
$(CLI_BUILD)/v4l2_ctrl_cli: $(CLI_BUILD)/v4l2_ctrl_cli.o $(COMMON_OBJ) $(V4L2_CTRL_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^
|
||||
|
||||
$(CLI_BUILD)/transport_cli: transport_cli.c $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) | $(CLI_BUILD)
|
||||
$(CLI_BUILD)/transport_cli: $(CLI_BUILD)/transport_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread
|
||||
|
||||
$(CLI_BUILD)/discovery_cli: discovery_cli.c $(COMMON_OBJ) $(SERIAL_OBJ) $(DISCOVERY_OBJ) | $(CLI_BUILD)
|
||||
$(CLI_BUILD)/discovery_cli: $(CLI_BUILD)/discovery_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(DISCOVERY_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread
|
||||
|
||||
$(CLI_BUILD)/config_cli: config_cli.c $(COMMON_OBJ) $(CONFIG_OBJ) | $(CLI_BUILD)
|
||||
$(CLI_BUILD)/config_cli: $(CLI_BUILD)/config_cli.o $(COMMON_OBJ) $(CONFIG_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^
|
||||
|
||||
$(CLI_BUILD)/protocol_cli: $(CLI_BUILD)/protocol_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(PROTOCOL_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread
|
||||
|
||||
$(CLI_BUILD)/query_cli: $(CLI_BUILD)/query_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(DISCOVERY_OBJ) $(PROTOCOL_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread
|
||||
|
||||
$(CLI_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f \
|
||||
$(CLI_OBJS) \
|
||||
$(CLI_OBJS:%.o=%.d) \
|
||||
$(CLI_BUILD)/media_ctrl_cli \
|
||||
$(CLI_BUILD)/v4l2_ctrl_cli \
|
||||
$(CLI_BUILD)/transport_cli \
|
||||
$(CLI_BUILD)/discovery_cli \
|
||||
$(CLI_BUILD)/config_cli
|
||||
$(CLI_BUILD)/config_cli \
|
||||
$(CLI_BUILD)/protocol_cli \
|
||||
$(CLI_BUILD)/query_cli
|
||||
|
||||
-include $(CLI_OBJS:%.o=%.d)
|
||||
|
||||
@@ -62,7 +62,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
if (!APP_IS_OK(err)) {
|
||||
fprintf(stderr, "config_load: errno %d\n", err.detail.syscall.err_no);
|
||||
app_error_print(&err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
297
dev/cli/query_cli.c
Normal file
297
dev/cli/query_cli.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user