20 Commits

Author SHA1 Message Date
8fa2f33bad Rename scale → scale_mode in protocol/struct layer; add control grouping future note
- `Proto_Display_Device_Info.scale` → `scale_mode`
- `Proto_Start_Display.scale` → `scale_mode`
- `PROTO_DISPLAY_CTRL_SCALE` → `PROTO_DISPLAY_CTRL_SCALE_MODE`
- `proto_write_start_display` param and all callers updated
- `on_display` callback param and all sites updated
- `Display_Slot.scale` → `scale_mode` in node
- Control name "Scale" → "Scale Mode"
- planning.md: add control grouping deferred decision

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:54:22 +00:00
8c4cd69443 Display device controls; device IDs in enum-devices; fix non-OK parse
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>
2026-03-29 22:02:42 +00:00
835cbbafba Fix connect accumulation; add display sinks to enum-devices
- 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>
2026-03-29 19:48:22 +00:00
54d48c9c8e Add no-signal animation to display windows
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>
2026-03-29 19:20:53 +00:00
32d31cbd1e Add display sink: START_DISPLAY/STOP_DISPLAY, multi-window xorg, random port
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>
2026-03-29 08:03:21 +00:00
6747c9e00d Wire reconciler and ingest into video node
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>
2026-03-29 02:17:16 +00:00
6c9e0ce7dc Add START_INGEST and STOP_INGEST protocol commands
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>
2026-03-29 02:02:38 +00:00
639a84b1b9 Add reconciler and ingest modules with CLI driver
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>
2026-03-29 01:52:17 +00:00
61c81398bb feat: node-to-node MJPEG streaming CLIs and shared V4L2 format header
Add stream_send_cli (V4L2 capture → TCP → VIDEO_FRAME) and
stream_recv_cli (TCP → threaded frame slot → GLFW display) to
exercise end-to-end streaming between two nodes on the same machine
or across the network.

Add include/stream_stats.h (header-only rolling-window fps/Mbps tracker)
and include/v4l2_fmt.h (header-only V4L2 format enumeration shared between
v4l2_view_cli and stream_send_cli). Refactor v4l2_view_cli to use the
shared header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:31:54 +00:00
611376dbc1 feat: xorg text overlays, font atlas generator, v4l2_view_cli
- 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>
2026-03-28 22:13:59 +00:00
7fd79e6120 feat: xorg viewer scale modes, resize fix, arch notes
Scale modes (STRETCH/FIT/FILL/1:1) with CENTER/TOP_LEFT anchor:
- UV crop via u_uv_scale/u_uv_offset uniforms in vertex shader
- glViewport sub-rect + glClear for FIT and 1:1 modes
- xorg_viewer_set_scale() / xorg_viewer_set_anchor() setters
- Stub implementations for both

Resize fix: glfwSetWindowUserPointer + framebuffer_size_callback calls
render() synchronously during resize so image tracks window edge
immediately. Forward declaration added to fix implicit decl error.

Q/Escape close the window via key_callback.

xorg_cli: --scale and --anchor arguments added.

architecture.md:
- Scale mode table and anchor docs in Frame Viewer Sink section
- Render loop design note: frame-driven not timer-driven, resize callback
  rationale, threading note (GL context ownership, frame queue)
- Text overlay section: tier 1 bitmap atlas (Pillow build tool, skyline
  packing, quad rendering), tier 2 HarfBuzz+FreeType, migration path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:30:28 +00:00
a1b52145d0 feat: add test_image module, xorg viewer sink, and feature flag build system
Feature flags (FEATURES= make variable):
  glfw       — GLFW + OpenGL viewer (libglfw3, libglew)
  vulkan     — future Vulkan renderer
  turbojpeg  — MJPEG decode/encode (libturbojpeg)
  xorg       — XRandR geometry + screen grab (libx11, libxrandr)
  vaapi      — VA-API hardware codec (libva)
Each flag injects -DHAVE_<FEATURE> and the relevant pkg-config flags.
Headless build: make (no FEATURES set).

test_image module (src/modules/test_image/):
  Generates test frames in YUV420, YUV422, and BGRA.
  Patterns: SMPTE 75% colour bars, greyscale ramp, white/black grid.
  BT.601 limited-range RGB→YCbCr in write_pixel().

test_image_cli (dev/cli/):
  Generates a frame and writes it as a PPM file for visual verification.
  Usage: test_image_cli [--pattern bars|ramp|grid]
                        [--width N] [--height N]
                        [--format yuv420|yuv422|bgra]
                        --out FILE.ppm

xorg module (src/modules/xorg/):
  xorg.c      — full GLFW+OpenGL implementation (compiled with FEATURES=glfw)
  xorg_stub.c — no-op stub (compiled otherwise; xorg_available() returns false)
  Renderer: full-screen quad via gl_VertexID, three GL_R8 textures for YUV,
  BT.601 matrix in fragment shader, GL_BGRA texture for packed frames.
  MJPEG path: tjDecompressToYUVPlanes → planar YUV → upload (requires turbojpeg).
  push_yuv420/push_bgra/push_mjpeg all usable independently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 20:54:07 +00:00
62c25247ef Add protocol module, video-node binary, query/web CLI tools
- 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>
2026-03-27 01:04:56 +00:00
ba26bd0cb7 Add config module: INI loader with schema-driven defaults
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>
2026-03-26 22:37:53 +00:00
fe350e531e Add discovery module: UDP multicast announcements and peer tracking
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>
2026-03-26 22:19:56 +00:00
197ab7d5db Simplify frame header to message_type + payload_length (6 bytes)
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>
2026-03-26 22:00:43 +00:00
ff48559b12 Add transport module: TCP framing, thread-per-connection, inbound limit
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>
2026-03-26 21:31:55 +00:00
b56dfae672 Add serial module — little-endian binary serialization primitives
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>
2026-03-25 22:15:41 +00:00
4ba2a8118d Fix MEDIA_LNK_FL_* and MEDIA_PAD_FL_* visibility in CLI translation unit
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>
2026-03-25 21:51:49 +00:00
a29c556851 Add common, media_ctrl and v4l2_ctrl modules with CLI drivers and docs
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>
2026-03-25 21:40:37 +00:00