mxlSleepUntil blocks the entire thread including LWS poll, causing the
control server to become unresponsive and grains to be missed. Instead,
check mxlGetNsUntilIndex and skip process() if the grain isn't due yet
(>2ms away). The node loop spins with poll(1) keeping LWS responsive,
and only attempts a grain read when it's nearly due.
Match the mxl-gst-sink pattern: sleep until the next grain's delivery
deadline using mxlSleepUntil, then read with a short 5ms timeout.
Uses 2-grain read delay (40ms at 50fps) for buffering headroom.
Previous approach of mxlSleepForNs + 20ms GetGrain timeout caused the
passthrough to fall behind: each iteration took ~25ms (poll+sleep+read),
missing grains and perpetually chasing headIndex via realign.
Instead of chasing headIndex from mxlFlowReaderGetRuntimeInfo (which
points to the NEXT grain to be written, causing perpetual TOO_EARLY),
the passthrough now uses mxlTimestampToIndex + mxlGetNsUntilIndex for
proper timing alignment, matching the pattern used by mxl-gst-sink.
Key changes:
- realign() computes read_index from current TAI time minus 1 grain delay
- process() uses mxlSleepForNs to wait until the target grain is due
- on_add_reader fetches grain_rate from mxlFlowConfigInfo
- Removed separate write_index_ (uses grain_info.index for writer)
- control_server: add Connection: close header + always return -1 after
HTTP response to force-close connection. Without this, lws_service()
blocks forever after the first POST /cmd, freezing the node process loop.
- passthrough: initialize read_index_ to runtime.headIndex when reader
is added (prevents TOO_EARLY errors on first read)
- passthrough: handle MXL_ERR_OUT_OF_RANGE_TOO_EARLY in addition to
TOO_LATE (both jump to head index)
- passthrough: use grain_info.index as writer index instead of separate
write_index_ counter (MXL writers must use TAI-based grain indices)
- passthrough: reduce grain read timeout from 100ms to 20ms for tighter
loop with LWS poll
- node_runner: add 'status' command handler that sends node status
back via control server
- Node control server now accepts HTTP POST /cmd for command dispatch
(in addition to existing WebSocket control)
- Added NodeControlClient: engine sends commands to nodes via HTTP POST
- New REST endpoints:
POST /api/graph/nodes/:id/connect-input - connect node input to MXL flow
POST /api/graph/nodes/:id/connect-output - create new MXL flow + connect output
POST /api/graph/nodes/:id/disconnect-port - remove reader/writer
POST /api/graph/nodes/:id/command - send raw command to node
- Flow IDs now use proper UUID v4 format (MXL requires standard UUIDs)
- Flow definitions use NMOS format (urn:x-nmos:format:video)
- Engine passes --mxl-domain to node processes
- User-provided node IDs (via 'id' field in POST body)
- End-to-end verified: testsrc → passthrough → new MXL output flow
- Use lws_http_get_uri_and_method() for proper HTTP method detection
(GET, POST, PUT, DELETE all supported)
- Handle GET/DELETE in LWS_CALLBACK_HTTP without waiting for body
- Support user-provided node IDs via config.id field
- Fixes all REST API endpoints hanging on GET requests
LWS v4.x requires an HTTP mount with LWSMPRO_CALLBACK to properly
route WebSocket upgrade requests to the dmf-control protocol handler.
Without this, WS connections were rejected with 403 Forbidden.