diff --git a/dev/cli/Makefile b/dev/cli/Makefile index 13a4684..633d783 100644 --- a/dev/cli/Makefile +++ b/dev/cli/Makefile @@ -27,7 +27,8 @@ CLI_SRCS = \ v4l2_view_cli.c \ stream_send_cli.c \ stream_recv_cli.c \ - reconciler_cli.c + reconciler_cli.c \ + controller_cli.c CLI_OBJS = $(CLI_SRCS:%.c=$(CLI_BUILD)/%.o) @@ -46,7 +47,8 @@ all: \ $(CLI_BUILD)/v4l2_view_cli \ $(CLI_BUILD)/stream_send_cli \ $(CLI_BUILD)/stream_recv_cli \ - $(CLI_BUILD)/reconciler_cli + $(CLI_BUILD)/reconciler_cli \ + $(CLI_BUILD)/controller_cli # Module objects delegate to their sub-makes. $(COMMON_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/common @@ -105,6 +107,9 @@ $(CLI_BUILD)/stream_recv_cli: $(CLI_BUILD)/stream_recv_cli.o $(COMMON_OBJ) $(SER $(CLI_BUILD)/reconciler_cli: $(CLI_BUILD)/reconciler_cli.o $(RECONCILER_OBJ) $(CC) $(CFLAGS) -o $@ $^ +$(CLI_BUILD)/controller_cli: $(CLI_BUILD)/controller_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(PROTOCOL_OBJ) + $(CC) $(CFLAGS) -o $@ $^ -lpthread + $(CLI_BUILD): mkdir -p $@ @@ -124,6 +129,7 @@ clean: $(CLI_BUILD)/v4l2_view_cli \ $(CLI_BUILD)/stream_send_cli \ $(CLI_BUILD)/stream_recv_cli \ - $(CLI_BUILD)/reconciler_cli + $(CLI_BUILD)/reconciler_cli \ + $(CLI_BUILD)/controller_cli -include $(CLI_OBJS:%.o=%.d) diff --git a/dev/cli/controller_cli.c b/dev/cli/controller_cli.c new file mode 100644 index 0000000..cd31930 --- /dev/null +++ b/dev/cli/controller_cli.c @@ -0,0 +1,423 @@ +#include +#include +#include +#include +#include + +#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 " + " [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_help(void) +{ + printf("commands:\n" + " enum-devices\n" + " enum-controls \n" + " get-control \n" + " set-control \n" + " start-ingest " + " [format] [width] [height] [fps_n] [fps_d]\n" + " stop-ingest \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 \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 \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 \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 \n"); } + else { cmd_stop_ingest(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; +} diff --git a/planning.md b/planning.md index 0e23d34..515d220 100644 --- a/planning.md +++ b/planning.md @@ -86,6 +86,7 @@ Each module gets a corresponding CLI driver that exercises its API and serves as | `stream_send_cli` | V4L2 + `transport` + `protocol` | Capture MJPEG from V4L2, connect to receiver, send VIDEO_FRAME messages; prints fps/Mbps stats | | `stream_recv_cli` | `transport` + `protocol` + `xorg` | Listen for incoming VIDEO_FRAME stream, display in viewer; fps/Mbps overlay; threaded transport→GL handoff | | `reconciler_cli` | `reconciler` | Simulated state machine experiment — define resources with fake transitions, drive reconciler via CLI commands; validates the generic reconciler before wiring into the node | +| `controller_cli` | `transport` + `protocol` | Interactive controller REPL — connects to a running node by host:port; supports enum-devices, enum-controls, get/set-control, start-ingest, stop-ingest | ### Web UI (`dev/web/`)