From 8c4cd69443c3356fbf06ee0a569618db42701b6b Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Sun, 29 Mar 2026 22:02:42 +0000 Subject: [PATCH] Display device controls; device IDs in enum-devices; fix non-OK parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display controls (enum/get/set): - Add PROTO_DISPLAY_CTRL_SCALE/ANCHOR/NO_SIGNAL_FPS constants to protocol.h - handle_enum_controls: if device index maps to an active display slot, return the three display controls (scale, anchor, no_signal_fps) - handle_get_control: read display control values from slot under mutex - handle_set_control: write display control values to slot under mutex; scale/anchor are applied to the viewer by display_loop_tick each tick Device IDs in enum-devices output: - Proto_Display_Device_Info gains device_id field (wire format +2 bytes) - handle_enum_devices computes device_id = total_v4l2 + display_index - on_video_node/on_standalone callbacks take int* userdata to print [idx] - on_display prints [device_id] from the wire field Bug fix — protocol error on invalid device index: - proto_read_enum_controls_response: early-return APP_OK after reading status if status != OK; error responses have no count/data fields, so the CUR_CHECK on count was failing with "payload too short" Helpers added to main.c: - count_v4l2_devices(): sum of media vnodes + standalone - find_display_by_device_idx(): maps flat index to Display_Slot Co-Authored-By: Claude Sonnet 4.6 --- dev/cli/controller_cli.c | 21 ++++-- include/protocol.h | 12 +++ src/modules/protocol/protocol.c | 6 +- src/node/main.c | 127 +++++++++++++++++++++++++++++++- 4 files changed, 155 insertions(+), 11 deletions(-) diff --git a/dev/cli/controller_cli.c b/dev/cli/controller_cli.c index 0a31ae0..e4e1ad4 100644 --- a/dev/cli/controller_cli.c +++ b/dev/cli/controller_cli.c @@ -137,14 +137,17 @@ static void on_video_node( uint8_t pflags, uint8_t is_capture, void *ud) { - (void)eflags; (void)pflags; (void)ud; + (void)eflags; (void)pflags; + int *idx = ud; char caps[128]; caps_str(dcaps, caps, sizeof(caps)); - printf(" video %.*s entity=%.*s type=0x%08x caps=[%s]%s\n", + printf(" [%d] video %.*s entity=%.*s type=0x%08x caps=[%s]%s\n", + *idx, (int)path_len, path, (int)ename_len, ename, etype, caps, is_capture ? " [capture]" : ""); + (*idx)++; } static void on_standalone( @@ -152,10 +155,12 @@ static void on_standalone( const char *name, uint8_t name_len, void *ud) { - (void)ud; - printf(" standalone %.*s card=%.*s\n", + int *idx = ud; + printf(" [%d] standalone %.*s card=%.*s\n", + *idx, (int)path_len, path, (int)name_len, name); + (*idx)++; } static const char *scale_name(uint8_t s) @@ -170,6 +175,7 @@ static const char *scale_name(uint8_t s) } static void on_display( + uint16_t device_id, uint16_t stream_id, int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h, @@ -177,8 +183,8 @@ static void on_display( void *ud) { (void)ud; - printf(" display stream=%u pos=%d,%d size=%ux%u scale=%s anchor=%s\n", - stream_id, win_x, win_y, win_w, win_h, + printf(" [%u] display stream=%u pos=%d,%d size=%ux%u scale=%s anchor=%s\n", + device_id, stream_id, win_x, win_y, win_w, win_h, scale_name(scale), anchor == 0 ? "center" : "topleft"); } @@ -231,9 +237,10 @@ static void on_frame(struct Transport_Conn *conn, switch (cs->pending_cmd) { case PROTO_CMD_ENUM_DEVICES: { struct Proto_Response_Header hdr; + int dev_idx = 0; struct App_Error e = proto_read_enum_devices_response( frame->payload, frame->payload_length, &hdr, - on_media_device, on_video_node, on_standalone, on_display, NULL); + on_media_device, on_video_node, on_standalone, on_display, &dev_idx); 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); diff --git a/include/protocol.h b/include/protocol.h index abe562f..5f27c7b 100644 --- a/include/protocol.h +++ b/include/protocol.h @@ -149,9 +149,11 @@ struct Proto_Standalone_Device_Info { /* * An active display window (video sink role). + * device_id is the flat device index (follows all V4L2 devices). * stream_id is the stream being displayed; win_* are current geometry. */ struct Proto_Display_Device_Info { + uint16_t device_id; uint16_t stream_id; int16_t win_x, win_y; uint16_t win_w, win_h; @@ -159,6 +161,15 @@ struct Proto_Display_Device_Info { uint8_t anchor; }; +/* ------------------------------------------------------------------------- + * Display device pseudo-control IDs — used in ENUM_CONTROLS / GET_CONTROL / + * SET_CONTROL for display device indices returned by ENUM_DEVICES. + * ------------------------------------------------------------------------- */ + +#define PROTO_DISPLAY_CTRL_SCALE 0x00D00001u /* int 0-3: stretch/fit/fill/1:1 */ +#define PROTO_DISPLAY_CTRL_ANCHOR 0x00D00002u /* int 0-1: center/topleft */ +#define PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS 0x00D00003u /* int 1-60: no-signal animation fps */ + struct Proto_Monitor_Info { int32_t x, y; uint32_t width, height; @@ -495,6 +506,7 @@ struct App_Error proto_read_enum_devices_response( const char *name, uint8_t name_len, void *userdata), void (*on_display)( + uint16_t device_id, uint16_t stream_id, int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h, diff --git a/src/modules/protocol/protocol.c b/src/modules/protocol/protocol.c index d69f46b..cf6729f 100644 --- a/src/modules/protocol/protocol.c +++ b/src/modules/protocol/protocol.c @@ -414,6 +414,7 @@ struct App_Error proto_write_enum_devices_response(struct Transport_Conn *conn, e = wbuf_u16(&b, display_count); if (!APP_IS_OK(e)) { goto fail; } for (uint16_t i = 0; i < display_count; i++) { const struct Proto_Display_Device_Info *d = &displays[i]; + e = wbuf_u16(&b, d->device_id); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_u16(&b, d->stream_id); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_i16(&b, d->win_x); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_i16(&b, d->win_y); if (!APP_IS_OK(e)) { goto fail; } @@ -791,6 +792,7 @@ struct App_Error proto_read_enum_devices_response( uint16_t display_count = cur_u16(&c); CUR_CHECK(c); for (uint16_t i = 0; i < display_count; i++) { + uint16_t device_id = cur_u16(&c); uint16_t stream_id = cur_u16(&c); int16_t win_x = (int16_t)cur_u16(&c); int16_t win_y = (int16_t)cur_u16(&c); @@ -800,7 +802,7 @@ struct App_Error proto_read_enum_devices_response( uint8_t anchor = cur_u8(&c); CUR_CHECK(c); if (on_display) { - on_display(stream_id, win_x, win_y, + on_display(device_id, stream_id, win_x, win_y, win_w, win_h, scale, anchor, userdata); } } @@ -830,6 +832,8 @@ struct App_Error proto_read_enum_controls_response( header_out->request_id = cur_u16(&c); header_out->status = cur_u16(&c); + CUR_CHECK(c); + if (header_out->status != PROTO_STATUS_OK) { return APP_OK; } uint16_t count = cur_u16(&c); CUR_CHECK(c); diff --git a/src/node/main.c b/src/node/main.c index 5af3cfe..cc5fa0d 100644 --- a/src/node/main.c +++ b/src/node/main.c @@ -510,6 +510,14 @@ static void display_loop_tick(struct Node *node) if (d->current_state != DISP_OPEN || !d->viewer) { continue; } + /* Sync scale/anchor — may be updated live via SET_CONTROL */ + pthread_mutex_lock(&d->mutex); + Xorg_Scale cur_scale = d->scale; + Xorg_Anchor cur_anchor = d->anchor; + pthread_mutex_unlock(&d->mutex); + xorg_viewer_set_scale(d->viewer, cur_scale); + xorg_viewer_set_anchor(d->viewer, cur_anchor); + /* Deliver pending frame (no lock held during decode/upload) */ pthread_mutex_lock(&d->mutex); uint8_t *fdata = NULL; @@ -787,6 +795,43 @@ static const char *resolve_device_path(struct Node *node, int idx) return NULL; } +/* Count all V4L2 device indices (media vnodes + standalone). */ +static int count_v4l2_devices(struct Node *node) +{ + int n = 0; + for (int i = 0; i < node->devices.media_count; i++) { + n += node->devices.media[i].vnode_count; + } + for (int i = 0; i < node->devices.vnode_count; i++) { + if (!node->devices.vnodes[i].claimed) { n++; } + } + return n; +} + +/* + * If idx falls in the display device range (>= count_v4l2_devices), return + * the corresponding active Display_Slot, or NULL if out of range. + * The slot may be read without holding its mutex for the non-mutex fields; + * callers should lock the mutex for fields that require it. + */ +static struct Display_Slot *find_display_by_device_idx(struct Node *node, int idx) +{ + int base = count_v4l2_devices(node); + if (idx < base) { return NULL; } + int disp_idx = idx - base; + int found = 0; + for (int i = 0; i < MAX_DISPLAYS; i++) { + struct Display_Slot *d = &node->displays[i]; + pthread_mutex_lock(&d->mutex); + int active = d->allocated && d->wanted_state == DISP_OPEN; + pthread_mutex_unlock(&d->mutex); + if (!active) { continue; } + if (found == disp_idx) { return d; } + found++; + } + return NULL; +} + /* ------------------------------------------------------------------------- * Request handlers * ------------------------------------------------------------------------- */ @@ -829,6 +874,8 @@ static void handle_enum_devices(struct Node *node, standalone_count++; } + int total_v4l2 = vnode_offset + standalone_count; + struct Proto_Display_Device_Info disp_infos[MAX_DISPLAYS]; int disp_count = 0; for (int i = 0; i < MAX_DISPLAYS; i++) { @@ -836,6 +883,7 @@ static void handle_enum_devices(struct Node *node, pthread_mutex_lock(&d->mutex); int snap = d->allocated && d->wanted_state == DISP_OPEN; struct Proto_Display_Device_Info info = { + .device_id = (uint16_t)(total_v4l2 + disp_count), .stream_id = d->stream_id, .win_x = (int16_t)d->win_x, .win_y = (int16_t)d->win_y, @@ -869,7 +917,33 @@ static void handle_enum_controls(struct Node *node, } const char *path = resolve_device_path(node, (int)req.device_index); if (!path) { - proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); + struct Display_Slot *disp = find_display_by_device_idx(node, (int)req.device_index); + if (!disp) { + proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); + return; + } + pthread_mutex_lock(&disp->mutex); + int scale = (int)disp->scale; + int anchor = (int)disp->anchor; + int no_signal_fps = disp->no_signal_fps > 0 ? disp->no_signal_fps : 15; + pthread_mutex_unlock(&disp->mutex); + struct Proto_Control_Info ctrls[] = { + { .id = PROTO_DISPLAY_CTRL_SCALE, + .type = 1, .name = "Scale", + .min = 0, .max = 3, .step = 1, .default_val = 1, + .current_val = scale }, + { .id = PROTO_DISPLAY_CTRL_ANCHOR, + .type = 1, .name = "Anchor", + .min = 0, .max = 1, .step = 1, .default_val = 0, + .current_val = anchor }, + { .id = PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS, + .type = 1, .name = "No-signal FPS", + .min = 1, .max = 60, .step = 1, .default_val = 15, + .current_val = no_signal_fps }, + }; + e = proto_write_enum_controls_response(conn, + req.request_id, PROTO_STATUS_OK, ctrls, 3); + if (!APP_IS_OK(e)) { app_error_print(&e); } return; } struct V4l2_Ctrl_Handle *handle; @@ -898,7 +972,27 @@ static void handle_get_control(struct Node *node, } const char *path = resolve_device_path(node, (int)req.device_index); if (!path) { - proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); + struct Display_Slot *disp = find_display_by_device_idx(node, (int)req.device_index); + if (!disp) { + proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); + return; + } + pthread_mutex_lock(&disp->mutex); + int32_t value = 0; + int found = 1; + switch (req.control_id) { + case PROTO_DISPLAY_CTRL_SCALE: value = (int32_t)disp->scale; break; + case PROTO_DISPLAY_CTRL_ANCHOR: value = (int32_t)disp->anchor; break; + case PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS: value = disp->no_signal_fps > 0 ? disp->no_signal_fps : 15; break; + default: found = 0; break; + } + pthread_mutex_unlock(&disp->mutex); + if (!found) { + proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, 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); } return; } struct V4l2_Ctrl_Handle *handle; @@ -930,7 +1024,34 @@ static void handle_set_control(struct Node *node, } const char *path = resolve_device_path(node, (int)req.device_index); if (!path) { - proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); + struct Display_Slot *disp = find_display_by_device_idx(node, (int)req.device_index); + if (!disp) { + proto_write_control_response(conn, req.request_id, PROTO_STATUS_NOT_FOUND, NULL, 0); + return; + } + pthread_mutex_lock(&disp->mutex); + int found = 1; + switch (req.control_id) { + case PROTO_DISPLAY_CTRL_SCALE: + if (req.value >= 0 && req.value <= 3) { + disp->scale = (Xorg_Scale)req.value; + } + break; + case PROTO_DISPLAY_CTRL_ANCHOR: + if (req.value >= 0 && req.value <= 1) { + disp->anchor = (Xorg_Anchor)req.value; + } + break; + case PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS: + if (req.value >= 1 && req.value <= 60) { + disp->no_signal_fps = (int)req.value; + } + break; + default: found = 0; break; + } + pthread_mutex_unlock(&disp->mutex); + uint16_t status = found ? PROTO_STATUS_OK : PROTO_STATUS_NOT_FOUND; + proto_write_control_response(conn, req.request_id, status, NULL, 0); return; } struct V4l2_Ctrl_Handle *handle;