maziggy 1 month ago
parent
commit
749257e97a
2 changed files with 58 additions and 92 deletions
  1. 28 78
      spoolbuddy/daemon/display_control.py
  2. 30 14
      spoolbuddy/install/spoolbuddy-idle.sh

+ 28 - 78
spoolbuddy/daemon/display_control.py

@@ -3,22 +3,22 @@
 Brightness: DSI backlights are controlled via sysfs /sys/class/backlight/*/brightness.
 Brightness: DSI backlights are controlled via sysfs /sys/class/backlight/*/brightness.
             HDMI brightness is handled by the frontend via CSS filter.
             HDMI brightness is handled by the frontend via CSS filter.
 Blanking:   swayidle is the sole authority on screen blanking (idle timeout →
 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 logging
 import os
 import os
-import shutil
-import subprocess
+import stat
 import time
 import time
 from pathlib import Path
 from pathlib import Path
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 BACKLIGHT_BASE = Path("/sys/class/backlight")
 BACKLIGHT_BASE = Path("/sys/class/backlight")
+WAKE_FIFO = Path("/tmp/spoolbuddy-wake")
 
 
 
 
 class DisplayControl:
 class DisplayControl:
@@ -28,20 +28,12 @@ class DisplayControl:
         self._blank_timeout = 0  # seconds, 0 = disabled
         self._blank_timeout = 0  # seconds, 0 = disabled
         self._last_activity = time.monotonic()
         self._last_activity = time.monotonic()
         self._blanked = False
         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:
         if self._backlight_path:
             logger.info("Backlight found: %s (max=%d)", self._backlight_path, self._max_brightness)
             logger.info("Backlight found: %s (max=%d)", self._backlight_path, self._max_brightness)
         else:
         else:
             logger.info("No DSI backlight found, brightness control via frontend CSS")
             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:
     def _find_backlight(self) -> Path | None:
         if not BACKLIGHT_BASE.exists():
         if not BACKLIGHT_BASE.exists():
             return None
             return None
@@ -87,14 +79,14 @@ class DisplayControl:
     def wake(self):
     def wake(self):
         """Wake screen on activity (NFC tag, scale weight change).
         """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._last_activity = time.monotonic()
         self._blanked = False
         self._blanked = False
-        self._wlopm(on=True)
+        self._signal_wake()
 
 
     def tick(self):
     def tick(self):
         """Called periodically from heartbeat loop. Tracks idle state internally."""
         """Called periodically from heartbeat loop. Tracks idle state internally."""
@@ -106,64 +98,22 @@ class DisplayControl:
             self._blanked = True
             self._blanked = True
             logger.debug("Screen idle timeout reached (swayidle manages blanking)")
             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
             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:
         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
spoolbuddy/install/spoolbuddy-idle.sh

@@ -85,22 +85,38 @@ if [ -n "$BACKEND_URL" ] && [ -n "$API_KEY" ] && [ -n "$DEVICE_ID" ]; then
     fi
     fi
 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
 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
 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" \
     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"