From 49e5076eeab8eacabdc69212b9071046c7355cb4 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Fri, 27 Mar 2026 02:00:18 +0000 Subject: [PATCH] Fix menu controls: wire up menu items in ctrl_enum_cb; document control commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/node/main.c: ctrl_enum_cb was discarding menu_count and menu_items, causing empty dropdowns for all MENU/INTEGER_MENU controls. Added a menu item pool (MAX_MENU_POOL=128 items) to Ctrl_Build; the callback now copies items into the pool and sets menu_count/menu_items on the control. docs/protocol.md: add missing sections — str8 primitive, ENUM_DEVICES, ENUM_CONTROLS (with control type/flag tables and menu item notes), GET_CONTROL, and SET_CONTROL schemas. Co-Authored-By: Claude Sonnet 4.6 --- docs/protocol.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++ src/node/main.c | 24 ++++++++- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/docs/protocol.md b/docs/protocol.md index f26754d..c952fa8 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -266,3 +266,128 @@ No Avahi or Bonjour dependency — nodes open a raw UDP multicast socket directl `site_id = 0` means "local / unassigned". Both sides of a site-to-site link will independently default to `0`, so a gateway cannot forward announcements across the boundary without rewriting the field — all nodes on both sides would appear identical. The gateway assigns a distinct non-zero `site_id` to each side and rewrites `site_id` in all announcements (and any protocol messages carrying a `site_id`) as they cross the boundary. Receiving an announcement with `site_id = 0` on a cross-site link is a gateway protocol error. + +--- + +## Serialisation Primitives + +`str8` — a length-prefixed UTF-8 string, not NUL-terminated on the wire: + +``` ++------------+-------------------+ +| length: u8 | bytes: length × u8 | ++------------+-------------------+ +``` + +Maximum string length is 255 bytes. + +--- + +## Control Commands + +All requests follow the `CONTROL_REQUEST` frame format (request_id + command + command-specific fields). All responses follow the `CONTROL_RESPONSE` frame format (request_id + status + response-specific fields). + +### `ENUM_DEVICES` (0x0003) + +**Request** — no extra fields beyond request_id and command. + +**Response** on status `OK`: + +``` ++-------------------+ +| media_count: u16 | ++-------------------+ + × media_count: + +------------+------------+-----------+-------------+-----------------+ + | path: str8 | driver:str8| model:str8| bus_info:str8| vnode_count: u8 | + +------------+------------+-----------+-------------+-----------------+ + × vnode_count: + +------------+-----------------+------------------+-------------------+------------------+----------------+------------------+ + | path: str8 | entity_name:str8| entity_type: u32 | entity_flags: u32 | device_caps: u32 | pad_flags: u8 | is_capture: u8 | + +------------+-----------------+------------------+-------------------+------------------+----------------+------------------+ ++----------------------+ +| standalone_count: u16 | ++----------------------+ + × standalone_count: + +------------+------------+ + | path: str8 | name: str8 | + +------------+------------+ +``` + +`device_caps` carries `V4L2_CAP_*` bits from `VIDIOC_QUERYCAP` (using `device_caps` if `V4L2_CAP_DEVICE_CAPS` is set, otherwise `capabilities`). Notable bits: `0x00000001` = `VIDEO_CAPTURE`, `0x00800000` = `META_CAPTURE`. + +`is_capture` is `1` if `device_caps & V4L2_CAP_VIDEO_CAPTURE`, else `0`. + +### `ENUM_CONTROLS` (0x0004) + +**Request**: + +``` ++--------------------+ +| device_index: u16 | ++--------------------+ +``` + +**Response** on status `OK`: + +``` ++---------------+ +| count: u16 | ++---------------+ + × count: + +---------+---------+----------+------------+----------+---------+----------+-------------+-----------------+------------------+ + | id: u32 | type: u8 | flags: u32 | name: str8 | min: i32 | max: i32 | step: i32 | default: i32 | current: i32 | menu_count: u8 | + +---------+---------+----------+------------+----------+---------+----------+-------------+-----------------+------------------+ + × menu_count: + +------------+------------+------------------+ + | index: u32 | name: str8 | int_value: i64 | + +------------+------------+------------------+ +``` + +`type` values match `V4L2_CTRL_TYPE_*`: + +| Value | Type | +|---|---| +| `1` | `INTEGER` — use a slider | +| `2` | `BOOLEAN` — use a checkbox | +| `3` | `MENU` — use a select; `name` of each menu item is the label | +| `4` | `BUTTON` — use a button | +| `9` | `INTEGER_MENU` — use a select; `int_value` of each menu item is the label | + +`flags` bits (from `V4L2_CTRL_FLAG_*`): `0x0001` = disabled, `0x0002` = grabbed (read-only due to another active control), `0x0004` = read-only. + +For `MENU` and `INTEGER_MENU` controls, set the control value to a menu item's `index` (not its `int_value`). `int_value` is informational display text only. + +Menu items may have non-contiguous `index` values (gaps where the driver returns `EINVAL` for `VIDIOC_QUERYMENU`). + +### `GET_CONTROL` (0x0005) + +**Request**: + +``` ++--------------------+------------------+ +| device_index: u16 | control_id: u32 | ++--------------------+------------------+ +``` + +**Response** on status `OK`: + +``` ++-------------+ +| value: i32 | ++-------------+ +``` + +### `SET_CONTROL` (0x0006) + +**Request**: + +``` ++--------------------+------------------+-------------+ +| device_index: u16 | control_id: u32 | value: i32 | ++--------------------+------------------+-------------+ +``` + +**Response** — no extra fields beyond request_id and status. + +For `MENU` and `INTEGER_MENU` controls, `value` must be a valid menu item `index` as returned by `ENUM_CONTROLS`. diff --git a/src/node/main.c b/src/node/main.c index 9afca5c..83a40dd 100644 --- a/src/node/main.c +++ b/src/node/main.c @@ -195,9 +195,14 @@ static void build_device_list(struct Device_List *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; }; @@ -206,7 +211,6 @@ static void ctrl_enum_cb( uint32_t menu_count, const struct V4l2_Menu_Item *menu_items, void *userdata) { - (void)menu_count; (void)menu_items; struct Ctrl_Build *b = userdata; if (b->count >= MAX_CONTROLS) { return; } @@ -225,6 +229,24 @@ static void ctrl_enum_cb( 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; + } + } } /* -------------------------------------------------------------------------