#include #include #include #include #include #include #include #include #include #include #include "config.h" #include "discovery.h" #include "transport.h" #include "protocol.h" #include "media_ctrl.h" #include "v4l2_ctrl.h" #include "error.h" /* ------------------------------------------------------------------------- * Device enumeration * ------------------------------------------------------------------------- */ /* Entity type flag for a V4L2 I/O interface entity */ #define MEDIA_ENT_F_IO_V4L 0x10001u #define MAX_VIDEO_NODES 32 #define MAX_MEDIA_DEVICES 8 #define MAX_VNODES_PER_MD 8 #define MAX_CONTROLS 256 /* 5 ("/dev/") + NAME_MAX (255) + 1 (NUL) = 261; round up */ #define DEV_PATH_MAX 264 struct VNode { char path[DEV_PATH_MAX]; uint32_t dev_major; uint32_t dev_minor; uint32_t device_caps; /* V4L2_CAP_* from VIDIOC_QUERYCAP cap.device_caps */ int claimed; char card[32]; /* VIDIOC_QUERYCAP card name, empty if unavailable */ }; struct MNode { char path[DEV_PATH_MAX]; char entity_name[32]; uint32_t entity_type; uint32_t entity_flags; uint32_t device_caps; uint8_t is_capture; int vnode_index; /* index into VNode array */ }; struct MediaDev { char path[DEV_PATH_MAX]; char driver[16]; char model[32]; char bus_info[32]; struct MNode vnodes[MAX_VNODES_PER_MD]; int vnode_count; }; struct Device_List { struct VNode vnodes[MAX_VIDEO_NODES]; int vnode_count; struct MediaDev media[MAX_MEDIA_DEVICES]; int media_count; }; static int scan_video_nodes(struct Device_List *dl) { DIR *d = opendir("/dev"); if (!d) { return -1; } struct dirent *ent; while ((ent = readdir(d)) != NULL && dl->vnode_count < MAX_VIDEO_NODES) { if (strncmp(ent->d_name, "video", 5) != 0) { continue; } const char *suffix = ent->d_name + 5; int numeric = (*suffix != '\0'); for (const char *p = suffix; *p; p++) { if (*p < '0' || *p > '9') { numeric = 0; break; } } if (!numeric) { continue; } struct VNode *v = &dl->vnodes[dl->vnode_count]; snprintf(v->path, sizeof(v->path), "/dev/%s", ent->d_name); struct stat st; if (stat(v->path, &st) != 0 || !S_ISCHR(st.st_mode)) { continue; } v->dev_major = (uint32_t)major(st.st_rdev); v->dev_minor = (uint32_t)minor(st.st_rdev); v->claimed = 0; v->card[0] = '\0'; /* Try to get card name */ int fd = open(v->path, O_RDONLY | O_NONBLOCK); if (fd >= 0) { struct v4l2_capability cap; if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) { strncpy(v->card, (const char *)cap.card, sizeof(v->card) - 1); v->card[sizeof(v->card) - 1] = '\0'; /* use per-node caps when available, fall back to physical caps */ v->device_caps = (cap.capabilities & V4L2_CAP_DEVICE_CAPS) ? cap.device_caps : cap.capabilities; } close(fd); } dl->vnode_count++; } closedir(d); return 0; } struct Entity_Cb_State { struct Device_List *dl; struct MediaDev *md; }; static void entity_callback(const struct Media_Entity *entity, void *userdata) { struct Entity_Cb_State *state = userdata; struct Device_List *dl = state->dl; struct MediaDev *md = state->md; if (entity->dev_major == 0) { return; } if (md->vnode_count >= MAX_VNODES_PER_MD) { return; } /* Find matching video node by device number */ for (int i = 0; i < dl->vnode_count; i++) { if (dl->vnodes[i].dev_major != entity->dev_major) { continue; } if (dl->vnodes[i].dev_minor != entity->dev_minor) { continue; } dl->vnodes[i].claimed = 1; struct MNode *mn = &md->vnodes[md->vnode_count++]; strncpy(mn->path, dl->vnodes[i].path, sizeof(mn->path) - 1); mn->path[sizeof(mn->path) - 1] = '\0'; strncpy(mn->entity_name, entity->name, sizeof(mn->entity_name) - 1); mn->entity_name[sizeof(mn->entity_name) - 1] = '\0'; mn->entity_type = entity->type; mn->entity_flags = entity->flags; mn->device_caps = dl->vnodes[i].device_caps; mn->is_capture = (mn->device_caps & V4L2_CAP_VIDEO_CAPTURE) ? 1 : 0; mn->vnode_index = i; break; } } static void scan_media_devices(struct Device_List *dl) { DIR *d = opendir("/dev"); if (!d) { return; } struct dirent *ent; while ((ent = readdir(d)) != NULL && dl->media_count < MAX_MEDIA_DEVICES) { if (strncmp(ent->d_name, "media", 5) != 0) { continue; } const char *suffix = ent->d_name + 5; int numeric = (*suffix != '\0'); for (const char *p = suffix; *p; p++) { if (*p < '0' || *p > '9') { numeric = 0; break; } } if (!numeric) { continue; } struct MediaDev *md = &dl->media[dl->media_count]; snprintf(md->path, sizeof(md->path), "/dev/%s", ent->d_name); md->vnode_count = 0; struct Media_Ctrl *ctrl; if (!APP_IS_OK(media_ctrl_open(md->path, &ctrl))) { continue; } struct Media_Device_Info info; if (APP_IS_OK(media_ctrl_get_info(ctrl, &info))) { strncpy(md->driver, info.driver, sizeof(md->driver) - 1); strncpy(md->model, info.model, sizeof(md->model) - 1); strncpy(md->bus_info, info.bus_info, sizeof(md->bus_info) - 1); md->driver[sizeof(md->driver) - 1] = '\0'; md->model[sizeof(md->model) - 1] = '\0'; md->bus_info[sizeof(md->bus_info) - 1] = '\0'; } struct Entity_Cb_State state = { .dl = dl, .md = md }; media_ctrl_enum_entities(ctrl, entity_callback, &state); media_ctrl_close(ctrl); dl->media_count++; } closedir(d); } static void build_device_list(struct Device_List *dl) { memset(dl, 0, sizeof(*dl)); scan_video_nodes(dl); scan_media_devices(dl); } /* ------------------------------------------------------------------------- * Control enumeration helpers * ------------------------------------------------------------------------- */ #define MAX_MENU_POOL 128 /* total menu items across all controls */ struct Ctrl_Build { struct Proto_Control_Info items[MAX_CONTROLS]; char names[MAX_CONTROLS][32]; struct Proto_Menu_Item menu_pool[MAX_MENU_POOL]; char menu_names[MAX_MENU_POOL][32]; int menu_pool_used; int count; }; static void ctrl_enum_cb( const struct V4l2_Ctrl_Desc *desc, uint32_t menu_count, const struct V4l2_Menu_Item *menu_items, void *userdata) { struct Ctrl_Build *b = userdata; if (b->count >= MAX_CONTROLS) { return; } int i = b->count++; strncpy(b->names[i], desc->name, 31); b->names[i][31] = '\0'; b->items[i].id = desc->id; b->items[i].type = (uint8_t)desc->type; b->items[i].flags = desc->flags; b->items[i].name = b->names[i]; b->items[i].min = desc->min; b->items[i].max = desc->max; b->items[i].step = desc->step; b->items[i].default_val = desc->default_value; b->items[i].current_val = desc->current_value; b->items[i].menu_count = 0; b->items[i].menu_items = NULL; if (menu_count > 0 && menu_items) { int avail = MAX_MENU_POOL - b->menu_pool_used; uint8_t mc = (menu_count > (uint32_t)avail) ? (uint8_t)avail : (uint8_t)menu_count; if (mc > 0) { b->items[i].menu_items = &b->menu_pool[b->menu_pool_used]; b->items[i].menu_count = mc; for (uint8_t j = 0; j < mc; j++) { int slot = b->menu_pool_used + j; strncpy(b->menu_names[slot], menu_items[j].name, 31); b->menu_names[slot][31] = '\0'; b->menu_pool[slot].index = menu_items[j].index; b->menu_pool[slot].name = b->menu_names[slot]; b->menu_pool[slot].int_value = menu_items[j].value; } b->menu_pool_used += mc; } } } /* ------------------------------------------------------------------------- * Node state * ------------------------------------------------------------------------- */ struct Node { struct Config *config; struct Transport_Server *server; struct Discovery *discovery; struct Device_List devices; }; /* ------------------------------------------------------------------------- * Request handlers * ------------------------------------------------------------------------- */ static void handle_enum_devices(struct Node *node, struct Transport_Conn *conn, uint16_t request_id) { /* Build Proto_Media_Device_Info array */ struct Proto_Video_Node_Info vnodes[MAX_MEDIA_DEVICES * MAX_VNODES_PER_MD]; struct Proto_Media_Device_Info mdevs[MAX_MEDIA_DEVICES]; int vnode_offset = 0; for (int i = 0; i < node->devices.media_count; i++) { struct MediaDev *md = &node->devices.media[i]; mdevs[i].path = md->path; mdevs[i].driver = md->driver; mdevs[i].model = md->model; mdevs[i].bus_info = md->bus_info; mdevs[i].video_node_count = (uint8_t)md->vnode_count; mdevs[i].video_nodes = &vnodes[vnode_offset]; for (int j = 0; j < md->vnode_count; j++) { struct MNode *mn = &md->vnodes[j]; vnodes[vnode_offset + j].path = mn->path; vnodes[vnode_offset + j].entity_name = mn->entity_name; vnodes[vnode_offset + j].entity_type = mn->entity_type; vnodes[vnode_offset + j].entity_flags= mn->entity_flags; vnodes[vnode_offset + j].device_caps = mn->device_caps; vnodes[vnode_offset + j].pad_flags = 0; vnodes[vnode_offset + j].is_capture = mn->is_capture; } vnode_offset += md->vnode_count; } /* Build standalone list */ struct Proto_Standalone_Device_Info standalone[MAX_VIDEO_NODES]; int standalone_count = 0; for (int i = 0; i < node->devices.vnode_count; i++) { if (node->devices.vnodes[i].claimed) { continue; } standalone[standalone_count].path = node->devices.vnodes[i].path; standalone[standalone_count].name = node->devices.vnodes[i].card; standalone_count++; } struct App_Error e = proto_write_enum_devices_response(conn, request_id, PROTO_STATUS_OK, mdevs, (uint16_t)node->devices.media_count, standalone, (uint16_t)standalone_count); if (!APP_IS_OK(e)) { app_error_print(&e); } } static void handle_enum_controls(struct Node *node, struct Transport_Conn *conn, const uint8_t *payload, uint32_t length) { struct Proto_Enum_Controls_Req req; struct App_Error e = proto_read_enum_controls_req(payload, length, &req); if (!APP_IS_OK(e)) { proto_write_control_response(conn, 0, PROTO_STATUS_INVALID_PARAMS, NULL, 0); return; } /* Resolve device_index to a path across media-owned + standalone nodes */ const char *path = NULL; int idx = (int)req.device_index; for (int i = 0; i < node->devices.media_count && path == NULL; i++) { struct MediaDev *md = &node->devices.media[i]; if (idx < md->vnode_count) { path = md->vnodes[idx].path; } else { idx -= md->vnode_count; } } if (path == NULL) { /* Check standalone */ for (int i = 0; i < node->devices.vnode_count; i++) { if (node->devices.vnodes[i].claimed) { continue; } if (idx == 0) { path = node->devices.vnodes[i].path; break; } idx--; } } if (path == NULL) { proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); return; } struct V4l2_Ctrl_Handle *handle; e = v4l2_ctrl_open(path, &handle); if (!APP_IS_OK(e)) { proto_write_control_response(conn, req.request_id, PROTO_STATUS_ERROR, NULL, 0); return; } struct Ctrl_Build build = { .count = 0 }; v4l2_ctrl_enumerate(handle, ctrl_enum_cb, &build); v4l2_ctrl_close(handle); e = proto_write_enum_controls_response(conn, req.request_id, PROTO_STATUS_OK, build.items, (uint16_t)build.count); if (!APP_IS_OK(e)) { app_error_print(&e); } } static void handle_get_control(struct Node *node, struct Transport_Conn *conn, const uint8_t *payload, uint32_t length) { struct Proto_Get_Control_Req req; struct App_Error e = proto_read_get_control_req(payload, length, &req); if (!APP_IS_OK(e)) { proto_write_control_response(conn, 0, PROTO_STATUS_INVALID_PARAMS, NULL, 0); return; } /* Same device resolution as enum_controls */ const char *path = NULL; int idx = (int)req.device_index; for (int i = 0; i < node->devices.media_count && path == NULL; i++) { struct MediaDev *md = &node->devices.media[i]; if (idx < md->vnode_count) { path = md->vnodes[idx].path; } else { idx -= md->vnode_count; } } if (path == NULL) { for (int i = 0; i < node->devices.vnode_count; i++) { if (node->devices.vnodes[i].claimed) { continue; } if (idx == 0) { path = node->devices.vnodes[i].path; break; } idx--; } } if (path == NULL) { proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); return; } struct V4l2_Ctrl_Handle *handle; e = v4l2_ctrl_open(path, &handle); if (!APP_IS_OK(e)) { proto_write_control_response(conn, req.request_id, PROTO_STATUS_ERROR, NULL, 0); return; } int32_t value; e = v4l2_ctrl_get(handle, req.control_id, &value); v4l2_ctrl_close(handle); if (!APP_IS_OK(e)) { proto_write_control_response(conn, req.request_id, PROTO_STATUS_ERROR, NULL, 0); return; } e = proto_write_get_control_response(conn, req.request_id, PROTO_STATUS_OK, value); if (!APP_IS_OK(e)) { app_error_print(&e); } } static void handle_set_control(struct Node *node, struct Transport_Conn *conn, const uint8_t *payload, uint32_t length) { struct Proto_Set_Control_Req req; struct App_Error e = proto_read_set_control_req(payload, length, &req); if (!APP_IS_OK(e)) { proto_write_control_response(conn, 0, PROTO_STATUS_INVALID_PARAMS, NULL, 0); return; } const char *path = NULL; int idx = (int)req.device_index; for (int i = 0; i < node->devices.media_count && path == NULL; i++) { struct MediaDev *md = &node->devices.media[i]; if (idx < md->vnode_count) { path = md->vnodes[idx].path; } else { idx -= md->vnode_count; } } if (path == NULL) { for (int i = 0; i < node->devices.vnode_count; i++) { if (node->devices.vnodes[i].claimed) { continue; } if (idx == 0) { path = node->devices.vnodes[i].path; break; } idx--; } } if (path == NULL) { proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); return; } struct V4l2_Ctrl_Handle *handle; e = v4l2_ctrl_open(path, &handle); if (!APP_IS_OK(e)) { proto_write_control_response(conn, req.request_id, PROTO_STATUS_ERROR, NULL, 0); return; } e = v4l2_ctrl_set(handle, req.control_id, req.value); v4l2_ctrl_close(handle); uint16_t status = APP_IS_OK(e) ? PROTO_STATUS_OK : PROTO_STATUS_ERROR; proto_write_control_response(conn, req.request_id, status, NULL, 0); } /* ------------------------------------------------------------------------- * Transport callbacks * ------------------------------------------------------------------------- */ static void on_frame(struct Transport_Conn *conn, struct Transport_Frame *frame, void *userdata) { struct Node *node = userdata; if (frame->message_type == 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)) { free(frame->payload); return; } switch (hdr.command) { case PROTO_CMD_ENUM_DEVICES: handle_enum_devices(node, conn, hdr.request_id); break; case PROTO_CMD_ENUM_CONTROLS: handle_enum_controls(node, conn, frame->payload, frame->payload_length); break; case PROTO_CMD_GET_CONTROL: handle_get_control(node, conn, frame->payload, frame->payload_length); break; case PROTO_CMD_SET_CONTROL: handle_set_control(node, conn, frame->payload, frame->payload_length); break; default: proto_write_control_response(conn, hdr.request_id, PROTO_STATUS_UNKNOWN_CMD, NULL, 0); break; } } free(frame->payload); } static void on_connect(struct Transport_Conn *conn, void *userdata) { (void)conn; (void)userdata; printf("peer connected\n"); } static void on_disconnect(struct Transport_Conn *conn, void *userdata) { (void)conn; (void)userdata; printf("peer disconnected\n"); } /* ------------------------------------------------------------------------- * Config schema * ------------------------------------------------------------------------- */ static const struct Config_Flag_Def function_flag_defs[] = { { "source", DISCOVERY_FLAG_SOURCE }, { "relay", DISCOVERY_FLAG_RELAY }, { "sink", DISCOVERY_FLAG_SINK }, { "controller", DISCOVERY_FLAG_CONTROLLER }, { NULL, 0 } }; static const struct Config_Def schema[] = { { "node", "name", CONFIG_STRING, "unnamed:0", NULL }, { "node", "site_id", CONFIG_UINT16, "0", NULL }, { "node", "tcp_port", CONFIG_UINT16, "8000", NULL }, { "node", "function", CONFIG_FLAGS, "source", function_flag_defs }, { "discovery", "interval_ms", CONFIG_UINT32, "5000", NULL }, { "discovery", "timeout_intervals", CONFIG_UINT32, "3", NULL }, { "transport", "max_connections", CONFIG_UINT32, "16", NULL }, { "transport", "max_payload", CONFIG_UINT32, "16777216", NULL }, { NULL } }; /* ------------------------------------------------------------------------- * Entry point * ------------------------------------------------------------------------- */ static void usage(void) { fprintf(stderr, "usage: video-node \n" " video-node --defaults\n"); } int main(int argc, char **argv) { if (argc < 2) { usage(); return 1; } struct Node node; memset(&node, 0, sizeof(node)); /* Load config */ struct App_Error e; if (strcmp(argv[1], "--defaults") == 0) { e = config_defaults(&node.config, schema); } else { e = config_load(&node.config, argv[1], schema); } if (!APP_IS_OK(e)) { app_error_print(&e); return 1; } uint16_t tcp_port = config_get_u16(node.config, "node", "tcp_port"); uint16_t site_id = config_get_u16(node.config, "node", "site_id"); uint32_t func = config_get_flags(node.config, "node", "function"); const char *name = config_get_str(node.config, "node", "name"); uint32_t interval = config_get_u32(node.config, "discovery", "interval_ms"); uint32_t timeout_i = config_get_u32(node.config, "discovery", "timeout_intervals"); uint32_t max_conn = config_get_u32(node.config, "transport", "max_connections"); uint32_t max_pay = config_get_u32(node.config, "transport", "max_payload"); printf("node: %s port=%u site=%u\n", name, tcp_port, site_id); /* Enumerate devices */ printf("scanning devices...\n"); build_device_list(&node.devices); printf("found %d media device(s), %d video node(s)\n", node.devices.media_count, node.devices.vnode_count); /* Start transport server */ struct Transport_Server_Config srv_cfg = { .port = tcp_port, .max_connections = (int)max_conn, .max_payload = max_pay, .on_frame = on_frame, .on_connect = on_connect, .on_disconnect = on_disconnect, .userdata = &node, }; e = transport_server_create(&node.server, &srv_cfg); if (!APP_IS_OK(e)) { app_error_print(&e); return 1; } e = transport_server_start(node.server); if (!APP_IS_OK(e)) { app_error_print(&e); return 1; } /* Start discovery */ struct Discovery_Config disc_cfg = { .site_id = site_id, .tcp_port = tcp_port, .function_flags = (uint16_t)func, .name = name, .interval_ms = interval, .timeout_intervals= timeout_i, .on_peer_found = NULL, .on_peer_lost = NULL, }; e = discovery_create(&node.discovery, &disc_cfg); if (!APP_IS_OK(e)) { app_error_print(&e); return 1; } e = discovery_start(node.discovery); if (!APP_IS_OK(e)) { app_error_print(&e); return 1; } printf("ready\n"); pause(); return 0; }