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