|
|
@@ -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)
|