Skip to content

feat(viewer): blank/unblank commands to turn the display off - #3065

Merged
vpetersson merged 3 commits into
masterfrom
feat/viewer-display-blank
Jun 11, 2026
Merged

feat(viewer): blank/unblank commands to turn the display off#3065
vpetersson merged 3 commits into
masterfrom
feat/viewer-display-blank

Conversation

@vpetersson

Copy link
Copy Markdown
Contributor

Motivation

There was no way to turn an Anthias display off. On a Wayland board you can run wlr-randr --output X --off externally, but on eglfs/Pi boards the Qt app owns the DRM master, so the display can't be powered off from outside the process at all (DPMS sysfs writes are rejected; vcgencmd display_power is a no-op under vc4-KMS). This adds a first-class, cross-platform way to blank the screen.

What it does

Two new viewer commands on the anthias.viewer Redis channel, alongside next/previous/stop/play:

  • blank — darken the screen and pause playback.
    • Wayland (x86 / pi5 / arm64): wlr-randr powers every connector off (true DPMS power-off, monitor sleeps).
    • eglfs / linuxfb (pi2 / pi3 / pi4): the Qt app holds the DRM master and can't be powered off externally, so the asset loop paints a new all-black BLACK_SCREEN image (the same proven loadImage path as the standby screen). Backlight stays on; the screen goes black.
  • unblank — power the connector back on (Wayland) and resume playback.

_wlr_output_names() gained an include_disabled flag so unblank can re-enable a connector that's currently Enabled: no.

Threading

blank_display() / unblank_display() run on the subscriber thread: they flip display_blanked + loop_is_stopped and issue the out-of-process wlr-randr call there, but defer the webview repaint to the main loop thread (start_loop), which owns current_browser_url — mirroring the existing rotation-bounce discipline. view_image() no-ops once the URL is already BLACK_SCREEN, so the paint costs one loadImage and then idles.

Testing

  • 6 new unit tests (wlr power targets enabled+disabled outputs; no-op off Wayland; blank/unblank state + power calls on wayland vs eglfs; commands registered). Full non-integration suite green (1121 passed).
  • Live on x86 (Wayland): blankSet output DP-1 --off, wlr-randr reports Enabled: no; unblankSet output DP-1 --on, Enabled: yes.
  • eglfs: the black-paint reuses the exact loadImage path the standby/splash screen already uses on Pi 4 (verified rendering there), and black.png serving was confirmed (200) after a server restart. Full on-device eglfs confirmation needs a Pi on a build that contains this change (the Pi 4 testbed is a few commits behind master); the screen is headless so the proof there is the webview fetching /static/img/black.png + the loop pausing.

Notes

  • Backlight is not powered off on eglfs/linuxfb (not externally controllable while Qt holds the DRM master) — the screen goes black instead. Wayland boards get a true power-off.

Adds a way to blank the screen on demand, parallel to the existing
next/previous/stop/play viewer commands on the anthias.viewer Redis
channel:

* Wayland boards (x86/pi5/arm64): wlr-randr powers the connector off
  (true DPMS power-off) and back on. _wlr_output_names() gained an
  include_disabled flag so unblank can re-enable a connector that's
  currently Enabled: no.
* eglfs/linuxfb boards (pi2/pi3/pi4): the Qt app owns the DRM master
  and can't be powered off externally, so the asset loop paints a new
  all-black BLACK_SCREEN image instead (same proven loadImage path as
  the standby screen). Backlight stays on; the screen goes black.

blank_display() flips display_blanked + loop_is_stopped from the
subscriber thread and runs the out-of-process wlr-randr call there; the
webview repaint is deferred to the main loop thread (start_loop), which
owns current_browser_url — mirroring the existing rotation-bounce
threading discipline.

Validated live on x86 (wayland): `blank` -> DP-1 Enabled: no, `unblank`
-> Enabled: yes. eglfs black-paint reuses the standby loadImage path.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson
vpetersson requested a review from a team as a code owner June 11, 2026 09:51
@vpetersson
vpetersson requested a review from Copilot June 11, 2026 09:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class “blank/unblank” viewer commands to support turning displays off (Wayland via wlr-randr) or blanking to black (eglfs/linuxfb via a black image) when the viewer owns DRM master, integrating with the existing Redis anthias.viewer command channel.

Changes:

  • Introduces blank / unblank commands and supporting state (display_blanked) in the viewer.
  • Adds wlroots output-power toggling via wlr-randr and extends _wlr_output_names() to optionally include disabled outputs.
  • Adds unit tests covering power targeting, platform no-ops, state transitions, and command registration.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/anthias_viewer/__init__.py Implements wlroots DPMS on/off, blank/unblank command handlers, state, and main-loop blank painting.
src/anthias_viewer/constants.py Adds BLACK_SCREEN URL constant for eglfs/linuxfb blanking.
tests/test_viewer.py Adds unit tests for wlroots power behavior and blank/unblank commands/state.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/anthias_viewer/__init__.py Outdated
Comment thread src/anthias_viewer/__init__.py Outdated
- Import get_skip_event from anthias_viewer.utils in the test instead
  of via the viewer module, which doesn't explicitly re-export it
  (mypy attr-defined under --no-implicit-reexport).
- start_loop: guard the black repaint on current_browser_url so it
  doesn't re-call view_image() — and re-log "Current url ..." at INFO —
  on every 0.1s tick while blanked (Copilot).
- Route stop/play through module-level helpers that set the
  loop_is_stopped global start_loop actually reads; the prior
  setattr(__main__, ...) wrote a dead namespace under
  `python -m anthias_viewer` and never paused the loop. play now
  implies unblank when the display is blanked (Copilot).
- Add tests for stop flag + play-implies-unblank.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson

Copy link
Copy Markdown
Contributor Author

On-device eglfs validation — done. Built this branch's viewer+server as derived images on top of the 3f05ead (current master) pi4-64 base, streamed them to a real Raspberry Pi 4 testbed (eglfs, QT_QPA_PLATFORM=eglfs, DEVICE_TYPE=pi4-64) over the LAN, and exercised the commands:

  • viewer blank → server access log shows the webview fetch GET /static/img/black.png 200, and the asset loop stops cycling (paused). Screen goes black.
  • While blanked: zero repeated black.png loads/logs over a 6 s window — confirms the current_browser_url guard (no per-100 ms INFO spam).
  • viewer unblank → playback resumes (Showing asset … (video)).

Viewer booted clean on the migrated config (no prefer_dark_mode KeyError). So the eglfs black-paint path is confirmed end-to-end on hardware, complementing the live Wayland (wlr-randr off/on) validation on x86.

http is intentional — the viewer talks to the local anthias-server over
plain HTTP (TLS is the opt-in Caddy sidecar's job), identical to the
existing STANDBY_SCREEN / SPLASH_PAGE_URL. Annotate the line # NOSONAR,
the repo's documented convention for Sonar false positives under
Automatic Analysis (see sonar-project.properties; cf. test_csrf.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@sonarqubecloud

Copy link
Copy Markdown

@vpetersson
vpetersson merged commit a3aa63f into master Jun 11, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants