Files
video-setup/dev/cli/controller_cli.c
mikael-lovqvists-claude-agent 32d31cbd1e Add display sink: START_DISPLAY/STOP_DISPLAY, multi-window xorg, random port
Protocol:
- Add PROTO_CMD_START_DISPLAY (0x000A) and PROTO_CMD_STOP_DISPLAY (0x000B)
  with write/read functions; Proto_Start_Display carries stream_id, window
  position/size, scale and anchor; PROTO_DISPLAY_SCALE_*/ANCHOR_* constants

Node display sink:
- Display_Slot struct with wanted_state/current_state (DISP_CLOSED/DISP_OPEN);
  handlers set wanted state, display_loop_tick on main thread reconciles
- Up to MAX_DISPLAYS (4) simultaneous viewer windows
- on_frame routes incoming VIDEO_FRAME messages to matching display slot;
  transport thread deposits payload, main thread consumes without holding lock
  during JPEG decode/upload
- Main thread runs GL event loop when xorg is available; headless fallback
  joins reconciler timer thread as before

Xorg multi-window:
- Ref-count glfwInit/glfwTerminate via glfw_acquire/glfw_release so closing
  one viewer does not terminate GLFW for remaining windows
- Add glfwMakeContextCurrent before GL calls in push_yuv420, push_bgra,
  push_mjpeg and poll so each viewer uses its own GL context correctly

Transport random port:
- Bind port 0 lets the OS assign a free port; getsockname reads it back
  into server->bound_port after bind
- Add transport_server_get_port() accessor
- Default tcp_port changed from 8000 to 0 (random); node prints actual
  port after server start so it is always visible in output
- Add --port PORT CLI override (before config-file argument)

controller_cli:
- Add start-display and stop-display commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 08:03:21 +00:00

464 lines
14 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <semaphore.h>
#include "transport.h"
#include "protocol.h"
#include "error.h"
/* -------------------------------------------------------------------------
* Shared state between REPL and transport read thread
* ------------------------------------------------------------------------- */
struct Ctrl_State {
sem_t sem;
uint16_t pending_cmd;
uint16_t last_status;
int32_t last_value; /* GET_CONTROL response */
};
/* -------------------------------------------------------------------------
* Response display helpers — reused across commands
* ------------------------------------------------------------------------- */
static void caps_str(uint32_t caps, char *buf, size_t len)
{
static const struct { uint32_t bit; const char *name; } flags[] = {
{ 0x00000001u, "video-capture" },
{ 0x00000002u, "video-output" },
{ 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_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 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);
}
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");
}
static void on_menu_item(
uint32_t index,
const char *name, uint8_t name_len,
int64_t int_value,
void *ud)
{
(void)ud;
printf(" menu %u %.*s val=%lld\n",
index,
(int)name_len, name,
(long long)int_value);
}
/* -------------------------------------------------------------------------
* Transport callbacks
* ------------------------------------------------------------------------- */
static void on_frame(struct Transport_Conn *conn,
struct Transport_Frame *frame, void *userdata)
{
(void)conn;
struct Ctrl_State *cs = userdata;
if (frame->message_type != PROTO_MSG_CONTROL_RESPONSE) {
free(frame->payload);
return;
}
switch (cs->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: status=%u\n", hdr.status);
}
cs->last_status = 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, on_menu_item, NULL);
if (!APP_IS_OK(e)) { app_error_print(&e); }
else if (hdr.status != PROTO_STATUS_OK) {
fprintf(stderr, "ENUM_CONTROLS: status=%u\n", hdr.status);
}
cs->last_status = hdr.status;
break;
}
case PROTO_CMD_GET_CONTROL: {
struct Proto_Get_Control_Resp resp;
struct App_Error e = proto_read_get_control_response(
frame->payload, frame->payload_length, &resp);
if (!APP_IS_OK(e)) { app_error_print(&e); }
else if (resp.status == PROTO_STATUS_OK) {
printf(" value = %d\n", resp.value);
} else {
fprintf(stderr, "GET_CONTROL: status=%u\n", resp.status);
}
cs->last_status = resp.status;
break;
}
default: {
/* Generic response: just read request_id + status */
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); }
else if (hdr.status != PROTO_STATUS_OK) {
fprintf(stderr, "command 0x%04x: status=%u\n",
cs->pending_cmd, hdr.status);
} else {
printf(" ok\n");
}
cs->last_status = APP_IS_OK(e) ? hdr.status : PROTO_STATUS_ERROR;
break;
}
}
free(frame->payload);
sem_post(&cs->sem);
}
static void on_disconnect(struct Transport_Conn *conn, void *userdata)
{
(void)conn; (void)userdata;
printf("disconnected from node\n");
}
/* -------------------------------------------------------------------------
* Request helpers
* ------------------------------------------------------------------------- */
static uint16_t next_req_id(uint16_t *counter)
{
return ++(*counter);
}
/* Send a request, set pending_cmd, wait for response */
#define SEND_AND_WAIT(cs, cmd, send_expr) do { \
(cs)->pending_cmd = (cmd); \
struct App_Error _e = (send_expr); \
if (!APP_IS_OK(_e)) { app_error_print(&_e); break; } \
sem_wait(&(cs)->sem); \
} while (0)
/* -------------------------------------------------------------------------
* REPL command implementations
* ------------------------------------------------------------------------- */
static void cmd_enum_devices(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req)
{
printf("devices:\n");
SEND_AND_WAIT(cs, PROTO_CMD_ENUM_DEVICES,
proto_write_enum_devices(conn, next_req_id(req)));
}
static void cmd_enum_controls(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req,
const char *idx_str)
{
int idx = atoi(idx_str);
printf("controls for device %d:\n", idx);
SEND_AND_WAIT(cs, PROTO_CMD_ENUM_CONTROLS,
proto_write_enum_controls(conn, next_req_id(req), (uint16_t)idx));
}
static void cmd_get_control(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req,
const char *idx_str, const char *id_str)
{
int idx = atoi(idx_str);
uint32_t id = (uint32_t)strtoul(id_str, NULL, 0);
printf("get control 0x%08x on device %d:\n", id, idx);
SEND_AND_WAIT(cs, PROTO_CMD_GET_CONTROL,
proto_write_get_control(conn, next_req_id(req), (uint16_t)idx, id));
}
static void cmd_set_control(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req,
const char *idx_str, const char *id_str, const char *val_str)
{
int idx = atoi(idx_str);
uint32_t id = (uint32_t)strtoul(id_str, NULL, 0);
int32_t val = (int32_t)atoi(val_str);
SEND_AND_WAIT(cs, PROTO_CMD_SET_CONTROL,
proto_write_set_control(conn, next_req_id(req), (uint16_t)idx, id, val));
}
static void cmd_start_ingest(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req,
int ntok, char *tokens[])
{
/* Required: stream_id device dest_host dest_port
* Optional: format width height fps_n fps_d */
if (ntok < 5) {
printf("usage: start-ingest <stream_id> <device> <dest_host> <dest_port>"
" [format] [width] [height] [fps_n] [fps_d]\n"
" format: 0=auto 1=mjpeg (default 0)\n");
return;
}
uint16_t stream_id = (uint16_t)atoi(tokens[1]);
const char *device = tokens[2];
const char *host = tokens[3];
uint16_t port = (uint16_t)atoi(tokens[4]);
uint16_t format = ntok > 5 ? (uint16_t)atoi(tokens[5]) : 0;
uint16_t width = ntok > 6 ? (uint16_t)atoi(tokens[6]) : 0;
uint16_t height = ntok > 7 ? (uint16_t)atoi(tokens[7]) : 0;
uint16_t fps_n = ntok > 8 ? (uint16_t)atoi(tokens[8]) : 0;
uint16_t fps_d = ntok > 9 ? (uint16_t)atoi(tokens[9]) : 1;
printf("start-ingest: stream=%u device=%s dest=%s:%u"
" format=%u %ux%u fps=%u/%u\n",
stream_id, device, host, port, format, width, height, fps_n, fps_d);
SEND_AND_WAIT(cs, PROTO_CMD_START_INGEST,
proto_write_start_ingest(conn, next_req_id(req),
stream_id, format, width, height, fps_n, fps_d,
PROTO_TRANSPORT_ENCAPSULATED, device, host, port));
}
static void cmd_stop_ingest(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req,
const char *sid_str)
{
uint16_t stream_id = (uint16_t)atoi(sid_str);
printf("stop-ingest: stream=%u\n", stream_id);
SEND_AND_WAIT(cs, PROTO_CMD_STOP_INGEST,
proto_write_stop_ingest(conn, next_req_id(req), stream_id));
}
static void cmd_start_display(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req,
int ntok, char *tokens[])
{
/* Required: stream_id
* Optional: win_x win_y win_w win_h */
if (ntok < 2) {
printf("usage: start-display <stream_id> [win_x] [win_y] [win_w] [win_h]\n");
return;
}
uint16_t stream_id = (uint16_t)atoi(tokens[1]);
int16_t win_x = ntok > 2 ? (int16_t)atoi(tokens[2]) : 0;
int16_t win_y = ntok > 3 ? (int16_t)atoi(tokens[3]) : 0;
uint16_t win_w = ntok > 4 ? (uint16_t)atoi(tokens[4]) : 0;
uint16_t win_h = ntok > 5 ? (uint16_t)atoi(tokens[5]) : 0;
printf("start-display: stream=%u pos=%d,%d size=%ux%u\n",
stream_id, win_x, win_y, win_w, win_h);
SEND_AND_WAIT(cs, PROTO_CMD_START_DISPLAY,
proto_write_start_display(conn, next_req_id(req),
stream_id, win_x, win_y, win_w, win_h,
PROTO_DISPLAY_SCALE_FIT, PROTO_DISPLAY_ANCHOR_CENTER));
}
static void cmd_stop_display(struct Transport_Conn *conn,
struct Ctrl_State *cs, uint16_t *req,
const char *sid_str)
{
uint16_t stream_id = (uint16_t)atoi(sid_str);
printf("stop-display: stream=%u\n", stream_id);
SEND_AND_WAIT(cs, PROTO_CMD_STOP_DISPLAY,
proto_write_stop_display(conn, next_req_id(req), stream_id));
}
static void cmd_help(void)
{
printf("commands:\n"
" enum-devices\n"
" enum-controls <device_index>\n"
" get-control <device_index> <control_id_hex>\n"
" set-control <device_index> <control_id_hex> <value>\n"
" start-ingest <stream_id> <device> <dest_host> <dest_port>"
" [format] [width] [height] [fps_n] [fps_d]\n"
" stop-ingest <stream_id>\n"
" start-display <stream_id> [win_x] [win_y] [win_w] [win_h]\n"
" stop-display <stream_id>\n"
" help\n"
" quit / exit\n");
}
/* -------------------------------------------------------------------------
* Entry point
* ------------------------------------------------------------------------- */
static void usage(void)
{
fprintf(stderr,
"usage: controller_cli --host HOST [--port PORT]\n"
"\n"
" Interactive controller for a video node.\n"
" --host HOST node hostname or IP (required)\n"
" --port PORT node TCP port (default 8000)\n");
}
int main(int argc, char **argv)
{
const char *host = NULL;
uint16_t port = 8000;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
host = argv[++i];
} else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
port = (uint16_t)atoi(argv[++i]);
} else {
usage(); return 1;
}
}
if (!host) { usage(); return 1; }
/* Connect */
struct Ctrl_State cs;
memset(&cs, 0, sizeof(cs));
sem_init(&cs.sem, 0, 0);
struct Transport_Conn *conn;
struct App_Error e = transport_connect(&conn, host, port,
TRANSPORT_DEFAULT_MAX_PAYLOAD, on_frame, on_disconnect, &cs);
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
printf("connected to %s:%u\n\n", host, port);
cmd_help();
printf("\n");
/* REPL */
uint16_t req_id = 0;
char line[512];
while (1) {
printf("> ");
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) { break; }
/* Strip trailing newline */
size_t len = strlen(line);
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) {
line[--len] = '\0';
}
/* Tokenise (up to 12 tokens) */
char *tokens[12];
int ntok = 0;
char *p = line;
while (*p && ntok < 12) {
while (*p == ' ' || *p == '\t') { p++; }
if (!*p) { break; }
tokens[ntok++] = p;
while (*p && *p != ' ' && *p != '\t') { p++; }
if (*p) { *p++ = '\0'; }
}
if (ntok == 0) { continue; }
const char *cmd = tokens[0];
if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) {
break;
} else if (strcmp(cmd, "help") == 0) {
cmd_help();
} else if (strcmp(cmd, "enum-devices") == 0) {
cmd_enum_devices(conn, &cs, &req_id);
} else if (strcmp(cmd, "enum-controls") == 0) {
if (ntok < 2) { printf("usage: enum-controls <device_index>\n"); }
else { cmd_enum_controls(conn, &cs, &req_id, tokens[1]); }
} else if (strcmp(cmd, "get-control") == 0) {
if (ntok < 3) { printf("usage: get-control <device_index> <control_id>\n"); }
else { cmd_get_control(conn, &cs, &req_id, tokens[1], tokens[2]); }
} else if (strcmp(cmd, "set-control") == 0) {
if (ntok < 4) { printf("usage: set-control <device_index> <control_id> <value>\n"); }
else { cmd_set_control(conn, &cs, &req_id, tokens[1], tokens[2], tokens[3]); }
} else if (strcmp(cmd, "start-ingest") == 0) {
cmd_start_ingest(conn, &cs, &req_id, ntok, tokens);
} else if (strcmp(cmd, "stop-ingest") == 0) {
if (ntok < 2) { printf("usage: stop-ingest <stream_id>\n"); }
else { cmd_stop_ingest(conn, &cs, &req_id, tokens[1]); }
} else if (strcmp(cmd, "start-display") == 0) {
cmd_start_display(conn, &cs, &req_id, ntok, tokens);
} else if (strcmp(cmd, "stop-display") == 0) {
if (ntok < 2) { printf("usage: stop-display <stream_id>\n"); }
else { cmd_stop_display(conn, &cs, &req_id, tokens[1]); }
} else {
printf("unknown command: %s (type 'help' for commands)\n", cmd);
}
}
transport_conn_close(conn);
sem_destroy(&cs.sem);
return 0;
}