mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
The display service PNG-encoded its frame to /tmp/led_matrix_preview.png at 5 fps, 24/7 — identical frames, no viewers, per-call imports and a chmod every write. On the devpi baseline the display service idles at ~92% CPU; this was one of its biggest fixed costs. - New pure policy (src/common/snapshot_policy.py, unit-tested off-Pi): WRITE changed frames at full rate only while a viewer is watching, at a 30s idle cadence otherwise; NEVER re-encode unchanged frames — bump mtime (os.utime) every 20s instead, keeping the health check's snapshot-age liveness proxy (60s threshold in api_v3) green. Cross- referencing comments guard the two constants. - Viewer detection: the web SSE display broadcaster (which only runs while browsers are subscribed) touches /tmp/led_matrix_preview_viewer each loop; the display service stats it at most 1/s. On viewer arrival the write clock resets so the first frame lands within ~1s. - Hoisted the per-call pathlib/permission_utils imports; directory permissions ensured once instead of every frame. Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
"""Snapshot write policy for the display preview mirror.
|
|
|
|
The display service mirrors frames to /tmp/led_matrix_preview.png, which
|
|
serves two consumers with different needs:
|
|
|
|
- The web UI's live preview (SSE reader in web_interface/app.py) wants
|
|
fresh frames — but only while a browser is actually watching.
|
|
- The health check (web_interface/blueprints/api_v3.py, hardware status)
|
|
uses the file's AGE as a liveness proxy: age >= 60s reads as degraded.
|
|
|
|
PNG-encoding every frame at 5 fps forever — identical frames, no viewers —
|
|
was one of the biggest fixed CPU costs on the Pi. This module is the pure
|
|
decision logic (extracted so it's unit-testable off-Pi; display_manager
|
|
imports rgbmatrix unconditionally and can't be):
|
|
|
|
WRITE — encode + atomically replace the snapshot file
|
|
TOUCH — os.utime only: keeps the health-check mtime fresh and lets
|
|
the SSE reader (mtime-gated) resend at a low rate, without
|
|
paying for a PNG encode of an unchanged frame
|
|
SKIP — do nothing
|
|
|
|
Policy:
|
|
- With a fresh viewer marker: changed frames write at up to 1/VIEWER_INTERVAL.
|
|
- Without viewers: changed frames still write at 1/IDLE_INTERVAL so the
|
|
preview page shows something recent on open.
|
|
- Unchanged frames are never re-encoded; the mtime is touched every
|
|
TOUCH_INTERVAL so the health check (60s threshold) never degrades.
|
|
|
|
If any constant here changes, re-check the health threshold in
|
|
api_v3.py (get_hardware_status) — TOUCH_INTERVAL must stay well under it.
|
|
"""
|
|
|
|
from enum import Enum
|
|
|
|
# Snapshot cadence with a browser preview open (seconds).
|
|
VIEWER_INTERVAL = 0.2
|
|
# Snapshot cadence with no viewers — cheap freshness for page-open (seconds).
|
|
IDLE_INTERVAL = 30.0
|
|
# Max age of the last write/touch before bumping mtime for the health
|
|
# check. MUST stay well under api_v3's 60s degraded threshold.
|
|
TOUCH_INTERVAL = 20.0
|
|
# A viewer marker older than this no longer counts as a live viewer.
|
|
VIEWER_MARKER_FRESH_SEC = 5.0
|
|
|
|
|
|
class SnapshotAction(Enum):
|
|
WRITE = "write"
|
|
TOUCH = "touch"
|
|
SKIP = "skip"
|
|
|
|
|
|
def decide(now: float, last_write_ts: float, last_touch_ts: float,
|
|
viewer_fresh: bool, frame_changed: bool) -> SnapshotAction:
|
|
"""Decide what to do with the current frame.
|
|
|
|
Args:
|
|
now: current monotonic-ish timestamp (same clock as the ts args)
|
|
last_write_ts: when a frame was last actually encoded+written
|
|
last_touch_ts: when the file mtime was last bumped (write or touch)
|
|
viewer_fresh: a browser preview is currently watching
|
|
frame_changed: the frame differs from the last WRITTEN frame
|
|
"""
|
|
interval = VIEWER_INTERVAL if viewer_fresh else IDLE_INTERVAL
|
|
if frame_changed and (now - last_write_ts) >= interval:
|
|
return SnapshotAction.WRITE
|
|
if (now - max(last_write_ts, last_touch_ts)) >= TOUCH_INTERVAL:
|
|
return SnapshotAction.TOUCH
|
|
return SnapshotAction.SKIP
|