This commit is contained in:
maziggy
2026-04-12 11:12:05 +02:00
parent a25f200529
commit 749257e97a
2 changed files with 58 additions and 92 deletions
+28 -78
View File
@@ -3,22 +3,22 @@
Brightness: DSI backlights are controlled via sysfs /sys/class/backlight/*/brightness.
HDMI brightness is handled by the frontend via CSS filter.
Blanking: swayidle is the sole authority on screen blanking (idle timeout →
wlopm --off, touch → wlopm --on). The daemon only *wakes* the
display via wlopm --on when NFC/scale activity is detected — it
never blanks. wlopm --on is idempotent so both paths coexist
safely.
wlopm --off, touch → wlopm --on). The daemon wakes the display by
writing to a FIFO that the idle watchdog monitors — the watchdog
runs inside the Wayland session and calls wlopm --on on behalf of
the daemon.
"""
import logging
import os
import shutil
import subprocess
import stat
import time
from pathlib import Path
logger = logging.getLogger(__name__)
BACKLIGHT_BASE = Path("/sys/class/backlight")
WAKE_FIFO = Path("/tmp/spoolbuddy-wake")
class DisplayControl:
@@ -28,20 +28,12 @@ class DisplayControl:
self._blank_timeout = 0 # seconds, 0 = disabled
self._last_activity = time.monotonic()
self._blanked = False
self._wlopm_path = shutil.which("wlopm")
self._wayland_env: dict[str, str] | None = None
self._output = os.environ.get("SPOOLBUDDY_DISPLAY_OUTPUT", "HDMI-A-1")
if self._backlight_path:
logger.info("Backlight found: %s (max=%d)", self._backlight_path, self._max_brightness)
else:
logger.info("No DSI backlight found, brightness control via frontend CSS")
if self._wlopm_path:
logger.info("wlopm found at %s, HDMI wake/blank enabled", self._wlopm_path)
else:
logger.info("wlopm not found, HDMI wake/blank disabled")
def _find_backlight(self) -> Path | None:
if not BACKLIGHT_BASE.exists():
return None
@@ -87,14 +79,14 @@ class DisplayControl:
def wake(self):
"""Wake screen on activity (NFC tag, scale weight change).
Always calls wlopm --on regardless of the daemon's internal blanked
state, because swayidle may have blanked the screen independently and
the daemon has no way to know. wlopm --on is idempotent so calling it
while the screen is already on is harmless.
Writes to /tmp/spoolbuddy-wake FIFO which the idle watchdog
(spoolbuddy-idle.sh) monitors inside the Wayland session. The
watchdog calls wlopm --on on our behalf. No-op if the FIFO
doesn't exist (kiosk not running or blanking disabled without FIFO).
"""
self._last_activity = time.monotonic()
self._blanked = False
self._wlopm(on=True)
self._signal_wake()
def tick(self):
"""Called periodically from heartbeat loop. Tracks idle state internally."""
@@ -106,64 +98,22 @@ class DisplayControl:
self._blanked = True
logger.debug("Screen idle timeout reached (swayidle manages blanking)")
_WAYLAND_ENV_FILE = Path("/tmp/spoolbuddy-wayland-env")
def _discover_wayland_env(self) -> dict[str, str] | None:
"""Discover WAYLAND_DISPLAY and XDG_RUNTIME_DIR for the kiosk session.
The daemon runs as a systemd service outside the Wayland session, so
these variables aren't inherited. The kiosk idle watchdog
(spoolbuddy-idle.sh) writes them to /tmp/spoolbuddy-wayland-env on
startup — read that file.
"""
if not self._WAYLAND_ENV_FILE.exists():
return None
try:
env: dict[str, str] = {}
for line in self._WAYLAND_ENV_FILE.read_text().strip().splitlines():
if "=" in line:
key, value = line.split("=", 1)
env[key] = value
wayland_display = env.get("WAYLAND_DISPLAY", "")
xdg_runtime_dir = env.get("XDG_RUNTIME_DIR", "")
if wayland_display and xdg_runtime_dir:
return {"WAYLAND_DISPLAY": wayland_display, "XDG_RUNTIME_DIR": xdg_runtime_dir}
except Exception as e:
logger.warning("Failed to read %s: %s", self._WAYLAND_ENV_FILE, e)
return None
def _wlopm(self, on: bool) -> None:
"""Toggle HDMI output via wlopm. No-op if wlopm is unavailable."""
if not self._wlopm_path:
logger.warning("wlopm not available, cannot control HDMI")
def _signal_wake(self) -> None:
"""Write to the wake FIFO to request display power-on."""
if not WAKE_FIFO.exists():
return
# Retry discovery each call until the Wayland socket appears — labwc
# may start after the daemon on boot.
if self._wayland_env is None:
self._wayland_env = self._discover_wayland_env()
if self._wayland_env is None:
logger.warning("No Wayland socket found in /run/user/ (uid=%d), cannot control HDMI", os.getuid())
return
logger.info("Wayland session discovered: %s", self._wayland_env.get("WAYLAND_DISPLAY"))
flag = "--on" if on else "--off"
try:
env = {**os.environ, **self._wayland_env}
result = subprocess.run(
[self._wlopm_path, flag, self._output],
env=env,
timeout=5,
capture_output=True,
text=True,
)
if result.returncode != 0:
logger.warning(
"wlopm %s %s exit=%d: %s",
flag,
self._output,
result.returncode,
(result.stderr or result.stdout).strip(),
)
else:
logger.info("wlopm %s %s OK", flag, self._output)
except Exception as e:
logger.warning("wlopm %s %s failed: %s", flag, self._output, e)
# Verify it's actually a FIFO, not a regular file
if not stat.S_ISFIFO(WAKE_FIFO.stat().st_mode):
return
# Open non-blocking so we don't hang if no reader is attached
fd = os.open(str(WAKE_FIFO), os.O_WRONLY | os.O_NONBLOCK)
try:
os.write(fd, b"wake\n")
logger.info("Wake signal sent via FIFO")
finally:
os.close(fd)
except OSError as e:
# ENXIO = no reader on the FIFO (idle script not running) — expected
if e.errno != 6: # ENXIO
logger.debug("Wake FIFO write failed: %s", e)
+30 -14
View File
@@ -85,22 +85,38 @@ if [ -n "$BACKEND_URL" ] && [ -n "$API_KEY" ] && [ -n "$DEVICE_ID" ]; then
fi
fi
# Write Wayland env to a known file so the SpoolBuddy daemon (which runs as a
# systemd service outside the compositor) can discover the socket and call wlopm
# to wake the display on NFC/scale events.
WAYLAND_ENV_FILE="/tmp/spoolbuddy-wayland-env"
printf 'WAYLAND_DISPLAY=%s\nXDG_RUNTIME_DIR=%s\n' \
"${WAYLAND_DISPLAY:-}" "${XDG_RUNTIME_DIR:-}" > "$WAYLAND_ENV_FILE"
chmod 644 "$WAYLAND_ENV_FILE"
echo "wrote Wayland env to $WAYLAND_ENV_FILE"
# FIFO for the SpoolBuddy daemon to request display wake from outside the
# Wayland session (NFC tag scan, scale weight change). The daemon writes
# "wake\n" to this pipe; the monitor loop below calls wlopm --on.
WAKE_FIFO="/tmp/spoolbuddy-wake"
rm -f "$WAKE_FIFO"
mkfifo -m 622 "$WAKE_FIFO"
echo "wake FIFO created at $WAKE_FIFO"
if [ "$TIMEOUT" -le 0 ]; then
# Blanking explicitly disabled — don't launch swayidle at all.
echo "timeout<=0, sleeping forever"
exec sleep infinity
# Blanking explicitly disabled — just monitor the wake FIFO so NFC/scale
# wake still works even without swayidle.
echo "timeout<=0, monitoring wake FIFO only (no swayidle)"
while read -r _ < "$WAKE_FIFO"; do
wlopm --on "$OUTPUT" 2>/dev/null || true
done
exit 0
fi
echo "execing swayidle with timeout=$TIMEOUT output=$OUTPUT"
exec swayidle -w \
echo "starting swayidle with timeout=$TIMEOUT output=$OUTPUT"
swayidle -w \
timeout "$TIMEOUT" "wlopm --off $OUTPUT" \
resume "wlopm --on $OUTPUT"
resume "wlopm --on $OUTPUT" &
SWAYIDLE_PID=$!
# Monitor wake FIFO — when the daemon writes to it, turn the display on.
while read -r _ < "$WAKE_FIFO"; do
wlopm --on "$OUTPUT" 2>/dev/null || true
done &
FIFO_PID=$!
# If either process exits, clean up and exit.
wait -n "$SWAYIDLE_PID" "$FIFO_PID" 2>/dev/null
echo "child exited, cleaning up"
kill "$SWAYIDLE_PID" "$FIFO_PID" 2>/dev/null || true
rm -f "$WAKE_FIFO"