Compare commits
26 Commits
b7e87ceb46
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 92ba1adf29 | |||
| f3a6be0701 | |||
| a5198e84d2 | |||
| 780f45b46f | |||
| b4facf04be | |||
| 8fa2f33bad | |||
| 7777292dfd | |||
| 87b9800e41 | |||
| 86f135792f | |||
| 44090c1d6d | |||
| 8c4cd69443 | |||
| 1066f793e2 | |||
| 15f4a0f560 | |||
| ae2cc51626 | |||
| 835cbbafba | |||
| 961933e714 | |||
| 2481c3bae4 | |||
| 8460841e8e | |||
| 30ad5fbeae | |||
| d6fe653a2e | |||
| ba2c3cb6cd | |||
| 54d48c9c8e | |||
| 7808d832be | |||
| f8ecade810 | |||
| 996397d615 | |||
| edf2208e08 |
2
Makefile
2
Makefile
@@ -16,6 +16,8 @@ modules:
|
||||
$(MAKE) -C src/modules/protocol
|
||||
$(MAKE) -C src/modules/test_image
|
||||
$(MAKE) -C src/modules/xorg
|
||||
$(MAKE) -C src/modules/reconciler
|
||||
$(MAKE) -C src/modules/ingest
|
||||
|
||||
cli: modules
|
||||
$(MAKE) -C dev/cli
|
||||
|
||||
26
README.md
26
README.md
@@ -15,11 +15,18 @@ Designed to run on resource-constrained hardware (Raspberry Pi capturing raw MJP
|
||||
|
||||
- [docs/cli/media_ctrl_cli.md](docs/cli/media_ctrl_cli.md) — media device and topology tool
|
||||
- [docs/cli/v4l2_ctrl_cli.md](docs/cli/v4l2_ctrl_cli.md) — V4L2 camera control tool
|
||||
- `test_image_cli` — generate test patterns and write PPM output for visual inspection
|
||||
- `xorg_cli` — display a test pattern in the viewer window; exercises scale/anchor modes and text overlays
|
||||
- `v4l2_view_cli` — live camera viewer; auto-selects highest-FPS format, displays FPS overlay
|
||||
- `stream_send_cli` — capture MJPEG from V4L2, connect to a receiver over TCP, stream VIDEO_FRAME messages with per-stream stats
|
||||
- `stream_recv_cli` — listen for incoming TCP stream, display received MJPEG frames with fps/Mbps overlay
|
||||
- [docs/cli/transport_cli.md](docs/cli/transport_cli.md) — send/receive framed messages, inspect transport frame headers
|
||||
- [docs/cli/discovery_cli.md](docs/cli/discovery_cli.md) — announce and discover peers over UDP multicast; print found/lost events
|
||||
- [docs/cli/config_cli.md](docs/cli/config_cli.md) — load an INI config file and print the resolved values after applying schema defaults
|
||||
- [docs/cli/protocol_cli.md](docs/cli/protocol_cli.md) — send and receive typed protocol messages; inspect frame payloads
|
||||
- [docs/cli/query_cli.md](docs/cli/query_cli.md) — wait for first discovered node, send ENUM_DEVICES, print results
|
||||
- [docs/cli/test_image_cli.md](docs/cli/test_image_cli.md) — generate test patterns and write PPM output for visual inspection
|
||||
- [docs/cli/xorg_cli.md](docs/cli/xorg_cli.md) — display a test pattern in the viewer window; exercises scale/anchor modes and text overlays
|
||||
- [docs/cli/v4l2_view_cli.md](docs/cli/v4l2_view_cli.md) — live camera viewer; auto-selects highest-FPS format, displays FPS overlay
|
||||
- [docs/cli/stream_send_cli.md](docs/cli/stream_send_cli.md) — capture MJPEG from V4L2, connect to a receiver over TCP, stream VIDEO_FRAME messages with per-stream stats
|
||||
- [docs/cli/stream_recv_cli.md](docs/cli/stream_recv_cli.md) — listen for incoming TCP stream, display received MJPEG frames with fps/Mbps overlay
|
||||
- [docs/cli/reconciler_cli.md](docs/cli/reconciler_cli.md) — simulated state machine experiment; validate the reconciler with fake resources before wiring into the node
|
||||
- [docs/cli/controller_cli.md](docs/cli/controller_cli.md) — interactive REPL; connects to nodes by peer index or host:port; enum-devices, enum-controls, get/set-control, start/stop-ingest, start/stop-display
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -34,7 +41,7 @@ docs/ documentation
|
||||
|
||||
## Status
|
||||
|
||||
Core modules and the video node binary are working. The node can be queried over the wire protocol for device enumeration and V4L2 camera control. The development web UI connects to live nodes for inspection and control. The xorg viewer sink is implemented (GLFW+OpenGL, all scale/anchor modes, bitmap font atlas text overlays). A V4L2 capture viewer (`v4l2_view_cli`) demonstrates live camera display without going through the node system. Frame ingest-to-wire and relay have not started.
|
||||
Core modules and the video node binary are working end-to-end. The node can be queried over the wire protocol for device enumeration and V4L2 camera control. V4L2 ingest is live — a source node captures MJPEG and streams it to a sink node which displays it in an xorg window. The node supports both source (START_INGEST) and display sink (START_DISPLAY) roles. A reconciler manages V4L2 device and transport connection state. The development web UI connects to live nodes for inspection and control. Relay, archive, and codec have not started.
|
||||
|
||||
| Module | Status | Notes |
|
||||
|---|---|---|
|
||||
@@ -46,13 +53,14 @@ Core modules and the video node binary are working. The node can be queried over
|
||||
| `transport` | done | Framed TCP stream, single-write send |
|
||||
| `discovery` | done | UDP multicast announcements, peer table, found/lost callbacks |
|
||||
| `protocol` | done | Typed write/read functions for all message types |
|
||||
| `node` | done | Video node binary — config, discovery, transport server, V4L2/media request handlers |
|
||||
| `dev/web` | done | Development web UI — connects to live nodes, V4L2 inspection and control |
|
||||
| `test_image` | done | Test pattern generator — colour bars, luminance ramp, grid; YUV420/BGRA output |
|
||||
| `xorg` | done | GLFW+OpenGL viewer sink — YUV420/BGRA/MJPEG input, all scale/anchor modes, bitmap font atlas text overlays; screen grab and XRandR queries not yet implemented |
|
||||
| `reconciler` | done | Generic wanted/current state machine reconciler — BFS pathfinding, event + periodic tick |
|
||||
| `ingest` | done | V4L2 capture loop — open device, negotiate MJPEG, MMAP buffers, capture thread with on_frame callback |
|
||||
| `node` | done | Video node binary — source role (START/STOP_INGEST) and display sink role (START/STOP_DISPLAY); multi-window xorg viewer; declarative reconciler for device and connection state |
|
||||
| `dev/web` | done | Development web UI — connects to live nodes, V4L2 inspection and control |
|
||||
| `frame_alloc` | not started | Per-frame allocation with byte budget and ref counting |
|
||||
| `relay` | not started | Input dispatch to output queues (low-latency and completeness modes) |
|
||||
| `ingest` | not started | V4L2 capture loop — dequeue buffers, emit frames |
|
||||
| `archive` | not started | Write frames to disk, control messages to binary log |
|
||||
| `codec` | not started | Per-frame encode/decode (MJPEG, QOI, ZSTD-raw, VA-API H.264) |
|
||||
| `web node` | not started | Node.js peer — binary protocol socket side + HTTP/WebSocket to browser |
|
||||
|
||||
@@ -77,9 +77,10 @@ Errors are returned as `struct App_Error` values. Functions that can fail return
|
||||
/* modules/common/error.h */
|
||||
|
||||
typedef enum Error_Code {
|
||||
ERR_NONE = 0,
|
||||
ERR_SYSCALL = 1, /* errno is meaningful */
|
||||
ERR_INVALID = 2,
|
||||
ERR_NONE = 0,
|
||||
ERR_SYSCALL = 1, /* errno is meaningful */
|
||||
ERR_INVALID = 2,
|
||||
ERR_NOT_FOUND = 3,
|
||||
} Error_Code;
|
||||
|
||||
struct Syscall_Error_Detail {
|
||||
@@ -87,7 +88,8 @@ struct Syscall_Error_Detail {
|
||||
};
|
||||
|
||||
struct Invalid_Error_Detail {
|
||||
/* fields added as concrete cases arise */
|
||||
int config_line; /* source line number, or 0 if not applicable */
|
||||
const char *message; /* static string describing what was wrong */
|
||||
};
|
||||
|
||||
struct App_Error {
|
||||
@@ -110,16 +112,17 @@ struct App_Error {
|
||||
#define APP_IS_OK(e) \
|
||||
((e).code == ERR_NONE)
|
||||
|
||||
#define APP_ERROR(error_code, detail_field, ...) \
|
||||
((struct App_Error){ \
|
||||
.code = (error_code), \
|
||||
.file = __FILE__, \
|
||||
.line = __LINE__, \
|
||||
.detail = { .detail_field = { __VA_ARGS__ } } \
|
||||
})
|
||||
|
||||
#define APP_SYSCALL_ERROR() \
|
||||
APP_ERROR(ERR_SYSCALL, syscall, .err_no = errno)
|
||||
/* sets ERR_SYSCALL + captures errno */
|
||||
|
||||
#define APP_INVALID_ERROR() \
|
||||
/* sets ERR_INVALID, no message */
|
||||
|
||||
#define APP_INVALID_ERROR_MSG(cfg_line, msg) \
|
||||
/* sets ERR_INVALID with config_line and message */
|
||||
|
||||
#define APP_NOT_FOUND_ERROR() \
|
||||
/* sets ERR_NOT_FOUND */
|
||||
```
|
||||
|
||||
### Presentation
|
||||
@@ -140,11 +143,13 @@ video-setup/
|
||||
modules/ - translation units; each has a .c and a Makefile
|
||||
common/ - shared types (error, base definitions); no external dependencies
|
||||
<module>/
|
||||
node/ - video node entry point and integration (later)
|
||||
node/ - video node binary (source + display sink roles)
|
||||
include/ - public headers (.h files)
|
||||
dev/ - development aids; not part of the final deliverable
|
||||
cli/ - exploratory CLI drivers, one per module
|
||||
web/ - development web UI (Node.js/Express)
|
||||
experiments/ - freeform experiments
|
||||
tools/ - build-time code generators (e.g. gen_font_atlas)
|
||||
tests/ - automated tests (later)
|
||||
Makefile - top-level build
|
||||
```
|
||||
|
||||
@@ -109,8 +109,8 @@ $(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)/controller_cli: $(CLI_BUILD)/controller_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(DISCOVERY_OBJ) $(PROTOCOL_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread -lreadline
|
||||
|
||||
$(CLI_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
@@ -3,11 +3,82 @@
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <semaphore.h>
|
||||
#include <pthread.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <readline/readline.h>
|
||||
#include <readline/history.h>
|
||||
|
||||
#include "transport.h"
|
||||
#include "protocol.h"
|
||||
#include "discovery.h"
|
||||
#include "error.h"
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Discovery peer table
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
#define MAX_PEERS 16
|
||||
|
||||
struct Peer_Entry {
|
||||
char host[64];
|
||||
uint16_t port;
|
||||
char name[DISCOVERY_MAX_NAME_LEN + 1];
|
||||
};
|
||||
|
||||
static struct Peer_Entry peer_table[MAX_PEERS];
|
||||
static int peer_count = 0;
|
||||
static pthread_mutex_t peer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void on_peer_found(const struct Discovery_Peer *peer, void *ud)
|
||||
{
|
||||
(void)ud;
|
||||
pthread_mutex_lock(&peer_mutex);
|
||||
if (peer_count < MAX_PEERS) {
|
||||
struct in_addr a;
|
||||
a.s_addr = peer->addr;
|
||||
inet_ntop(AF_INET, &a, peer_table[peer_count].host,
|
||||
sizeof(peer_table[0].host));
|
||||
peer_table[peer_count].port = peer->tcp_port;
|
||||
strncpy(peer_table[peer_count].name, peer->name, DISCOVERY_MAX_NAME_LEN);
|
||||
peer_table[peer_count].name[DISCOVERY_MAX_NAME_LEN] = '\0';
|
||||
peer_count++;
|
||||
/* Print inline — readline will redraw the prompt */
|
||||
printf("\n[discovered %d] %s %s:%u\n",
|
||||
peer_count - 1,
|
||||
peer_table[peer_count - 1].name,
|
||||
peer_table[peer_count - 1].host,
|
||||
peer->tcp_port);
|
||||
rl_on_new_line();
|
||||
rl_redisplay();
|
||||
}
|
||||
pthread_mutex_unlock(&peer_mutex);
|
||||
}
|
||||
|
||||
static void on_peer_lost(const struct Discovery_Peer *peer, void *ud)
|
||||
{
|
||||
(void)ud;
|
||||
struct in_addr a;
|
||||
a.s_addr = peer->addr;
|
||||
char host[64];
|
||||
inet_ntop(AF_INET, &a, host, sizeof(host));
|
||||
|
||||
pthread_mutex_lock(&peer_mutex);
|
||||
for (int i = 0; i < peer_count; i++) {
|
||||
if (strcmp(peer_table[i].host, host) == 0 &&
|
||||
peer_table[i].port == peer->tcp_port) {
|
||||
printf("\n[lost] %s %s:%u\n",
|
||||
peer_table[i].name, host, peer->tcp_port);
|
||||
rl_on_new_line();
|
||||
rl_redisplay();
|
||||
memmove(&peer_table[i], &peer_table[i + 1],
|
||||
(size_t)(peer_count - i - 1) * sizeof(peer_table[0]));
|
||||
peer_count--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&peer_mutex);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Shared state between REPL and transport read thread
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -66,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(
|
||||
@@ -81,10 +155,38 @@ 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_mode_name(uint8_t s)
|
||||
{
|
||||
switch (s) {
|
||||
case 0: return "stretch";
|
||||
case 1: return "fit";
|
||||
case 2: return "fill";
|
||||
case 3: return "1:1";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
uint8_t scale_mode, uint8_t anchor,
|
||||
void *ud)
|
||||
{
|
||||
(void)ud;
|
||||
printf(" [%u] display stream=%u pos=%d,%d size=%ux%u scale_mode=%s anchor=%s\n",
|
||||
device_id, stream_id, win_x, win_y, win_w, win_h,
|
||||
scale_mode_name(scale_mode),
|
||||
anchor == 0 ? "center" : "topleft");
|
||||
}
|
||||
|
||||
static void on_control(
|
||||
@@ -135,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, 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);
|
||||
@@ -194,7 +297,9 @@ static void on_frame(struct Transport_Conn *conn,
|
||||
static void on_disconnect(struct Transport_Conn *conn, void *userdata)
|
||||
{
|
||||
(void)conn; (void)userdata;
|
||||
printf("disconnected from node\n");
|
||||
printf("\ndisconnected from node\n");
|
||||
rl_on_new_line();
|
||||
rl_redisplay();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
@@ -307,22 +412,25 @@ static void cmd_start_display(struct Transport_Conn *conn,
|
||||
int ntok, char *tokens[])
|
||||
{
|
||||
/* Required: stream_id
|
||||
* Optional: win_x win_y win_w win_h */
|
||||
* Optional: win_x win_y win_w win_h no_signal_fps */
|
||||
if (ntok < 2) {
|
||||
printf("usage: start-display <stream_id> [win_x] [win_y] [win_w] [win_h]\n");
|
||||
printf("usage: start-display <stream_id> [win_x] [win_y] [win_w] [win_h] [no_signal_fps]\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);
|
||||
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;
|
||||
uint8_t no_signal_fps = ntok > 6 ? (uint8_t)atoi(tokens[6]) : 0;
|
||||
printf("start-display: stream=%u pos=%d,%d size=%ux%u no_signal_fps=%u\n",
|
||||
stream_id, win_x, win_y, win_w, win_h,
|
||||
no_signal_fps > 0 ? no_signal_fps : 15);
|
||||
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));
|
||||
PROTO_DISPLAY_SCALE_FIT, PROTO_DISPLAY_ANCHOR_CENTER,
|
||||
no_signal_fps));
|
||||
}
|
||||
|
||||
static void cmd_stop_display(struct Transport_Conn *conn,
|
||||
@@ -338,6 +446,8 @@ static void cmd_stop_display(struct Transport_Conn *conn,
|
||||
static void cmd_help(void)
|
||||
{
|
||||
printf("commands:\n"
|
||||
" peers list discovered nodes\n"
|
||||
" connect [idx|host:port] connect to peer (no arg = first discovered)\n"
|
||||
" enum-devices\n"
|
||||
" enum-controls <device_index>\n"
|
||||
" get-control <device_index> <control_id_hex>\n"
|
||||
@@ -345,7 +455,7 @@ static void cmd_help(void)
|
||||
" 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"
|
||||
" start-display <stream_id> [win_x] [win_y] [win_w] [win_h] [no_signal_fps]\n"
|
||||
" stop-display <stream_id>\n"
|
||||
" help\n"
|
||||
" quit / exit\n");
|
||||
@@ -358,40 +468,79 @@ static void cmd_help(void)
|
||||
static void usage(void)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: controller_cli --host HOST [--port PORT]\n"
|
||||
"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");
|
||||
" --host HOST connect directly on startup\n"
|
||||
" --port PORT TCP port (default 8000; used with --host)\n"
|
||||
"\n"
|
||||
" Without --host: starts discovery and waits for nodes.\n"
|
||||
" Use 'connect' in the REPL to connect to a discovered node.\n");
|
||||
}
|
||||
|
||||
/* Attempt to connect/reconnect; prints result. Returns new conn or NULL. */
|
||||
static struct Transport_Conn *do_connect(struct Ctrl_State *cs,
|
||||
const char *host, uint16_t port,
|
||||
struct Transport_Conn *old_conn)
|
||||
{
|
||||
if (old_conn) { transport_conn_close(old_conn); }
|
||||
/* Reset state — drain stale semaphore posts from the old connection */
|
||||
cs->pending_cmd = 0;
|
||||
while (sem_trywait(&cs->sem) == 0) { /* drain */ }
|
||||
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 NULL;
|
||||
}
|
||||
printf("connected to %s:%u\n", host, port);
|
||||
return conn;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *host = NULL;
|
||||
uint16_t port = 8000;
|
||||
const char *init_host = NULL;
|
||||
uint16_t init_port = 8000;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
|
||||
host = argv[++i];
|
||||
init_host = argv[++i];
|
||||
} else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
|
||||
port = (uint16_t)atoi(argv[++i]);
|
||||
init_port = (uint16_t)atoi(argv[++i]);
|
||||
} else {
|
||||
usage(); return 1;
|
||||
}
|
||||
}
|
||||
if (!host) { usage(); return 1; }
|
||||
|
||||
/* Connect */
|
||||
/* Start discovery (always — useful even when --host given, for 'peers') */
|
||||
struct Discovery *disc = NULL;
|
||||
struct Discovery_Config dcfg = {0};
|
||||
dcfg.site_id = 0;
|
||||
dcfg.tcp_port = 0;
|
||||
dcfg.function_flags = DISCOVERY_FLAG_CONTROLLER;
|
||||
dcfg.name = "controller_cli";
|
||||
dcfg.on_peer_found = on_peer_found;
|
||||
dcfg.on_peer_lost = on_peer_lost;
|
||||
if (!APP_IS_OK(discovery_create(&disc, &dcfg)) ||
|
||||
!APP_IS_OK(discovery_start(disc))) {
|
||||
fprintf(stderr, "warning: discovery failed to start\n");
|
||||
disc = NULL;
|
||||
}
|
||||
|
||||
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; }
|
||||
struct Transport_Conn *conn = NULL;
|
||||
|
||||
if (init_host) {
|
||||
conn = do_connect(&cs, init_host, init_port, NULL);
|
||||
if (!conn) { return 1; }
|
||||
} else {
|
||||
printf("listening for nodes — type 'peers' to list, 'connect' to connect\n");
|
||||
}
|
||||
|
||||
printf("connected to %s:%u\n\n", host, port);
|
||||
cmd_help();
|
||||
printf("\n");
|
||||
|
||||
@@ -400,22 +549,17 @@ int main(int argc, char **argv)
|
||||
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';
|
||||
}
|
||||
char *rl_line = readline(conn ? "> " : "(no node) > ");
|
||||
if (!rl_line) { break; }
|
||||
if (*rl_line) { add_history(rl_line); }
|
||||
strncpy(line, rl_line, sizeof(line) - 1);
|
||||
line[sizeof(line) - 1] = '\0';
|
||||
free(rl_line);
|
||||
|
||||
/* 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; }
|
||||
@@ -429,8 +573,71 @@ int main(int argc, char **argv)
|
||||
|
||||
if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) {
|
||||
break;
|
||||
|
||||
} else if (strcmp(cmd, "help") == 0) {
|
||||
cmd_help();
|
||||
|
||||
} else if (strcmp(cmd, "peers") == 0) {
|
||||
pthread_mutex_lock(&peer_mutex);
|
||||
if (peer_count == 0) {
|
||||
printf("no peers discovered yet\n");
|
||||
} else {
|
||||
for (int i = 0; i < peer_count; i++) {
|
||||
printf(" [%d] %s %s:%u\n", i,
|
||||
peer_table[i].name,
|
||||
peer_table[i].host,
|
||||
peer_table[i].port);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&peer_mutex);
|
||||
|
||||
} else if (strcmp(cmd, "connect") == 0) {
|
||||
char host[64];
|
||||
uint16_t port = 8000;
|
||||
|
||||
if (ntok < 2) {
|
||||
/* No argument — connect to first discovered peer */
|
||||
pthread_mutex_lock(&peer_mutex);
|
||||
int ok = peer_count > 0;
|
||||
if (ok) {
|
||||
strncpy(host, peer_table[0].host, sizeof(host) - 1);
|
||||
host[sizeof(host) - 1] = '\0';
|
||||
port = peer_table[0].port;
|
||||
}
|
||||
pthread_mutex_unlock(&peer_mutex);
|
||||
if (!ok) {
|
||||
printf("no peers discovered yet — try 'peers'\n");
|
||||
continue;
|
||||
}
|
||||
} else if (strchr(tokens[1], ':')) {
|
||||
/* host:port */
|
||||
char *colon = strchr(tokens[1], ':');
|
||||
size_t hlen = (size_t)(colon - tokens[1]);
|
||||
if (hlen >= sizeof(host)) { hlen = sizeof(host) - 1; }
|
||||
memcpy(host, tokens[1], hlen);
|
||||
host[hlen] = '\0';
|
||||
port = (uint16_t)atoi(colon + 1);
|
||||
} else {
|
||||
/* numeric index into peer table */
|
||||
int idx = atoi(tokens[1]);
|
||||
pthread_mutex_lock(&peer_mutex);
|
||||
int ok = idx >= 0 && idx < peer_count;
|
||||
if (ok) {
|
||||
strncpy(host, peer_table[idx].host, sizeof(host) - 1);
|
||||
host[sizeof(host) - 1] = '\0';
|
||||
port = peer_table[idx].port;
|
||||
}
|
||||
pthread_mutex_unlock(&peer_mutex);
|
||||
if (!ok) {
|
||||
printf("index %d out of range — try 'peers'\n", idx);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
conn = do_connect(&cs, host, port, conn);
|
||||
|
||||
} else if (!conn) {
|
||||
printf("not connected — use 'connect' to connect to a node\n");
|
||||
|
||||
} else if (strcmp(cmd, "enum-devices") == 0) {
|
||||
cmd_enum_devices(conn, &cs, &req_id);
|
||||
} else if (strcmp(cmd, "enum-controls") == 0) {
|
||||
@@ -457,7 +664,8 @@ int main(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
transport_conn_close(conn);
|
||||
if (conn) { transport_conn_close(conn); }
|
||||
if (disc) { discovery_destroy(disc); }
|
||||
sem_destroy(&cs.sem);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ static void on_frame(struct Transport_Conn *conn,
|
||||
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);
|
||||
on_media_device, on_video_node, on_standalone, NULL, NULL);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); }
|
||||
else if (hdr.status != PROTO_STATUS_OK) {
|
||||
fprintf(stderr, "ENUM_DEVICES failed: status=%u\n", hdr.status);
|
||||
|
||||
80
docs/cli/config_cli.md
Normal file
80
docs/cli/config_cli.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# config_cli
|
||||
|
||||
A development tool for loading and inspecting INI configuration files against the node's schema. Useful for verifying that a config file parses correctly and seeing what values would be applied.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Load a config file
|
||||
|
||||
```sh
|
||||
./config_cli <file>
|
||||
```
|
||||
|
||||
Parses the file and prints all resolved values. Values absent from the file are filled with schema defaults.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
./config_cli /etc/video-node/node.cfg
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
[node]
|
||||
name = camera:0
|
||||
site_id = 0
|
||||
tcp_port = 8000
|
||||
function = source
|
||||
|
||||
[discovery]
|
||||
interval_ms = 5000
|
||||
timeout_intervals = 3
|
||||
|
||||
[transport]
|
||||
max_connections = 16
|
||||
```
|
||||
|
||||
### Print schema defaults
|
||||
|
||||
```sh
|
||||
./config_cli --defaults
|
||||
```
|
||||
|
||||
Prints all keys with their default values and types, without reading any file.
|
||||
|
||||
---
|
||||
|
||||
## Config Schema
|
||||
|
||||
| Section | Key | Type | Default |
|
||||
|---|---|---|---|
|
||||
| `node` | `name` | string | `unnamed:0` |
|
||||
| `node` | `site_id` | uint16 | `0` |
|
||||
| `node` | `tcp_port` | uint16 | `0` (auto) |
|
||||
| `node` | `function` | flags | `source` |
|
||||
| `discovery` | `interval_ms` | uint32 | `5000` |
|
||||
| `discovery` | `timeout_intervals` | uint32 | `3` |
|
||||
| `transport` | `max_connections` | uint32 | `16` |
|
||||
|
||||
`function` accepts a comma-separated list of role flags: `source`, `relay`, `sink`, `controller`.
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`config_cli` exercises the `config` module used by the video node at startup. Both the node binary and `config_cli` share the same schema definition, so this tool is an accurate preview of how the node will interpret a config file.
|
||||
172
docs/cli/controller_cli.md
Normal file
172
docs/cli/controller_cli.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# controller_cli
|
||||
|
||||
An interactive REPL for controlling video nodes. Connects to a running node over TCP and lets you enumerate devices and controls, adjust camera parameters, start and stop ingest streams, and open and close display windows. Uses readline for line editing and history.
|
||||
|
||||
> **Note:** `controller_cli` is a temporary development tool. The long-term replacement is a dedicated `controller` binary that maintains simultaneous connections to all discovered nodes rather than switching between them.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`. Requires `libreadline`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./controller_cli [--host HOST] [--port PORT]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--host HOST` | — | Connect to this host on startup |
|
||||
| `--port PORT` | `8000` | TCP port to use with `--host` |
|
||||
|
||||
Without `--host`, the tool starts in discovery mode and waits for nodes to appear. Connect to a discovered node from the REPL using the `connect` command.
|
||||
|
||||
---
|
||||
|
||||
## REPL Commands
|
||||
|
||||
### Discovery
|
||||
|
||||
```
|
||||
peers
|
||||
```
|
||||
|
||||
List all currently discovered nodes with their index, name, host, and port.
|
||||
|
||||
```
|
||||
connect [idx | host:port]
|
||||
```
|
||||
|
||||
Connect to a node. With no argument, connects to the first discovered peer. Examples:
|
||||
|
||||
```
|
||||
connect # first discovered peer
|
||||
connect 1 # peer at index 1 in the peers list
|
||||
connect 192.168.1.42:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Device enumeration
|
||||
|
||||
```
|
||||
enum-devices
|
||||
```
|
||||
|
||||
List all devices on the connected node: media controller devices and their video nodes (with device index), standalone V4L2 devices, and active display windows. Each entry shows its flat device index `[N]` for use in control commands.
|
||||
|
||||
```
|
||||
enum-controls <device_index>
|
||||
```
|
||||
|
||||
List all controls for a device. For V4L2 devices this returns camera parameters; for display windows it returns display controls.
|
||||
|
||||
**Display device controls:**
|
||||
|
||||
| ID | Name | Range | Description |
|
||||
|---|---|---|---|
|
||||
| `0x00D00001` | Scale | 0–3 | 0=stretch 1=fit 2=fill 3=1:1 |
|
||||
| `0x00D00002` | Anchor | 0–1 | 0=center 1=topleft |
|
||||
| `0x00D00003` | No-signal FPS | 1–60 | No-signal animation frame rate |
|
||||
|
||||
---
|
||||
|
||||
### Control get/set
|
||||
|
||||
```
|
||||
get-control <device_index> <control_id_hex>
|
||||
set-control <device_index> <control_id_hex> <value>
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
get-control 0 0x00980900 # read Brightness on device 0
|
||||
set-control 0 0x00980900 200 # set Brightness to 200
|
||||
set-control 3 0x00D00001 1 # set Scale to 'fit' on display device 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Ingest (source node)
|
||||
|
||||
```
|
||||
start-ingest <stream_id> <device_path> <dest_host> <dest_port>
|
||||
[format] [width] [height] [fps_n] [fps_d]
|
||||
```
|
||||
|
||||
Tell the connected node to open a V4L2 device and stream to `dest_host:dest_port`. Format, resolution, and frame rate default to auto-select (best MJPEG) when omitted.
|
||||
|
||||
```
|
||||
stop-ingest <stream_id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Display (sink node)
|
||||
|
||||
```
|
||||
start-display <stream_id> [win_x] [win_y] [win_w] [win_h] [no_signal_fps]
|
||||
```
|
||||
|
||||
Tell the connected node to open a display window for `stream_id`. Position and size default to `0,0 1280×720`. No-signal FPS defaults to 15.
|
||||
|
||||
```
|
||||
stop-display <stream_id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Other
|
||||
|
||||
```
|
||||
help show command list
|
||||
quit exit
|
||||
exit exit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example session
|
||||
|
||||
```
|
||||
$ ./controller_cli
|
||||
listening for nodes...
|
||||
[discovered 0] camera:0 192.168.1.42:8000
|
||||
> connect
|
||||
connected to 192.168.1.42:8000
|
||||
> enum-devices
|
||||
devices:
|
||||
media /dev/media0 driver=unicam model=unicam ... (1 video node)
|
||||
[0] video /dev/video0 entity=unicam-image ... [capture]
|
||||
> start-ingest 1 /dev/video0 192.168.1.55 8001
|
||||
ok
|
||||
> connect 1
|
||||
[discovered 1] display:0 192.168.1.55:8000
|
||||
connected to 192.168.1.55:8000
|
||||
> start-display 1
|
||||
ok
|
||||
> enum-devices
|
||||
devices:
|
||||
[3] display stream=1 pos=0,0 size=1280x720 scale=stretch anchor=center
|
||||
> set-control 3 0x00D00001 1
|
||||
ok
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`controller_cli` is the primary human interface for the video routing system during development. It exercises the full control channel: discovery, TCP transport, and every protocol command type. It is the reference implementation for how a controller interacts with nodes.
|
||||
|
||||
See also: [`query_cli.md`](query_cli.md) for a simpler read-only query; [`stream_send_cli.md`](stream_send_cli.md) / [`stream_recv_cli.md`](stream_recv_cli.md) for testing streams without a full node.
|
||||
53
docs/cli/discovery_cli.md
Normal file
53
docs/cli/discovery_cli.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# discovery_cli
|
||||
|
||||
A development tool for testing the discovery layer. Announces a node on the local network via UDP multicast and prints peers as they appear and disappear.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
discovery_cli <name> <tcp_port> [flags]
|
||||
```
|
||||
|
||||
| Argument | Description |
|
||||
|---|---|
|
||||
| `name` | Node name in `namespace:instance` form, e.g. `v4l2:microscope` |
|
||||
| `tcp_port` | The TCP port this node listens on for transport connections |
|
||||
| `flags` | Comma-separated role list: `source`, `relay`, `sink`, `controller` (default: `source`) |
|
||||
|
||||
Example — announce a source node and watch for peers:
|
||||
|
||||
```sh
|
||||
./discovery_cli camera:0 8000 source
|
||||
```
|
||||
|
||||
Example output as peers are found and lost:
|
||||
|
||||
```
|
||||
found: unnamed:0 192.168.1.42:8001 site=0 flags=source
|
||||
found: display:0 192.168.1.55:8002 site=0 flags=sink
|
||||
lost: unnamed:0 192.168.1.42:8001
|
||||
```
|
||||
|
||||
Run two instances on the same machine (different ports) to verify mutual discovery.
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`discovery_cli` exercises the `discovery` module, which the video node uses to find peers on the LAN without configuration. The node starts a discovery listener on startup; controllers use discovered peer tables to locate nodes before connecting.
|
||||
|
||||
See also: [`query_cli.md`](query_cli.md) for a combined discovery + protocol query.
|
||||
64
docs/cli/protocol_cli.md
Normal file
64
docs/cli/protocol_cli.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# protocol_cli
|
||||
|
||||
A development tool for testing the protocol layer in isolation. Runs a server that decodes and prints all incoming control messages, or a client that connects and sends a sample STREAM_OPEN request.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Server mode
|
||||
|
||||
```sh
|
||||
./protocol_cli --server [port]
|
||||
```
|
||||
|
||||
Default port: `8000`. Listens for connections and prints a decoded description of every received frame.
|
||||
|
||||
Example output on an incoming STREAM_OPEN:
|
||||
|
||||
```
|
||||
CONTROL_REQUEST request_id=1 STREAM_OPEN stream_id=1 format=0x0001 pixel_format=0x0000 origin=0x0001
|
||||
```
|
||||
|
||||
Recognised message types and commands:
|
||||
|
||||
| Type | Description |
|
||||
|---|---|
|
||||
| `VIDEO_FRAME` | stream_id + compressed payload |
|
||||
| `STREAM_EVENT` | stream_id + event code (INTERRUPTED / RESUMED) |
|
||||
| `CONTROL_REQUEST` | request_id + command (STREAM_OPEN, STREAM_CLOSE, ENUM_DEVICES, ENUM_CONTROLS, GET_CONTROL, SET_CONTROL, ENUM_MONITORS) |
|
||||
| `CONTROL_RESPONSE` | request_id + status (OK, ERROR, UNKNOWN_CMD, INVALID_PARAMS, NOT_FOUND) |
|
||||
|
||||
### Client mode
|
||||
|
||||
```sh
|
||||
./protocol_cli --client <host> <port>
|
||||
```
|
||||
|
||||
Connects to the server and sends a single STREAM_OPEN request, then waits for the response.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
./protocol_cli --client 127.0.0.1 8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`protocol_cli` exercises the `protocol` module that all nodes use to encode and decode wire messages. Pairing the server with a running node lets you inspect raw control traffic; pairing the client with a running node lets you inject individual commands for debugging.
|
||||
|
||||
See also: [`transport_cli.md`](transport_cli.md) for raw frame-level testing without protocol parsing.
|
||||
64
docs/cli/query_cli.md
Normal file
64
docs/cli/query_cli.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# query_cli
|
||||
|
||||
An integration smoke test that combines discovery and protocol. Waits for a video node to appear on the local network, then sends an ENUM_DEVICES query and prints the results. Optionally enumerates controls on a specific device.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./query_cli [--timeout ms] [--controls device_index]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--timeout ms` | `5000` | How long to wait for a node to appear (milliseconds) |
|
||||
| `--controls idx` | — | After enumeration, also query controls for this device index |
|
||||
|
||||
Example — query the first node found within 5 s:
|
||||
|
||||
```sh
|
||||
./query_cli
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
discovered: camera:0 192.168.1.42:8000
|
||||
querying devices...
|
||||
media /dev/media0 driver=unicam model=unicam bus=platform:fe801000.csi (1 video node)
|
||||
[0] video /dev/video0 entity=unicam-image type=0x00010001 caps=[video-capture] [capture]
|
||||
[1] standalone /dev/video1 card=USB Camera
|
||||
```
|
||||
|
||||
Example with controls:
|
||||
|
||||
```sh
|
||||
./query_cli --controls 1
|
||||
```
|
||||
|
||||
```
|
||||
ctrl id=0x00980900 Brightness int min=0 max=255 step=1 default=128 current=127
|
||||
ctrl id=0x00980901 Contrast int min=0 max=255 step=1 default=128 current=128
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`query_cli` is an end-to-end integration test: it exercises discovery, transport, and protocol in one shot. It is the simplest way to verify that a node is reachable and responding correctly to control requests before using `controller_cli` for full interaction.
|
||||
|
||||
See also: [`discovery_cli.md`](discovery_cli.md), [`controller_cli.md`](controller_cli.md).
|
||||
83
docs/cli/reconciler_cli.md
Normal file
83
docs/cli/reconciler_cli.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# reconciler_cli
|
||||
|
||||
An interactive REPL for exploring the reconciler module. Sets up a simulated three-resource state machine (device, transport, stream) with declared dependencies and lets you drive reconciliation manually — useful for understanding reconciler behaviour before wiring it into the node.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./reconciler_cli
|
||||
```
|
||||
|
||||
Drops into an interactive prompt:
|
||||
|
||||
```
|
||||
reconciler> _
|
||||
```
|
||||
|
||||
### Demo resources
|
||||
|
||||
Three resources are pre-configured:
|
||||
|
||||
| Resource | States | Notes |
|
||||
|---|---|---|
|
||||
| `device` | CLOSED → OPEN → STREAMING | Simulates a V4L2 device |
|
||||
| `transport` | DISCONNECTED → CONNECTED | Depends on device=OPEN |
|
||||
| `stream` | INACTIVE → ACTIVE | Depends on transport=CONNECTED and device=STREAMING |
|
||||
|
||||
Dependencies are checked before each transition: the reconciler will not advance a resource until all its prerequisites are met. Blocked resources show the unmet dependency in the status output.
|
||||
|
||||
### Commands
|
||||
|
||||
```
|
||||
status print all resources with current/wanted state and transition status
|
||||
want <name> <state> set the desired state (by number or case-insensitive name)
|
||||
tick run one reconciler tick
|
||||
run tick until stable (max 20 ticks) or a failure occurs
|
||||
fail <name> make the next action for this resource fail (simulates an error)
|
||||
help show commands
|
||||
quit / exit exit
|
||||
```
|
||||
|
||||
### Example session
|
||||
|
||||
```
|
||||
reconciler> want device STREAMING
|
||||
reconciler> want transport CONNECTED
|
||||
reconciler> want stream ACTIVE
|
||||
reconciler> run
|
||||
[device] CLOSED → OPEN ok
|
||||
[device] OPEN → STREAMING ok
|
||||
[transport] DISCONNECTED → CONNECTED ok
|
||||
[stream] INACTIVE → ACTIVE ok
|
||||
reconciler> status
|
||||
device current=STREAMING wanted=STREAMING
|
||||
transport current=CONNECTED wanted=CONNECTED
|
||||
stream current=ACTIVE wanted=ACTIVE
|
||||
reconciler> fail transport
|
||||
reconciler> tick
|
||||
[transport] CONNECTED → DISCONNECTED error (simulated)
|
||||
reconciler> status
|
||||
device current=STREAMING wanted=STREAMING
|
||||
transport current=DISCONNECTED wanted=CONNECTED (will retry)
|
||||
stream current=ACTIVE wanted=ACTIVE (blocked: transport != CONNECTED)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
The reconciler module manages device open/close and transport connect/disconnect in the video node. `reconciler_cli` lets you exercise the BFS pathfinding, dependency checking, and event-driven tick logic without any real hardware — the same code paths that run in the node on every incoming command or timer event.
|
||||
67
docs/cli/stream_recv_cli.md
Normal file
67
docs/cli/stream_recv_cli.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# stream_recv_cli
|
||||
|
||||
Listens for an incoming TCP stream of `VIDEO_FRAME` protocol messages and displays the frames in an X11 window with a per-stream fps/Mbps overlay. Pair with [`stream_send_cli`](stream_send_cli.md) to test end-to-end video transport without a full node.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
Requires GLFW, OpenGL, and libjpeg-turbo.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./stream_recv_cli [--port PORT] [--stream-id N]
|
||||
[--scale stretch|fit|fill|1:1]
|
||||
[--anchor center|topleft]
|
||||
[--x N] [--y N]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--port PORT` | `7700` | TCP port to listen on |
|
||||
| `--stream-id N` | `0` | Filter by stream ID; `0` accepts any stream |
|
||||
| `--scale` | `fit` | Frame scaling in window |
|
||||
| `--anchor` | `center` | Frame alignment in window |
|
||||
| `--x N` | `0` | Window X position |
|
||||
| `--y N` | `0` | Window Y position |
|
||||
|
||||
Press **Q** or **Escape** to close the window.
|
||||
|
||||
### Example
|
||||
|
||||
```sh
|
||||
./stream_recv_cli --port 7700 --scale fit
|
||||
```
|
||||
|
||||
### Statistics overlay
|
||||
|
||||
Updated every 0.5 seconds:
|
||||
|
||||
```
|
||||
stream 1: 30.1 fps 18.3 Mbps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture note
|
||||
|
||||
The tool is multi-threaded: transport receive runs on a background thread and deposits frames into a shared slot (mutex + condition variable); the main thread owns the GLFW/OpenGL context and pulls from that slot on each render cycle. This is the same pattern used by the node's display sink.
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`stream_recv_cli` tests the sink end of the display pipeline: `VIDEO_FRAME` receive → MJPEG decode → xorg render. The node's display sink (started via `START_DISPLAY`) performs the same operation, driven by the control channel.
|
||||
|
||||
See also: [`stream_send_cli.md`](stream_send_cli.md) for the send side; [`controller_cli.md`](controller_cli.md) to use the full node pipeline.
|
||||
64
docs/cli/stream_send_cli.md
Normal file
64
docs/cli/stream_send_cli.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# stream_send_cli
|
||||
|
||||
Captures MJPEG from a V4L2 device and streams it to a receiver over TCP as `VIDEO_FRAME` protocol messages. Prints per-stream throughput statistics. Pair with [`stream_recv_cli`](stream_recv_cli.md) to test an end-to-end stream without running a full node.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./stream_send_cli [--device PATH] [--host HOST] [--port PORT] [--stream-id N]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--device PATH` | `/dev/video0` | V4L2 capture device |
|
||||
| `--host HOST` | `127.0.0.1` | Receiver hostname or IP |
|
||||
| `--port PORT` | `7700` | Receiver TCP port |
|
||||
| `--stream-id N` | `1` | Stream ID embedded in each `VIDEO_FRAME` message |
|
||||
|
||||
### Example
|
||||
|
||||
```sh
|
||||
# Terminal 1 — start a receiver
|
||||
./stream_recv_cli --port 7700
|
||||
|
||||
# Terminal 2 — start the sender
|
||||
./stream_send_cli --device /dev/video0 --host 127.0.0.1 --port 7700 --stream-id 1
|
||||
```
|
||||
|
||||
### Statistics output
|
||||
|
||||
Printed to stderr every 0.5 seconds:
|
||||
|
||||
```
|
||||
stream 1: 30.2 fps 18.4 Mbps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The tool opens the device and selects the highest-FPS MJPEG mode automatically.
|
||||
- Frames are sent as-is (raw MJPEG from the kernel) wrapped in `VIDEO_FRAME` messages — no re-encoding.
|
||||
- The connection is outbound: `stream_send_cli` connects to the receiver, not the other way around. This mirrors the node's START_INGEST behaviour.
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`stream_send_cli` tests the source end of the ingest pipeline: V4L2 capture → transport send → `VIDEO_FRAME` messages. The node's ingest module performs the same operation, but driven by `START_INGEST` commands over the control channel.
|
||||
|
||||
See also: [`stream_recv_cli.md`](stream_recv_cli.md) for the receive side; [`v4l2_view_cli.md`](v4l2_view_cli.md) for local-only viewing.
|
||||
70
docs/cli/test_image_cli.md
Normal file
70
docs/cli/test_image_cli.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# test_image_cli
|
||||
|
||||
A development tool for generating test images and writing them to PPM files. Used to verify the `test_image` module's pattern generators and pixel format output without needing a camera or display.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./test_image_cli [--pattern bars|ramp|grid]
|
||||
[--width N] [--height N]
|
||||
[--format yuv420|yuv422|bgra]
|
||||
--out FILE.ppm
|
||||
```
|
||||
|
||||
All options are optional except `--out`.
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--pattern` | `bars` | Test pattern to generate |
|
||||
| `--width N` | `1280` | Image width in pixels |
|
||||
| `--height N` | `720` | Image height in pixels |
|
||||
| `--format` | `yuv420` | Internal pixel format before PPM conversion |
|
||||
| `--out FILE` | required | Output file path |
|
||||
|
||||
### Patterns
|
||||
|
||||
| Name | Description |
|
||||
|---|---|
|
||||
| `bars` | Colour bars (SMPTE-style) |
|
||||
| `ramp` | Luminance ramp from black to white |
|
||||
| `grid` | Crosshatch grid |
|
||||
|
||||
### Formats
|
||||
|
||||
| Name | Description |
|
||||
|---|---|
|
||||
| `yuv420` | Planar YUV 4:2:0 |
|
||||
| `yuv422` | Packed YUV 4:2:2 |
|
||||
| `bgra` | Packed BGRA 8-bit |
|
||||
|
||||
The output is always written as a PPM (RGB) file regardless of format; the internal format affects how the pattern is generated and converted.
|
||||
|
||||
### Example
|
||||
|
||||
```sh
|
||||
./test_image_cli --pattern bars --width 1920 --height 1080 --format yuv420 --out bars.ppm
|
||||
```
|
||||
|
||||
Open with any image viewer that supports PPM (e.g. `feh`, `eog`, GIMP).
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`test_image_cli` exercises the `test_image` module used by development tools to inject synthetic frames without a camera. The same module drives the xorg test pattern display in `xorg_cli`.
|
||||
|
||||
See also: [`xorg_cli.md`](xorg_cli.md) for live window rendering.
|
||||
69
docs/cli/transport_cli.md
Normal file
69
docs/cli/transport_cli.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# transport_cli
|
||||
|
||||
A development tool for exercising the transport layer. Starts a framed-TCP server (which echoes received frames) or a client that connects and sends a sequence of test frames.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Server mode
|
||||
|
||||
Listen on a port and echo back every frame received:
|
||||
|
||||
```sh
|
||||
./transport_cli server <port> [max_connections]
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
./transport_cli server 8000
|
||||
```
|
||||
|
||||
Output (per received frame):
|
||||
|
||||
```
|
||||
received frame: type=0x0001 len=8
|
||||
```
|
||||
|
||||
### Client mode
|
||||
|
||||
Connect to a server and send three test frames:
|
||||
|
||||
```sh
|
||||
./transport_cli client <host> <port>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
./transport_cli client 127.0.0.1 8000
|
||||
```
|
||||
|
||||
Each frame carries a fixed payload with a counter:
|
||||
|
||||
```
|
||||
frame 0: payload = deadbeef 00000000
|
||||
frame 1: payload = deadbeef 00000001
|
||||
frame 2: payload = deadbeef 00000002
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`transport_cli` exercises the `transport` module, which provides the framed TCP stream used by all node-to-node communication. The frame header (message type + payload length) and single-write send are the building blocks for the protocol layer.
|
||||
|
||||
See also: [`protocol_cli.md`](protocol_cli.md) for typed message-level testing.
|
||||
69
docs/cli/v4l2_view_cli.md
Normal file
69
docs/cli/v4l2_view_cli.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# v4l2_view_cli
|
||||
|
||||
A live camera viewer. Opens a V4L2 capture device, selects the best available format, and displays the video stream in an X11 window with a real-time FPS and format overlay. Bypasses the node system entirely — useful for verifying a camera works before wiring it into a node.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
Requires GLFW, OpenGL, libjpeg-turbo, and a V4L2-capable kernel.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./v4l2_view_cli [--device PATH]
|
||||
[--width N --height N]
|
||||
[--format mjpeg|yuyv]
|
||||
[--scale stretch|fit|fill|1:1]
|
||||
[--anchor center|topleft]
|
||||
[--x N] [--y N]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--device PATH` | `/dev/video0` | V4L2 device to open |
|
||||
| `--width N` | auto | Capture width; if omitted, selects highest-FPS mode |
|
||||
| `--height N` | auto | Capture height |
|
||||
| `--format` | auto | Prefer `mjpeg` or `yuyv`; auto selects best available |
|
||||
| `--scale` | `fit` | Frame scaling in window |
|
||||
| `--anchor` | `center` | Frame alignment in window |
|
||||
| `--x N` | `0` | Window X position |
|
||||
| `--y N` | `0` | Window Y position |
|
||||
|
||||
Press **Q** or **Escape** to close the window.
|
||||
|
||||
### Auto format selection
|
||||
|
||||
Without `--width`/`--height`, the tool selects the format with the highest frame rate, and within that the largest resolution. This is the same logic the node's ingest module uses.
|
||||
|
||||
### Overlay
|
||||
|
||||
Every 0.5 seconds the overlay updates with:
|
||||
|
||||
```
|
||||
MJPEG 1280x720 @ 30.0 fps
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```sh
|
||||
./v4l2_view_cli --device /dev/video0 --scale fit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`v4l2_view_cli` is a standalone sanity-check tool. It exercises the same V4L2 format enumeration, mmap capture, MJPEG decode, and xorg rendering path that the node's ingest + display pipeline uses — but without any transport or protocol overhead.
|
||||
|
||||
See also: [`stream_send_cli.md`](stream_send_cli.md) to capture and send over the network; [`xorg_cli.md`](xorg_cli.md) for static test patterns.
|
||||
73
docs/cli/xorg_cli.md
Normal file
73
docs/cli/xorg_cli.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# xorg_cli
|
||||
|
||||
A development tool for testing the xorg viewer sink. Opens an X11 window using GLFW/OpenGL, renders a test pattern at the chosen scale and anchor, and displays a text overlay showing the current date. The window stays open until the user presses Q, Escape, or closes it.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make cli
|
||||
```
|
||||
|
||||
The binary is placed in `build/cli/`.
|
||||
|
||||
Requires the GLFW, OpenGL, and libjpeg-turbo libraries. If compiled without `HAVE_GLFW`, the binary will print an error and exit.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./xorg_cli [--pattern bars|ramp|grid]
|
||||
[--width N] [--height N]
|
||||
[--format yuv420|bgra]
|
||||
[--scale stretch|fit|fill|1:1]
|
||||
[--anchor center|topleft]
|
||||
[--x N] [--y N]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--pattern` | `bars` | Test pattern to render |
|
||||
| `--width N` | `1280` | Window width in pixels |
|
||||
| `--height N` | `720` | Window height in pixels |
|
||||
| `--format` | `yuv420` | Frame pixel format |
|
||||
| `--scale` | `stretch` | How the frame fills the window |
|
||||
| `--anchor` | `center` | Frame alignment within the window |
|
||||
| `--x N` | `0` | Window X position on screen |
|
||||
| `--y N` | `0` | Window Y position on screen |
|
||||
|
||||
### Scale modes
|
||||
|
||||
| Mode | Description |
|
||||
|---|---|
|
||||
| `stretch` | Fill the window, ignoring aspect ratio |
|
||||
| `fit` | Largest rect that fits, preserving aspect ratio (black bars) |
|
||||
| `fill` | Smallest rect that covers, preserving aspect ratio (crops edges) |
|
||||
| `1:1` | Native pixel size, no scaling |
|
||||
|
||||
### Anchor modes
|
||||
|
||||
Anchor applies when the frame does not fill the window (fit, fill, 1:1 modes):
|
||||
|
||||
| Mode | Description |
|
||||
|---|---|
|
||||
| `center` | Centre the frame in the window |
|
||||
| `topleft` | Align frame to the top-left corner |
|
||||
|
||||
### Example
|
||||
|
||||
```sh
|
||||
./xorg_cli --pattern grid --scale fit --anchor center --width 1920 --height 1080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to the Video Routing System
|
||||
|
||||
`xorg_cli` exercises the `xorg` module used by the video node for its display sink role. The same viewer, scale/anchor logic, and text overlay system are used by `stream_recv_cli` and the node's `START_DISPLAY` command.
|
||||
|
||||
See also: [`v4l2_view_cli.md`](v4l2_view_cli.md) for live camera feed display; [`stream_recv_cli.md`](stream_recv_cli.md) for network stream display.
|
||||
@@ -43,11 +43,81 @@ A node may set multiple bits — a relay that also archives sets both `RELAY` an
|
||||
|
||||
### Behaviour
|
||||
|
||||
- Nodes send announcements periodically (e.g. every 5 s) and immediately on startup
|
||||
- Nodes send announcements periodically (default every 5 s) and immediately on startup via multicast
|
||||
- No daemon — the node process itself sends and listens; no background service required
|
||||
- On receiving an announcement, the control plane records the peer (address, port, name, function) and can initiate a transport connection if needed
|
||||
- A node going silent for a configured number of announcement intervals is considered offline
|
||||
- Announcements are informational only — the hub validates identity at connection time
|
||||
- On receiving an announcement the node records the peer (address, port, name, capabilities) and can initiate a transport connection if needed
|
||||
- A peer that goes silent for `timeout_intervals × interval_ms` is considered offline and removed from the peer table
|
||||
- Announcements are informational only — identity is validated at TCP connection time
|
||||
|
||||
#### Startup — new node joins the network
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant N as New Node
|
||||
participant MC as Multicast group
|
||||
participant A as Node A
|
||||
participant B as Node B
|
||||
|
||||
N->>MC: announce (multicast)
|
||||
MC-->>A: receives announce
|
||||
MC-->>B: receives announce
|
||||
A->>N: announce (unicast reply)
|
||||
B->>N: announce (unicast reply)
|
||||
Note over N,B: All parties now know each other.<br/>Subsequent keepalives are multicast only.
|
||||
```
|
||||
|
||||
Each node that hears a new peer sends a **unicast reply** directly to that peer. This allows the new node to populate its peer table within one round-trip rather than waiting up to `interval_ms` for other nodes' next scheduled broadcast.
|
||||
|
||||
#### Steady-state keepalive
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Node A
|
||||
participant MC as Multicast group
|
||||
participant B as Node B
|
||||
participant C as Node C
|
||||
|
||||
loop every interval_ms
|
||||
A->>MC: announce (multicast)
|
||||
MC-->>B: receives — updates last_seen_ms, no reply
|
||||
MC-->>C: receives — updates last_seen_ms, no reply
|
||||
end
|
||||
```
|
||||
|
||||
Known peers update their `last_seen_ms` timestamp and do nothing else. No reply is sent, so there is no amplification.
|
||||
|
||||
#### Node loss — timeout
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Node A
|
||||
participant B as Node B (offline)
|
||||
|
||||
Note over B: Node B stops sending
|
||||
loop timeout_intervals × interval_ms elapses
|
||||
A->>A: check_timeouts() — not yet expired
|
||||
end
|
||||
A->>A: check_timeouts() — expired, remove B
|
||||
A->>A: on_peer_lost(B) callback
|
||||
```
|
||||
|
||||
#### Node restart — known limitation
|
||||
|
||||
The current implementation attempts to detect a restart by checking whether `site_id` changed for a known `(addr, port)` entry. In practice this **does not work**: `site_id` is a static configuration value and will be the same before and after a restart. A restarted node will therefore simply be treated as a continuing keepalive and will not receive an immediate unicast reply — it will have to wait up to `interval_ms` for the next scheduled multicast broadcast from its peers.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R as Restarted Node
|
||||
participant MC as Multicast group
|
||||
participant A as Node A
|
||||
|
||||
Note over R: Node restarts — same addr, port, site_id
|
||||
R->>MC: announce (multicast)
|
||||
MC-->>A: receives — site_id unchanged, treated as keepalive
|
||||
Note over A: No unicast reply sent. R waits up to interval_ms<br/>to learn about A via A's next scheduled multicast.
|
||||
```
|
||||
|
||||
**What needs to change:** a **boot nonce** (random `u32` generated at startup, not configured) should be added to the announcement payload. A change in boot nonce for a known peer unambiguously signals a restart and triggers an immediate unicast reply. This requires a wire format version bump and updates to the peer table struct, announcement builder, and receive logic.
|
||||
|
||||
### No Avahi/Bonjour Dependency
|
||||
|
||||
|
||||
@@ -491,6 +491,8 @@ packet-beta
|
||||
96-111: "win_h"
|
||||
112-119: "scale"
|
||||
120-127: "anchor"
|
||||
128-135: "no_signal_fps"
|
||||
136-143: "reserved"
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
@@ -500,6 +502,7 @@ packet-beta
|
||||
| `win_w`, `win_h` | Window size in pixels; `0` = default (1280×720) |
|
||||
| `scale` | `0`=stretch `1`=fit `2`=fill `3`=1:1 |
|
||||
| `anchor` | `0`=center `1`=topleft |
|
||||
| `no_signal_fps` | Frame rate of no-signal animation (0 = default 15 fps) |
|
||||
|
||||
**Response** — no extra fields beyond request_id and status. `OK` means the display slot was reserved; the window opens asynchronously on the main thread.
|
||||
|
||||
|
||||
@@ -147,6 +147,29 @@ struct Proto_Standalone_Device_Info {
|
||||
const char *name;
|
||||
};
|
||||
|
||||
/*
|
||||
* 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;
|
||||
uint8_t scale_mode;
|
||||
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_MODE 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;
|
||||
@@ -239,7 +262,7 @@ struct Proto_Stop_Ingest {
|
||||
* display incoming VIDEO_FRAME messages for the given stream_id.
|
||||
* win_x/win_y are screen-space window position (signed: multi-monitor).
|
||||
* win_w/win_h of 0 mean use a default size.
|
||||
* scale: 0=stretch 1=fit 2=fill 3=1:1 (PROTO_DISPLAY_SCALE_*)
|
||||
* scale_mode: 0=stretch 1=fit 2=fill 3=1:1 (PROTO_DISPLAY_SCALE_*)
|
||||
* anchor: 0=center 1=topleft (PROTO_DISPLAY_ANCHOR_*)
|
||||
*/
|
||||
struct Proto_Start_Display {
|
||||
@@ -249,8 +272,10 @@ struct Proto_Start_Display {
|
||||
int16_t win_y;
|
||||
uint16_t win_w;
|
||||
uint16_t win_h;
|
||||
uint8_t scale;
|
||||
uint8_t scale_mode;
|
||||
uint8_t anchor;
|
||||
uint8_t no_signal_fps; /* 0 = default (15); no-signal animation frame rate */
|
||||
/* 1 byte reserved */
|
||||
};
|
||||
|
||||
struct Proto_Stop_Display {
|
||||
@@ -340,7 +365,7 @@ struct App_Error proto_write_stop_ingest(struct Transport_Conn *conn,
|
||||
struct App_Error proto_write_start_display(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t stream_id,
|
||||
int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h,
|
||||
uint8_t scale, uint8_t anchor);
|
||||
uint8_t scale_mode, uint8_t anchor, uint8_t no_signal_fps);
|
||||
|
||||
/* CONTROL_REQUEST: STOP_DISPLAY */
|
||||
struct App_Error proto_write_stop_display(struct Transport_Conn *conn,
|
||||
@@ -360,7 +385,8 @@ struct App_Error proto_write_control_response(struct Transport_Conn *conn,
|
||||
struct App_Error proto_write_enum_devices_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status,
|
||||
const struct Proto_Media_Device_Info *media_devices, uint16_t media_count,
|
||||
const struct Proto_Standalone_Device_Info *standalone, uint16_t standalone_count);
|
||||
const struct Proto_Standalone_Device_Info *standalone, uint16_t standalone_count,
|
||||
const struct Proto_Display_Device_Info *displays, uint16_t display_count);
|
||||
|
||||
/* CONTROL_RESPONSE: ENUM_CONTROLS */
|
||||
struct App_Error proto_write_enum_controls_response(struct Transport_Conn *conn,
|
||||
@@ -479,6 +505,13 @@ struct App_Error proto_read_enum_devices_response(
|
||||
const char *path, uint8_t path_len,
|
||||
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,
|
||||
uint8_t scale_mode, uint8_t anchor,
|
||||
void *userdata),
|
||||
void *userdata);
|
||||
|
||||
/*
|
||||
|
||||
@@ -67,6 +67,14 @@ void xorg_viewer_set_overlay_text(Xorg_Viewer *v, int idx, int x, int y,
|
||||
/* Remove all text overlays. */
|
||||
void xorg_viewer_clear_overlays(Xorg_Viewer *v);
|
||||
|
||||
/*
|
||||
* Render one frame of animated analog-TV noise with a centred "NO SIGNAL"
|
||||
* label. time is seconds (e.g. glfwGetTime()); noise_res is cells per axis
|
||||
* (lower = coarser, default 80 when 0 is passed).
|
||||
* Call at low frame rate (~15 fps) when the viewer has no incoming stream.
|
||||
*/
|
||||
void xorg_viewer_render_no_signal(Xorg_Viewer *v, float time, float noise_res);
|
||||
|
||||
/*
|
||||
* Process pending window events.
|
||||
* Returns false when the user has closed the window.
|
||||
|
||||
37
planning.md
37
planning.md
@@ -15,14 +15,18 @@ video-setup/
|
||||
src/
|
||||
modules/
|
||||
common/ - shared definitions (error types, base types)
|
||||
config/ - INI file loader with schema-driven defaults, typed getters
|
||||
media_ctrl/ - Linux Media Controller API (topology, pad formats, links)
|
||||
v4l2_ctrl/ - V4L2 camera controls (enumerate, get, set)
|
||||
serial/ - little-endian binary serialization primitives
|
||||
transport/ - framed TCP stream, single-write send
|
||||
discovery/ - UDP multicast announcements, peer table, found/lost callbacks
|
||||
protocol/ - typed write_*/read_* message functions
|
||||
test_image/ - test pattern generator (colour bars, ramp, grid; YUV420/BGRA)
|
||||
xorg/ - GLFW+OpenGL viewer sink; stub for headless builds
|
||||
node/ - video node entry point and top-level integration (later)
|
||||
reconciler/ - generic wanted/current state machine reconciler
|
||||
ingest/ - V4L2 capture loop, MMAP buffers, on_frame callback
|
||||
node/ - video node binary (source + display sink roles)
|
||||
include/ - public headers
|
||||
dev/
|
||||
cli/ - exploratory CLI drivers, one per module
|
||||
@@ -55,13 +59,13 @@ Modules are listed in intended build order. Each depends only on modules above i
|
||||
| 5 | `transport` | done | Encapsulated transport — frame header, TCP stream abstraction, single-write send |
|
||||
| 6 | `discovery` | done | UDP multicast announcements, peer table, found/lost callbacks |
|
||||
| 7 | `protocol` | done | Typed `write_*`/`read_*` functions for all message types; builds on serial + transport |
|
||||
| — | `node` | done | Video node binary — config, discovery, transport server, V4L2/media control request handlers; display sink role (START_DISPLAY/STOP_DISPLAY handlers, multi-window xorg viewer, declarative display slot reconciler) |
|
||||
| 8 | `test_image` | done | Test pattern generator — colour bars, luminance ramp, grid crosshatch; YUV420/BGRA output |
|
||||
| 9 | `xorg` | done | GLFW+OpenGL viewer sink — YUV420/BGRA/MJPEG display, all scale/anchor modes, bitmap font atlas text overlays; XRandR queries and screen grab not yet implemented; viewer controls (zoom, pan, scale policy) not yet exposed remotely |
|
||||
| 10 | `reconciler` | done | Generic wanted/current state machine reconciler — resource state graphs, BFS pathfinding, event + periodic tick; used by node to manage V4L2 devices, transport connections, and future resources (codec processes etc.) |
|
||||
| 11 | `frame_alloc` | not started | Per-frame allocation with bookkeeping (byte budget, ref counting) |
|
||||
| 12 | `relay` | not started | Input dispatch to output queues (low-latency and completeness modes) |
|
||||
| 13 | `ingest` | done | V4L2 capture loop — open device, negotiate MJPEG format, MMAP buffers, capture thread with on_frame callback; start/stop lifecycle managed by reconciler |
|
||||
| 11 | `ingest` | done | V4L2 capture loop — open device, negotiate MJPEG format, MMAP buffers, capture thread with on_frame callback; start/stop lifecycle managed by reconciler |
|
||||
| — | `node` | done | Video node binary — config, discovery, transport server, V4L2/media control request handlers; display sink role (START_DISPLAY/STOP_DISPLAY handlers, multi-window xorg viewer, declarative display slot reconciler) |
|
||||
| 12 | `frame_alloc` | not started | Per-frame allocation with bookkeeping (byte budget, ref counting) |
|
||||
| 13 | `relay` | not started | Input dispatch to output queues (low-latency and completeness modes) |
|
||||
| 14 | `archive` | not started | Write frames to disk, control messages to binary log |
|
||||
| 15 | `codec` | not started | Per-frame encode/decode — MJPEG (libjpeg-turbo), QOI, ZSTD-raw, VA-API H.264 intra; used by screen grab source and archive |
|
||||
| 16 | `web node` | not started | Node.js/Express peer — speaks binary protocol on socket side, HTTP/WebSocket to browser; `protocol.mjs` mirrors C protocol module |
|
||||
@@ -80,13 +84,26 @@ Each module gets a corresponding CLI driver that exercises its API and serves as
|
||||
| `media_ctrl_cli` | `media_ctrl` | List media devices, show topology, configure pad formats |
|
||||
| `v4l2_ctrl_cli` | `v4l2_ctrl` | List controls, get/set values — lightweight `v4l2-ctl` equivalent |
|
||||
| `transport_cli` | `transport` | Send/receive framed messages, inspect headers |
|
||||
| `discovery_cli` | `discovery` | Announce and discover peers over UDP multicast; print found/lost events |
|
||||
| `config_cli` | `config` | Load an INI config file and print the resolved values after applying schema defaults |
|
||||
| `protocol_cli` | `protocol` | Send and receive typed protocol messages; inspect frame payloads |
|
||||
| `query_cli` | `discovery` + `protocol` | Wait for first discovered node, send ENUM_DEVICES, print results — integration smoke test |
|
||||
| `test_image_cli` | `test_image` | Generate test patterns, write PPM for visual inspection |
|
||||
| `xorg_cli` | `xorg` | Display test pattern in viewer window; exercises scale/anchor modes and text overlays |
|
||||
| `v4l2_view_cli` | V4L2 + `xorg` | Live camera viewer — auto-selects highest-FPS format, FPS/format overlay; bypasses node system |
|
||||
| `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, start-display, stop-display |
|
||||
| `controller_cli` | `transport` + `protocol` + `discovery` | Interactive controller REPL — connects to nodes by peer index or host:port; supports enum-devices, enum-controls, get/set-control, start-ingest, stop-ingest, start-display, stop-display; readline + discovery integration; **temporary dev tool** — will be superseded by a dedicated `controller` binary that holds simultaneous connections to all peers |
|
||||
|
||||
### Header-only utilities (`include/`)
|
||||
|
||||
Not modules (no `.c` or Makefile) but public interfaces used across CLI tools and the node:
|
||||
|
||||
| Header | Used by | Notes |
|
||||
|---|---|---|
|
||||
| `stream_stats.h` | `stream_send_cli`, `stream_recv_cli` | Per-stream rolling fps/Mbps stats; single-header, no dependencies |
|
||||
| `v4l2_fmt.h` | `ingest`, `v4l2_view_cli` | V4L2 format enumeration — (pixfmt, size, fps) combinations, best-format selection |
|
||||
|
||||
### Web UI (`dev/web/`)
|
||||
|
||||
@@ -114,4 +131,10 @@ These are open questions tracked in `architecture.md` that do not need to be res
|
||||
- Node discovery mechanism
|
||||
- Hard vs soft byte budget limits
|
||||
- Cooperative capture release: if a capture source has no live downstream targets for a configurable time window, stop capture and release the device. Intended as a resource-conservation policy rather than an immediate reaction to disconnect events. Requires the node to track downstream liveness (e.g. last successful send timestamp per output) and implement a reaper timer.
|
||||
- Xorg viewer remote controls: expose viewer state (zoom, pan, scale policy, anchor) as enumerable/settable controls via the protocol, analogous to V4L2 controls. Future extension: shader-based post-processing — initial candidates are a colour-correction shader and custom user-provided GLSL fragment shaders sent over the wire.
|
||||
- Unified device model: active display windows should be registered as devices alongside V4L2 cameras, using the same ENUM_DEVICES / ENUM_CONTROLS / GET_CONTROL / SET_CONTROL protocol. START_DISPLAY would return a device_id for the opened window; controls (scale, anchor, position, size, zoom, pan) are then addressable as (device_id, control_id) pairs like any other device. Requires a device_type field in ENUM_DEVICES responses so controllers can distinguish V4L2 devices from display windows. Future device types: codec processes, screen grab sources. This extends naturally to shader-based post-processing and other viewer state as controls.
|
||||
- Display viewer free pan/zoom mode: the current anchor system (center/topleft) only covers fixed alignment. A "free" mode should allow the controller (or the user via mouse/keyboard in the window) to set arbitrary pan offset and zoom level independently of the scale mode. The xorg viewer would need pan_x/pan_y (normalised or pixel offsets) and zoom_factor controls alongside the existing scale/anchor. This is a prerequisite for use cases like microscope inspection where the user needs to freely navigate a high-resolution source.
|
||||
- controller_cli is a temporary dev tool; the long-term replacement is a dedicated `controller` binary outside `dev/cli/` that maintains simultaneous connections to all discovered nodes (not switching between them). Commands address a specific node by peer index. This mirrors the web UI's model of administering the whole network rather than one node at a time. The `connect` / active-connection model in the current controller_cli is an interim design choice that should not be carried forward.
|
||||
- start-ingest peer addressing: the `dest_host` + `dest_port` in START_INGEST is awkward to type manually and requires the caller to know the target's TCP port. Should accept a peer ID (index from the discovered peer table on the node) so the node can resolve the address itself. Requires the node to run discovery and expose its peer table.
|
||||
- Connection multiplexing: currently each ingest stream opens its own outbound TCP connection to the destination. Multiple streams between the same two peers should share one connection, with stream_id used to demultiplex frames. This is the priority/encapsulation scheme described in the architecture — high-priority and low-latency frames from different streams travel over the same socket rather than competing across separate sockets.
|
||||
- Discovery boot nonce: the announcement payload needs a `boot_nonce` field (random u32 generated at startup, not configured). The current restart detection uses `site_id` change as a proxy, but `site_id` is static config and does not change on restart, so restarts are not detected and the restarted node waits up to `interval_ms` for peers to reply. Adding a boot nonce gives a reliable restart signal: a nonce change for a known (addr, port) entry triggers an immediate unicast reply. Requires a wire format version bump, peer table struct update, and changes to the announcement builder and receive logic.
|
||||
- Control grouping: controls should be organizable into named groups for both display organisation (collapsible sections in a UI) and protocol semantics (enumerate controls within a group, set a group of related controls atomically). Relevant for display devices where scale_mode, anchor, position, and size are logically related, and for cameras where white balance, exposure, and gain belong together. The current flat list of (control_id, name, type, value) tuples does not capture this.
|
||||
|
||||
@@ -56,11 +56,11 @@ static uint64_t now_ms(void) {
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
static int find_peer(struct Discovery *d, uint32_t addr, const char *name) {
|
||||
static int find_peer(struct Discovery *d, uint32_t addr, uint16_t tcp_port) {
|
||||
for (int i = 0; i < DISCOVERY_MAX_PEERS; i++) {
|
||||
if (d->peers[i].active
|
||||
&& d->peers[i].info.addr == addr
|
||||
&& strcmp(d->peers[i].info.name, name) == 0) {
|
||||
&& d->peers[i].info.addr == addr
|
||||
&& d->peers[i].info.tcp_port == tcp_port) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -76,19 +76,14 @@ static int find_slot(struct Discovery *d) {
|
||||
|
||||
/* -- send ------------------------------------------------------------------ */
|
||||
|
||||
static void send_announcement(struct Discovery *d) {
|
||||
size_t name_len = strlen(d->config.name);
|
||||
static size_t build_announcement(struct Discovery *d, uint8_t *buf) {
|
||||
size_t name_len = strlen(d->config.name);
|
||||
if (name_len > DISCOVERY_MAX_NAME_LEN) { name_len = DISCOVERY_MAX_NAME_LEN; }
|
||||
|
||||
uint32_t payload_len = (uint32_t)(ANN_FIXED_SIZE + name_len);
|
||||
size_t total = TRANSPORT_FRAME_HEADER_SIZE + payload_len;
|
||||
uint8_t buf[TRANSPORT_FRAME_HEADER_SIZE + ANN_FIXED_SIZE + DISCOVERY_MAX_NAME_LEN];
|
||||
|
||||
/* frame header */
|
||||
put_u16(buf, 0, 0x0010); /* message_type: DISCOVERY_ANNOUNCE */
|
||||
put_u16(buf, 0, 0x0010);
|
||||
put_u32(buf, 2, payload_len);
|
||||
|
||||
/* announcement payload */
|
||||
uint8_t *p = buf + TRANSPORT_FRAME_HEADER_SIZE;
|
||||
put_u8 (p, ANN_PROTOCOL_VERSION, DISCOVERY_PROTOCOL_VERSION);
|
||||
put_u16(p, ANN_SITE_ID, d->config.site_id);
|
||||
@@ -97,10 +92,26 @@ static void send_announcement(struct Discovery *d) {
|
||||
put_u8 (p, ANN_NAME_LEN, (uint8_t)name_len);
|
||||
memcpy(p + ANN_NAME, d->config.name, name_len);
|
||||
|
||||
return TRANSPORT_FRAME_HEADER_SIZE + payload_len;
|
||||
}
|
||||
|
||||
static void send_announcement(struct Discovery *d) {
|
||||
uint8_t buf[TRANSPORT_FRAME_HEADER_SIZE + ANN_FIXED_SIZE + DISCOVERY_MAX_NAME_LEN];
|
||||
size_t total = build_announcement(d, buf);
|
||||
sendto(d->sock, buf, total, 0,
|
||||
(struct sockaddr *)&d->mcast_addr, sizeof(d->mcast_addr));
|
||||
}
|
||||
|
||||
static void send_announcement_unicast(struct Discovery *d, uint32_t addr) {
|
||||
uint8_t buf[TRANSPORT_FRAME_HEADER_SIZE + ANN_FIXED_SIZE + DISCOVERY_MAX_NAME_LEN];
|
||||
size_t total = build_announcement(d, buf);
|
||||
struct sockaddr_in dest = {0};
|
||||
dest.sin_family = AF_INET;
|
||||
dest.sin_port = htons(DISCOVERY_PORT);
|
||||
dest.sin_addr.s_addr = addr;
|
||||
sendto(d->sock, buf, total, 0, (struct sockaddr *)&dest, sizeof(dest));
|
||||
}
|
||||
|
||||
/* -- timeout check --------------------------------------------------------- */
|
||||
|
||||
static void check_timeouts(struct Discovery *d) {
|
||||
@@ -198,19 +209,22 @@ static void *receive_thread_fn(void *arg) {
|
||||
|
||||
/* skip our own announcements */
|
||||
if (site_id == d->config.site_id
|
||||
&& strcmp(name, d->config.name) == 0) {
|
||||
&& tcp_port == d->config.tcp_port) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t addr = src.sin_addr.s_addr;
|
||||
uint64_t ts = now_ms();
|
||||
int is_new = 0;
|
||||
int is_new = 0;
|
||||
int reannounce = 0;
|
||||
struct Discovery_Peer peer_copy;
|
||||
|
||||
pthread_mutex_lock(&d->peers_mutex);
|
||||
|
||||
int idx = find_peer(d, addr, name);
|
||||
int idx = find_peer(d, addr, tcp_port);
|
||||
if (idx >= 0) {
|
||||
/* detect restart: same addr+port but site_id changed */
|
||||
if (d->peers[idx].info.site_id != site_id) { reannounce = 1; }
|
||||
d->peers[idx].last_seen_ms = ts;
|
||||
d->peers[idx].info.site_id = site_id;
|
||||
d->peers[idx].info.tcp_port = tcp_port;
|
||||
@@ -233,11 +247,12 @@ static void *receive_thread_fn(void *arg) {
|
||||
|
||||
pthread_mutex_unlock(&d->peers_mutex);
|
||||
|
||||
/* respond to every announcement — the sender may be a fresh instance
|
||||
* that doesn't know about us yet even if we already have it in our table */
|
||||
pthread_mutex_lock(&d->announce_mutex);
|
||||
pthread_cond_signal(&d->announce_cond);
|
||||
pthread_mutex_unlock(&d->announce_mutex);
|
||||
if (is_new || reannounce) {
|
||||
/* new peer, or peer restarted (site_id changed) — reply directly
|
||||
* to that host so it learns about us without waiting up to interval_ms.
|
||||
* Use unicast rather than multicast to avoid disturbing other nodes. */
|
||||
send_announcement_unicast(d, addr);
|
||||
}
|
||||
|
||||
if (is_new && d->config.on_peer_found) {
|
||||
d->config.on_peer_found(&peer_copy, d->config.userdata);
|
||||
|
||||
@@ -51,6 +51,14 @@ static struct App_Error wbuf_u16(struct Wbuf *b, uint16_t v) {
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_i16(struct Wbuf *b, int16_t v) {
|
||||
struct App_Error e = wbuf_grow(b, 2);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
put_i16(b->data, b->len, v);
|
||||
b->len += 2;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_u32(struct Wbuf *b, uint32_t v) {
|
||||
struct App_Error e = wbuf_grow(b, 4);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
@@ -366,7 +374,8 @@ struct App_Error proto_write_get_control_response(struct Transport_Conn *conn,
|
||||
struct App_Error proto_write_enum_devices_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status,
|
||||
const struct Proto_Media_Device_Info *media_devices, uint16_t media_count,
|
||||
const struct Proto_Standalone_Device_Info *standalone, uint16_t standalone_count)
|
||||
const struct Proto_Standalone_Device_Info *standalone, uint16_t standalone_count,
|
||||
const struct Proto_Display_Device_Info *displays, uint16_t display_count)
|
||||
{
|
||||
struct Wbuf b;
|
||||
struct App_Error e = wbuf_init(&b, 128);
|
||||
@@ -402,6 +411,19 @@ struct App_Error proto_write_enum_devices_response(struct Transport_Conn *conn,
|
||||
e = wbuf_str8(&b, standalone[i].name); if (!APP_IS_OK(e)) { goto fail; }
|
||||
}
|
||||
|
||||
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; }
|
||||
e = wbuf_u16(&b, d->win_w); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, d->win_h); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u8 (&b, d->scale_mode); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u8 (&b, d->anchor); if (!APP_IS_OK(e)) { goto fail; }
|
||||
}
|
||||
|
||||
e = transport_send_frame(conn, PROTO_MSG_CONTROL_RESPONSE, b.data, b.len);
|
||||
fail:
|
||||
wbuf_free(&b);
|
||||
@@ -602,13 +624,13 @@ struct App_Error proto_read_stop_ingest(
|
||||
}
|
||||
|
||||
/* START_DISPLAY: request_id(2) cmd(2) stream_id(2) win_x(2) win_y(2)
|
||||
* win_w(2) win_h(2) scale(1) anchor(1) = 16 bytes */
|
||||
* win_w(2) win_h(2) scale_mode(1) anchor(1) no_signal_fps(1) reserved(1) = 18 bytes */
|
||||
struct App_Error proto_write_start_display(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t stream_id,
|
||||
int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h,
|
||||
uint8_t scale, uint8_t anchor)
|
||||
uint8_t scale_mode, uint8_t anchor, uint8_t no_signal_fps)
|
||||
{
|
||||
uint8_t buf[16];
|
||||
uint8_t buf[18];
|
||||
uint32_t o = 0;
|
||||
put_u16(buf, o, request_id); o += 2;
|
||||
put_u16(buf, o, PROTO_CMD_START_DISPLAY); o += 2;
|
||||
@@ -617,10 +639,12 @@ struct App_Error proto_write_start_display(struct Transport_Conn *conn,
|
||||
put_i16(buf, o, win_y); o += 2;
|
||||
put_u16(buf, o, win_w); o += 2;
|
||||
put_u16(buf, o, win_h); o += 2;
|
||||
put_u8 (buf, o, scale); o += 1;
|
||||
put_u8 (buf, o, scale_mode); o += 1;
|
||||
put_u8 (buf, o, anchor); o += 1;
|
||||
put_u8 (buf, o, no_signal_fps); o += 1;
|
||||
put_u8 (buf, o, 0); o += 1; /* reserved */
|
||||
(void)o;
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 16);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 18);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_stop_display(struct Transport_Conn *conn,
|
||||
@@ -638,15 +662,16 @@ struct App_Error proto_read_start_display(
|
||||
struct Proto_Start_Display *out)
|
||||
{
|
||||
if (length < 16) { return APP_INVALID_ERROR_MSG(0, "START_DISPLAY payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->request_id = get_u16(payload, 0);
|
||||
/* skip command word at [2..3] */
|
||||
out->stream_id = get_u16(payload, 4);
|
||||
out->win_x = get_i16(payload, 6);
|
||||
out->win_y = get_i16(payload, 8);
|
||||
out->win_w = get_u16(payload, 10);
|
||||
out->win_h = get_u16(payload, 12);
|
||||
out->scale = get_u8 (payload, 14);
|
||||
out->anchor = get_u8 (payload, 15);
|
||||
out->stream_id = get_u16(payload, 4);
|
||||
out->win_x = get_i16(payload, 6);
|
||||
out->win_y = get_i16(payload, 8);
|
||||
out->win_w = get_u16(payload, 10);
|
||||
out->win_h = get_u16(payload, 12);
|
||||
out->scale_mode = get_u8 (payload, 14);
|
||||
out->anchor = get_u8 (payload, 15);
|
||||
out->no_signal_fps = length >= 18 ? get_u8(payload, 16) : 0;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
@@ -702,6 +727,12 @@ struct App_Error proto_read_enum_devices_response(
|
||||
const char *path, uint8_t path_len,
|
||||
const char *name, uint8_t name_len,
|
||||
void *userdata),
|
||||
void (*on_display)(
|
||||
uint16_t stream_id,
|
||||
int16_t win_x, int16_t win_y,
|
||||
uint16_t win_w, uint16_t win_h,
|
||||
uint8_t scale_mode, uint8_t anchor,
|
||||
void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
struct Cursor c;
|
||||
@@ -756,6 +787,27 @@ struct App_Error proto_read_enum_devices_response(
|
||||
if (on_standalone) { on_standalone(path, path_len, name, name_len, userdata); }
|
||||
}
|
||||
|
||||
/* Display section — optional; absent in messages from older nodes */
|
||||
if (c.ok && c.pos + 2 <= c.len) {
|
||||
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);
|
||||
uint16_t win_w = cur_u16(&c);
|
||||
uint16_t win_h = cur_u16(&c);
|
||||
uint8_t scale_mode = cur_u8(&c);
|
||||
uint8_t anchor = cur_u8(&c);
|
||||
CUR_CHECK(c);
|
||||
if (on_display) {
|
||||
on_display(device_id, stream_id, win_x, win_y,
|
||||
win_w, win_h, scale_mode, anchor, userdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
@@ -780,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);
|
||||
|
||||
|
||||
@@ -319,5 +319,11 @@ struct App_Error transport_send_frame(struct Transport_Conn *conn,
|
||||
}
|
||||
|
||||
void transport_conn_close(struct Transport_Conn *conn) {
|
||||
close(conn->fd);
|
||||
/* shutdown() rather than close(): signals EOF to the remote end and
|
||||
* unblocks the read thread without releasing the fd. The read thread
|
||||
* is the sole owner of the fd and will close() it when it exits.
|
||||
* Using close() here would create a race where the fd number could be
|
||||
* reused by the next transport_connect() before the detached read
|
||||
* thread calls its own close(), which would then close the wrong fd. */
|
||||
shutdown(conn->fd, SHUT_RDWR);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,31 @@ static const char *FRAG_YUV_SRC =
|
||||
" out_color = vec4(r, g, b, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
/*
|
||||
* Animated analog-TV noise — no textures, driven by u_time.
|
||||
* u_noise_res: cells per axis (lower = coarser pixels). Default ~80.
|
||||
* Uses the same VERT_SRC quad; u_uv_scale / u_uv_offset set to identity.
|
||||
*/
|
||||
static const char *FRAG_NOSIGNAL_SRC =
|
||||
"#version 330 core\n"
|
||||
"in vec2 v_uv;\n"
|
||||
"out vec4 out_color;\n"
|
||||
"uniform float u_time;\n"
|
||||
"uniform float u_noise_res;\n"
|
||||
"float hash(vec2 p) {\n"
|
||||
" p = fract(p * vec2(127.1, 311.7));\n"
|
||||
" p += dot(p, p + 19.19);\n"
|
||||
" return fract(p.x * p.y);\n"
|
||||
"}\n"
|
||||
"void main() {\n"
|
||||
" vec2 cell = floor(v_uv * u_noise_res) / u_noise_res;\n"
|
||||
" float tick = floor(u_time * 30.0);\n"
|
||||
" float n = hash(cell + tick * 0.017);\n"
|
||||
" float scan = 0.78 + 0.22 * sin(v_uv.y * u_noise_res * 6.2832 * 3.0);\n"
|
||||
" float luma = n * scan;\n"
|
||||
" out_color = vec4(luma * 0.72, luma * 0.80, luma * 0.62, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
/* Passthrough for BGRA — uploaded as GL_BGRA so driver swizzles to RGBA. */
|
||||
static const char *FRAG_RGB_SRC =
|
||||
"#version 330 core\n"
|
||||
@@ -189,6 +214,13 @@ struct Xorg_Viewer {
|
||||
Xorg_Anchor anchor;
|
||||
int frame_w, frame_h;
|
||||
|
||||
/* No-signal noise */
|
||||
GLuint prog_nosignal;
|
||||
GLint u_nosignal_uv_scale;
|
||||
GLint u_nosignal_uv_offset;
|
||||
GLint u_nosignal_time;
|
||||
GLint u_nosignal_res;
|
||||
|
||||
/* Solid rect (overlay background) */
|
||||
GLuint prog_rect;
|
||||
GLint u_rect_loc;
|
||||
@@ -397,6 +429,13 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||
v->u_uv_scale_rgb = glGetUniformLocation(v->prog_rgb, "u_uv_scale");
|
||||
v->u_uv_offset_rgb= glGetUniformLocation(v->prog_rgb, "u_uv_offset");
|
||||
|
||||
v->prog_nosignal = link_program(VERT_SRC, FRAG_NOSIGNAL_SRC);
|
||||
if (!v->prog_nosignal) { xorg_viewer_close(v); return NULL; }
|
||||
v->u_nosignal_uv_scale = glGetUniformLocation(v->prog_nosignal, "u_uv_scale");
|
||||
v->u_nosignal_uv_offset = glGetUniformLocation(v->prog_nosignal, "u_uv_offset");
|
||||
v->u_nosignal_time = glGetUniformLocation(v->prog_nosignal, "u_time");
|
||||
v->u_nosignal_res = glGetUniformLocation(v->prog_nosignal, "u_noise_res");
|
||||
|
||||
glGenVertexArrays(1, &v->vao);
|
||||
glGenTextures(4, v->tex);
|
||||
|
||||
@@ -817,6 +856,58 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* No-signal screen */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/*
|
||||
* Render one frame of animated analog-TV noise with a centred "NO SIGNAL"
|
||||
* label. time is seconds (e.g. from glfwGetTime()); noise_res is cells
|
||||
* per axis — lower = coarser pixels (default: 80).
|
||||
* Call at ~15 fps when the viewer has no live stream to display.
|
||||
*/
|
||||
void xorg_viewer_render_no_signal(Xorg_Viewer *v, float time, float noise_res)
|
||||
{
|
||||
if (!v) { return; }
|
||||
glfwMakeContextCurrent(v->window);
|
||||
|
||||
int fb_w, fb_h;
|
||||
glfwGetFramebufferSize(v->window, &fb_w, &fb_h);
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glViewport(0, 0, fb_w, fb_h);
|
||||
|
||||
glUseProgram(v->prog_nosignal);
|
||||
glUniform2f(v->u_nosignal_uv_scale, 1.0f, 1.0f);
|
||||
glUniform2f(v->u_nosignal_uv_offset, 0.0f, 0.0f);
|
||||
glUniform1f(v->u_nosignal_time, time);
|
||||
glUniform1f(v->u_nosignal_res, noise_res > 0.0f ? noise_res : 80.0f);
|
||||
|
||||
glBindVertexArray(v->vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
/* Measure "NO SIGNAL" text width to centre it. */
|
||||
const char *label = "NO SIGNAL";
|
||||
int text_w = 0, max_ascent = 0;
|
||||
for (const char *p = label; *p; p++) {
|
||||
unsigned char cp = (unsigned char)*p;
|
||||
text_w += font_glyphs[cp].advance;
|
||||
if (font_glyphs[cp].bearing_y > max_ascent) {
|
||||
max_ascent = font_glyphs[cp].bearing_y;
|
||||
}
|
||||
}
|
||||
int tx = (fb_w - text_w) / 2;
|
||||
int ty = (fb_h - max_ascent) / 2;
|
||||
|
||||
xorg_viewer_set_overlay_text(v, 0, tx, ty, label, 1.0f, 1.0f, 1.0f);
|
||||
draw_text_overlays(v, fb_w, fb_h);
|
||||
xorg_viewer_clear_overlays(v);
|
||||
|
||||
glfwSwapBuffers(v->window);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Poll and close */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -850,10 +941,11 @@ void xorg_viewer_close(Xorg_Viewer *v)
|
||||
if (v->tex_atlas) { glDeleteTextures(1, &v->tex_atlas); }
|
||||
if (v->prog_text) { glDeleteProgram(v->prog_text); }
|
||||
if (v->prog_rect) { glDeleteProgram(v->prog_rect); }
|
||||
if (v->vao) { glDeleteVertexArrays(1, &v->vao); }
|
||||
if (v->tex[0]) { glDeleteTextures(4, v->tex); }
|
||||
if (v->prog_yuv) { glDeleteProgram(v->prog_yuv); }
|
||||
if (v->prog_rgb) { glDeleteProgram(v->prog_rgb); }
|
||||
if (v->vao) { glDeleteVertexArrays(1, &v->vao); }
|
||||
if (v->tex[0]) { glDeleteTextures(4, v->tex); }
|
||||
if (v->prog_yuv) { glDeleteProgram(v->prog_yuv); }
|
||||
if (v->prog_rgb) { glDeleteProgram(v->prog_rgb); }
|
||||
if (v->prog_nosignal) { glDeleteProgram(v->prog_nosignal); }
|
||||
if (v->window) {
|
||||
glfwDestroyWindow(v->window);
|
||||
glfw_release();
|
||||
|
||||
@@ -23,7 +23,7 @@ $(NODE_BUILD)/video-node: $(MAIN_OBJ) \
|
||||
$(COMMON_OBJ) $(MEDIA_OBJ) $(V4L2_OBJ) $(SERIAL_OBJ) \
|
||||
$(TRANSPORT_OBJ) $(DISCOVERY_OBJ) $(CONFIG_OBJ) $(PROTOCOL_OBJ) \
|
||||
$(RECONCILER_OBJ) $(INGEST_OBJ) $(XORG_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread $(PKG_LDFLAGS)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread -lm $(PKG_LDFLAGS)
|
||||
|
||||
$(MAIN_OBJ): main.c | $(NODE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
186
src/node/main.c
186
src/node/main.c
@@ -7,6 +7,9 @@
|
||||
#include <pthread.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <math.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
@@ -175,7 +178,7 @@ struct Display_Slot {
|
||||
/* Config — written by handle_start_display before setting wanted */
|
||||
int win_x, win_y;
|
||||
int win_w, win_h;
|
||||
Xorg_Scale scale;
|
||||
Xorg_Scale scale_mode;
|
||||
Xorg_Anchor anchor;
|
||||
|
||||
/* Pending frame — deposited by transport thread, consumed by main */
|
||||
@@ -183,6 +186,11 @@ struct Display_Slot {
|
||||
uint32_t frame_len;
|
||||
int frame_ready;
|
||||
|
||||
/* No-signal animation */
|
||||
int no_signal_fps; /* 0 → default 15 */
|
||||
uint64_t last_frame_ms; /* monotonic ms of last successfully displayed frame */
|
||||
uint64_t last_no_signal_ms; /* monotonic ms of last no-signal render */
|
||||
|
||||
/* Viewer — created and used only on the main thread */
|
||||
Xorg_Viewer *viewer;
|
||||
};
|
||||
@@ -477,7 +485,7 @@ static void display_loop_tick(struct Node *node)
|
||||
Xorg_Viewer *v = xorg_viewer_open(
|
||||
d->win_x, d->win_y, d->win_w, d->win_h, title);
|
||||
if (v) {
|
||||
xorg_viewer_set_scale(v, d->scale);
|
||||
xorg_viewer_set_scale(v, d->scale_mode);
|
||||
xorg_viewer_set_anchor(v, d->anchor);
|
||||
d->viewer = v;
|
||||
pthread_mutex_lock(&d->mutex);
|
||||
@@ -502,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_mode;
|
||||
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;
|
||||
@@ -515,14 +531,31 @@ static void display_loop_tick(struct Node *node)
|
||||
}
|
||||
pthread_mutex_unlock(&d->mutex);
|
||||
|
||||
struct timespec _ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &_ts);
|
||||
uint64_t now_ms = (uint64_t)_ts.tv_sec * 1000u
|
||||
+ (uint64_t)_ts.tv_nsec / 1000000u;
|
||||
|
||||
if (fdata) {
|
||||
struct Proto_Video_Frame vf;
|
||||
if (APP_IS_OK(proto_read_video_frame(fdata, flen, &vf))) {
|
||||
xorg_viewer_push_mjpeg(d->viewer, vf.data, vf.data_len);
|
||||
d->last_frame_ms = now_ms;
|
||||
}
|
||||
free(fdata);
|
||||
}
|
||||
|
||||
/* Render no-signal only when stream has been silent for ≥1 s */
|
||||
if (now_ms - d->last_frame_ms >= 1000u) {
|
||||
uint64_t interval_ms = 1000u / (uint64_t)(d->no_signal_fps > 0 ? d->no_signal_fps : 15);
|
||||
if (now_ms - d->last_no_signal_ms >= interval_ms) {
|
||||
/* shader time: wrap ms to [0, 1000000) → [0, 1000) s, preserving float32 precision */
|
||||
float shader_t = (float)(now_ms % 1000000u) / 1000.0f;
|
||||
xorg_viewer_render_no_signal(d->viewer, shader_t, 80.0f);
|
||||
d->last_no_signal_ms = now_ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* Poll GLFW events; if user closes the window, treat as STOP_DISPLAY */
|
||||
if (!xorg_viewer_handle_events(d->viewer)) {
|
||||
xorg_viewer_close(d->viewer);
|
||||
@@ -762,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
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -804,10 +874,34 @@ 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++) {
|
||||
struct Display_Slot *d = &node->displays[i];
|
||||
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,
|
||||
.win_w = (uint16_t)d->win_w,
|
||||
.win_h = (uint16_t)d->win_h,
|
||||
.scale_mode = (uint8_t)d->scale_mode,
|
||||
.anchor = (uint8_t)d->anchor,
|
||||
};
|
||||
pthread_mutex_unlock(&d->mutex);
|
||||
if (!snap) { continue; }
|
||||
disp_infos[disp_count++] = info;
|
||||
}
|
||||
|
||||
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);
|
||||
standalone, (uint16_t)standalone_count,
|
||||
disp_infos, (uint16_t)disp_count);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); }
|
||||
}
|
||||
|
||||
@@ -823,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_mode = (int)disp->scale_mode;
|
||||
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_MODE,
|
||||
.type = 1, .name = "Scale Mode",
|
||||
.min = 0, .max = 3, .step = 1, .default_val = 1,
|
||||
.current_val = scale_mode },
|
||||
{ .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;
|
||||
@@ -852,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_MODE: value = (int32_t)disp->scale_mode; 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;
|
||||
@@ -884,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_MODE:
|
||||
if (req.value >= 0 && req.value <= 3) {
|
||||
disp->scale_mode = (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;
|
||||
@@ -1083,9 +1250,10 @@ static void handle_start_display(struct Node *node,
|
||||
d->win_y = (int)req.win_y;
|
||||
d->win_w = req.win_w > 0 ? (int)req.win_w : 1280;
|
||||
d->win_h = req.win_h > 0 ? (int)req.win_h : 720;
|
||||
d->scale = proto_scale_to_xorg(req.scale);
|
||||
d->anchor = proto_anchor_to_xorg(req.anchor);
|
||||
d->wanted_state = DISP_OPEN; /* reconciled by display_loop_tick */
|
||||
d->scale_mode = proto_scale_to_xorg(req.scale_mode);
|
||||
d->anchor = proto_anchor_to_xorg(req.anchor);
|
||||
d->no_signal_fps = req.no_signal_fps > 0 ? (int)req.no_signal_fps : 15;
|
||||
d->wanted_state = DISP_OPEN; /* reconciled by display_loop_tick */
|
||||
pthread_mutex_unlock(&d->mutex);
|
||||
|
||||
proto_write_control_response(conn, req.request_id, PROTO_STATUS_OK, NULL, 0);
|
||||
|
||||
Reference in New Issue
Block a user