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>
* feat(plugins): request_on_demand() / end_on_demand() -- plugins ask for the screen in-process
Four plugins (birdnet-go, mqtt-notifications, on-air, pomodoro-timer) take
the screen by writing the display_on_demand_request mailbox, which the
display reads once a second while the control socket is up and which stage 5
removes. This is the in-process way in that stage needed.
- BasePlugin.request_on_demand(mode=None, duration=None, pinned=False) and
end_on_demand(), safe from any thread, go through PluginManager to
DisplayController.submit_plugin_on_demand, which only queues (at most 32)
and wakes the render thread through ControlServer.wake(). The render
thread applies them in _drain_control_commands, after socket commands,
through _handle_on_demand_request, so they land within a frame; without a
socket, on the next pending-changes pass.
- A plugin's stop ends only its own session; a mailbox stop still ends any.
- Both answer the request id, or None with no display in the process (web
interface, check_plugin.py), a full queue, or a mock manager -- a plugin's
cue to write the mailbox, which the display still reads.
- docs/PLUGIN_API_REFERENCE.md documents the hasattr pattern for plugins
that must keep working on older cores; IPC_CONTROL_SOCKET.md and the
CHANGELOG are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(display): wire the on-demand handler only on a manager that has it
Tests and the golden traces stand in simpler plugin managers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>