Display controls (enum/get/set):
- Add PROTO_DISPLAY_CTRL_SCALE/ANCHOR/NO_SIGNAL_FPS constants to protocol.h
- handle_enum_controls: if device index maps to an active display slot,
return the three display controls (scale, anchor, no_signal_fps)
- handle_get_control: read display control values from slot under mutex
- handle_set_control: write display control values to slot under mutex;
scale/anchor are applied to the viewer by display_loop_tick each tick
Device IDs in enum-devices output:
- Proto_Display_Device_Info gains device_id field (wire format +2 bytes)
- handle_enum_devices computes device_id = total_v4l2 + display_index
- on_video_node/on_standalone callbacks take int* userdata to print [idx]
- on_display prints [device_id] from the wire field
Bug fix — protocol error on invalid device index:
- proto_read_enum_controls_response: early-return APP_OK after reading
status if status != OK; error responses have no count/data fields, so
the CUR_CHECK on count was failing with "payload too short"
Helpers added to main.c:
- count_v4l2_devices(): sum of media vnodes + standalone
- find_display_by_device_idx(): maps flat index to Display_Slot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
transport_conn_close previously called close(conn->fd), but the detached
read thread also calls close(conn->fd) when it exits. If the kernel reused
the fd number before the read thread ran, the thread's close() would hit
the new connection — explaining connections that appeared to not terminate.
Fix: use shutdown(SHUT_RDWR) instead. This signals EOF to the remote end
and unblocks the blocked read() without releasing the fd. The read thread
remains the sole owner of the fd and is the only one to call close().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- controller_cli: drain semaphore and reset pending_cmd in do_connect
so stale posts from old connection don't unblock the next command
- protocol: add Proto_Display_Device_Info; extend
proto_write_enum_devices_response and proto_read_enum_devices_response
with display section; backward-compatible (absent in older messages)
- node: handle_enum_devices snapshots active Display_Slots under mutex
and includes them in the response
- controller_cli: on_display callback prints display window info in
enum-devices output
- query_cli: updated to pass NULL on_display (no display interest)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace double wall-time with uint64_t monotonic milliseconds for
last_frame_ms and last_no_signal_ms. Integer ms is the right type
for a threshold comparison — no floating point needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The loop runs at ~200Hz; frames arrive at ~30fps. Most iterations have no
pending frame even during active streaming, so no-signal was rendering
between real frames. Fix: track last_frame_t and suppress no-signal while
a live stream is present (< 1s since last frame).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLOCK_MONOTONIC returns seconds since boot (~50000+s on a running system).
At that magnitude, float32 loses fractional precision in the hash function
and all cells evaluate to near-zero, producing a black screen instead of noise.
Wrapping to fmod(now, 1000.0) keeps the value small enough for the shader.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two nodes on the same host with the same name (e.g. unnamed:0) would
collide — the second announcement just updated the first entry's port.
Peer identity is addr+port; name is metadata, not identity.
Same fix applied to the self-skip check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a viewer window has no incoming stream, renders animated analog-TV
noise (hash-based, scanlines, phosphor tint) at configurable fps (default
15) with a centred "NO SIGNAL" text overlay.
- xorg: FRAG_NOSIGNAL_SRC shader + xorg_viewer_render_no_signal(v, time, noise_res)
- main: Display_Slot gains no_signal_fps + last_no_signal_t; display_loop_tick
drives no-signal render on idle slots via clock_gettime rate limiting
- protocol: START_DISPLAY extended by 2 bytes — no_signal_fps (0=default 15)
+ reserved; reader is backward-compatible (defaults 0 if length < 18)
- controller_cli: no_signal_fps optional arg on start-display
- docs: protocol.md updated with new field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol:
- Add PROTO_CMD_START_DISPLAY (0x000A) and PROTO_CMD_STOP_DISPLAY (0x000B)
with write/read functions; Proto_Start_Display carries stream_id, window
position/size, scale and anchor; PROTO_DISPLAY_SCALE_*/ANCHOR_* constants
Node display sink:
- Display_Slot struct with wanted_state/current_state (DISP_CLOSED/DISP_OPEN);
handlers set wanted state, display_loop_tick on main thread reconciles
- Up to MAX_DISPLAYS (4) simultaneous viewer windows
- on_frame routes incoming VIDEO_FRAME messages to matching display slot;
transport thread deposits payload, main thread consumes without holding lock
during JPEG decode/upload
- Main thread runs GL event loop when xorg is available; headless fallback
joins reconciler timer thread as before
Xorg multi-window:
- Ref-count glfwInit/glfwTerminate via glfw_acquire/glfw_release so closing
one viewer does not terminate GLFW for remaining windows
- Add glfwMakeContextCurrent before GL calls in push_yuv420, push_bgra,
push_mjpeg and poll so each viewer uses its own GL context correctly
Transport random port:
- Bind port 0 lets the OS assign a free port; getsockname reads it back
into server->bound_port after bind
- Add transport_server_get_port() accessor
- Default tcp_port changed from 8000 to 0 (random); node prints actual
port after server start so it is always visible in output
- Add --port PORT CLI override (before config-file argument)
controller_cli:
- Add start-display and stop-display commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'force' phony prerequisite to all sub-make delegation rules in
dev/cli/Makefile and src/node/Makefile so the sub-make is always
invoked and can check source timestamps itself; previously a stale
.o would never be rebuilt by a dependent Makefile
- Move stream_stats_record_frame inside the successful send branch in
on_ingest_frame so stats reflect actual delivered frames rather than
capture throughput; avoids misleading Mbps readings when the
transport is disconnected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each ingest stream gets two reconciler resources (device, transport) with
dependencies: transport waits for device OPEN (needs format for STREAM_OPEN),
device waits for transport CONNECTED before starting capture.
START_INGEST sets wanted state and triggers a tick; the reconciler drives
device CLOSED→OPEN→STREAMING and transport DISCONNECTED→CONNECTED over
subsequent ticks. STOP_INGEST reverses both.
External events (transport drop, ingest thread error) use
reconciler_force_current to push state backward; the periodic 500ms timer
thread re-drives toward wanted state automatically.
All 8 stream slots are pre-allocated at startup. on_ingest_frame sends
VIDEO_FRAME messages over the outbound transport connection, protected by
a per-stream conn_mutex.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
START_INGEST carries stream_id, format/width/height/fps, dest_host:port,
transport_mode (encapsulated or opaque), and device_path. All format fields
default to 0 (auto-select). STOP_INGEST carries stream_id only.
Both commands set wanted state on the node; reconciliation is asynchronous.
Protocol doc updated with wire schemas for both commands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reconciler: generic resource state machine — BFS pathfinding from current
to wanted state, dependency constraints, event/periodic tick model.
reconciler_cli exercises it with simulated device/transport/stream resources.
ingest: V4L2 capture module — open device, negotiate MJPEG format, MMAP
buffer pool, capture thread with on_frame callback. start/stop lifecycle
designed for reconciler management. Transport-agnostic: caller wires
on_frame to proto_write_video_frame.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- tools/gen_font_atlas: Python/Pillow build tool — skyline packs DejaVu
Sans glyphs 32-255 into a grayscale atlas, emits build/gen/font_atlas.h
with pixel data and Font_Glyph[256] metrics table
- xorg: bitmap font atlas text overlay rendering (GL_R8 atlas texture,
alpha-blended glyph quads, dark background rect per overlay)
- xorg: add xorg_viewer_set_overlay_text / clear_overlays API
- xorg: add xorg_viewer_handle_events for streaming use (events only,
no redundant render)
- xorg_cli: show today's date as white text overlay
- v4l2_view_cli: new tool — V4L2 capture with format auto-selection
(highest FPS then largest resolution), MJPEG/YUYV, measured FPS overlay
- docs: update README, planning, architecture to reflect current status
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
src/node/main.c: ctrl_enum_cb was discarding menu_count and menu_items,
causing empty dropdowns for all MENU/INTEGER_MENU controls. Added a
menu item pool (MAX_MENU_POOL=128 items) to Ctrl_Build; the callback now
copies items into the pool and sets menu_count/menu_items on the control.
docs/protocol.md: add missing sections — str8 primitive, ENUM_DEVICES,
ENUM_CONTROLS (with control type/flag tables and menu item notes),
GET_CONTROL, and SET_CONTROL schemas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Protocol module: framed binary encoding for control requests/responses
(ENUM_DEVICES, ENUM_CONTROLS, GET/SET_CONTROL, STREAM_OPEN/CLOSE)
- video-node: scans /dev/media* and /dev/video*, serves V4L2 device
topology and controls over TCP; uses UDP discovery for peer announce
- query_cli: auto-discovers a node, queries devices and controls
- protocol_cli: low-level protocol frame decoder for debugging
- dev/web: Express 5 ESM web inspector — live SSE discovery picker,
REST bridge to video-node, controls UI with sliders/selects/checkboxes
- Makefile: sequential module builds before cli/node to fix make -j races
- common.mk: add DEPFLAGS (-MMD -MP) for automatic header dependencies
- All module Makefiles: split compile/link, generate .d dependency files
- discovery: replace 100ms poll loop with pthread_cond_timedwait;
respond to all announcements (not just new peers) for instant re-discovery
- ENUM_DEVICES response: carry device_caps (V4L2_CAP_*) per video node
so clients can distinguish capture nodes from metadata nodes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Config_Entry now holds a union {s, u16, u32, flags} typed at parse time.
Getters read directly from the union — no string conversion at access time.
config_dump reconstructs flag display as 'token | token' from the bitmask.
Separators in flag values: comma, pipe, and whitespace all accepted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Config_Def schema tables declare section/key/type/default per module.
Typed getters: config_get_str, _u16, _u32, _flags.
FLAGS type parses space/comma-separated tokens via a Config_Flag_Def table.
config_defaults() gives schema defaults without a file.
config_dump() prints effective values for diagnostics.
config_cli: load a file or --defaults and dump effective config.
dev/example.cfg: sample config covering node, discovery, transport.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the receive thread detects a genuinely new peer it sets
early_announce, causing the announce thread to break out of its
sleep and send immediately. The new node receives our announcement
within ~100ms rather than waiting up to interval_ms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sends 6-byte framed announcements to 224.0.0.251:5353 on startup and
every interval_ms (default 5s). Receive thread maintains a peer table
(max 64 entries); fires on_peer_found for new peers, on_peer_lost when
a peer misses timeout_intervals (default 3) consecutive intervals.
Own announcements are filtered by name+site_id. SO_REUSEADDR+REUSEPORT
allows multiple processes on the same host for testing.
discovery_cli: announce <name> <tcp_port> [flags] — prints found/lost events.
Also notes future config module in planning.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes channel_id from the header. All message-specific identifiers
(stream_id, request_id, etc.) now live at the start of the payload,
interpreted by each message type handler. A relay seeing an unknown
type can skip or forward it using only payload_length, with no
knowledge of the payload structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
transport_server_create/start: binds TCP, spawns accept thread, closes
excess inbound connections when max_connections is reached.
transport_connect: outbound TCP, spawns read thread before returning.
transport_send_frame: packs 8-byte header with serial put_*, then writes
header + payload under a per-connection mutex (thread-safe).
Read thread: reads header, validates payload_length <= max_payload, mallocs
payload, calls on_frame (callback owns and must free payload). On error or
disconnect calls on_disconnect then frees conn.
transport_cli: server mode echoes received frames; client mode sends 3
test frames and prints echoes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
put_u8/16/32/64 and get_u8/16/32/64 (plus signed variants) for packing
and unpacking values at explicit byte offsets in a buffer. Each value is
encoded byte-by-byte with explicit shift operations — no struct casting,
no alignment assumptions, correct on any host endianness.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CC, CFLAGS, and BUILD are now defined once in common.mk at the repo root.
Each module and CLI Makefile sets ROOT then includes common.mk, eliminating
the repeated definitions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
architecture.md: replace JSON control payloads with binary serialization;
add Protocol Serialization section describing little-endian wire format,
put/get buffer layer, and write_*/read_* protocol layer.
planning.md: mark common/media_ctrl/v4l2_ctrl done; insert serial (#4)
and protocol (#6) modules with descriptions.
conventions.md: document -flto and its implication (no manual static for
inlining — compiler handles it at link time).
Makefiles: add -flto to CFLAGS in all four Makefiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CLI is a separate translation unit that doesn't include <linux/media.h>,
so the kernel flag constants were undefined there. Fix by defining our own
public constants in media_ctrl.h:
- MEDIA_LINK_FL_{ENABLED,IMMUTABLE,DYNAMIC} for link flags
- MEDIA_PAD_FLAG_{SINK,SOURCE} for pad flags (distinct names avoid
redefinition conflict with kernel's MEDIA_PAD_FL_* in media_ctrl.c)
media_ctrl.c now translates kernel flags to our constants on the way out.
Added fallback #defines for kernel constants that were missing them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Modules (src/modules/):
- common/error: App_Error struct with structured detail union, error codes,
app_error_print(); designed for future upgrade to preprocessor-generated
location codes
- media_ctrl: media device enumeration, topology query (entities/pads/links),
link enable/disable via Media Controller API (/dev/media*)
- v4l2_ctrl: control enumeration (with menu item fetching), get/set via
V4L2 ext controls API, device discovery (/dev/video*)
All modules use -std=c11 -D_GNU_SOURCE, build artifacts go to build/ only.
Kernel-version-dependent constants guarded with #ifdef + #warning.
CLI drivers (dev/cli/):
- media_ctrl_cli: list, info, topology, set-link subcommands
- v4l2_ctrl_cli: list, controls, get, set subcommands
Docs (docs/cli/):
- media_ctrl_cli.md and v4l2_ctrl_cli.md with usage, examples, and
context within the video routing system
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>