On ledpi (three cold starts) the display acknowledged the start as its
socket opened, then took ~5 s to act on it while Vegas built its first
strip; the status routes meanwhile showed the display's own idle state, so
a UI polling every 700 ms flashed idle.
The display's on-demand state now names the request it answers
(request_id). A delivered start keeps reading as status "starting" with
delivered: true, in /display/on-demand/status and as on_demand_pending in
/display/current-status, until the display publishes state for that
request id (a display without the field: any state newer than the
delivery), for at most DELIVERED_SHOWN_SECONDS (30 s). The display's
startup state, which can be published after the acknowledgement, names no
request and does not end it.
Tests: stays starting against the startup idle state (no id, an older id);
the matching active state and the matching error take over; an older
display's newer state takes over; the 30 s cap; the display's state names
its request. Mutation check: 11 mutants, 11 killed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control socket is now the only way the web interface sends the display a
command. The display stops reading display_on_demand_request and
plugin_error_clear_request, and the web interface stops writing them.
- Display: no mailbox poll (MailboxWatch, the 1 s / 0.25 s cadence,
_consume_on_demand_request, the deprecation log) and no persisted
display_on_demand_processed_id guard; the error publisher reads no clear
request. CacheManager.file_signature and MailboxWatch are removed.
- A write to either retired key is dropped by CacheManager.save_cache and
logged once per writer, naming the plugin from the call stack (or the
request's plugin_id), with the API to move to.
- Web: on-demand start with no display listening starts the service (when
start_service) and sends the request again once the socket answers (45 s,
10 s for a running service without a socket yet); every other failure is
a 503 (400 for invalid_args). Stop answers 503 when no display listens,
unless stop_service. errors/clear answers 503 with a reason-specific
message instead of writing a request; clear_pending is always false.
src.ipc.client.should_fall_back is replaced by display_not_listening.
- Kept: display_current_state, display_on_demand_state,
plugin_runtime_snapshot and the heartbeat (read whenever the socket cannot
answer), and display_on_demand_config (the display's resume record).
Tests: mailbox-only tests removed (test_on_demand_mailbox.py, the mailbox
cadence, file_signature and MailboxWatch tests); tests that injected
requests through the mailbox now use the socket queue or a plugin's
in-process request. The run-loop harness sends on-demand requests over its
fake control socket, so four golden traces change: on-demand starts and
stops land at the request instant instead of the next 0.25 s mailbox look
(one frame fewer on the screen they end), and in vegas.json within one
frame instead of 263 ms, which shifts the later 1 s-throttled WiFi-notice
check by under a second.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The display serves a control socket (/run/ledmatrix/control.sock) carrying versioned JSON commands, one per line, each answered. Stage 1 covers on-demand start, stop and status; commands are queued on the socket thread and applied on the render thread through the mailbox's own handler, and the web interface falls back to the file mailbox when the socket is unavailable. Protocol and security model: docs/IPC_CONTROL_SOCKET.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>