spoolbuddy-idle.sh 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #!/bin/bash
  2. # SpoolBuddy kiosk display idle watchdog.
  3. #
  4. # Powers the HDMI output off via wlopm after the configured inactivity
  5. # timeout, driven by swayidle inside the labwc Wayland session. The timeout
  6. # value is fetched once from the Bambuddy backend on startup so it matches
  7. # whatever the user picked in SpoolBuddy Settings → Display.
  8. #
  9. # Changes made in the UI are applied live: the daemon writes a
  10. # "reload-timeout N" line to /tmp/spoolbuddy-wake whenever it sees a new
  11. # value over the heartbeat, and the FIFO loop below kills the current
  12. # swayidle and starts a fresh one with the new timeout. No kiosk restart
  13. # is required.
  14. #
  15. # Runs in labwc's autostart file as the kiosk user — needs access to
  16. # WAYLAND_DISPLAY, which it inherits from the parent labwc process.
  17. set -u
  18. LOG_FILE="${SPOOLBUDDY_IDLE_LOG:-$HOME/.cache/spoolbuddy-idle.log}"
  19. mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
  20. exec >>"$LOG_FILE" 2>&1
  21. echo "=== $(date -Is) spoolbuddy-idle starting (pid=$$) ==="
  22. echo "WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-<unset>}"
  23. echo "XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-<unset>}"
  24. echo "PATH=$PATH"
  25. DEFAULT_TIMEOUT=300
  26. ENV_FILE="${SPOOLBUDDY_ENV_FILE:-/opt/bambuddy/spoolbuddy/.env}"
  27. OUTPUT="${SPOOLBUDDY_DISPLAY_OUTPUT:-HDMI-A-1}"
  28. # Wait for labwc to actually bring up its Wayland socket. Autostart fires
  29. # before labwc finishes exporting WAYLAND_DISPLAY on some systems, which
  30. # makes swayidle exit immediately.
  31. if [ -z "${WAYLAND_DISPLAY:-}" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then
  32. for _ in $(seq 1 20); do
  33. sock=$(ls -1 "$XDG_RUNTIME_DIR"/wayland-* 2>/dev/null | grep -v '\.lock$' | head -n1 || true)
  34. if [ -n "$sock" ]; then
  35. WAYLAND_DISPLAY="$(basename "$sock")"
  36. export WAYLAND_DISPLAY
  37. echo "auto-detected WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
  38. break
  39. fi
  40. sleep 0.5
  41. done
  42. fi
  43. if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
  44. XDG_RUNTIME_DIR="/run/user/$(id -u)"
  45. export XDG_RUNTIME_DIR
  46. echo "defaulted XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
  47. fi
  48. if [ -r "$ENV_FILE" ]; then
  49. set -a
  50. # shellcheck disable=SC1090
  51. . "$ENV_FILE"
  52. set +a
  53. fi
  54. BACKEND_URL="${SPOOLBUDDY_BACKEND_URL:-}"
  55. API_KEY="${SPOOLBUDDY_API_KEY:-}"
  56. DEVICE_ID="${SPOOLBUDDY_DEVICE_ID:-}"
  57. # Derive device_id from the first non-loopback NIC MAC address, the same
  58. # algorithm daemon/config.py uses so installs without an explicit
  59. # SPOOLBUDDY_DEVICE_ID still match.
  60. if [ -z "$DEVICE_ID" ]; then
  61. for iface in $(ls -1 /sys/class/net/ 2>/dev/null | sort); do
  62. [ "$iface" = "lo" ] && continue
  63. addr_file="/sys/class/net/$iface/address"
  64. [ -r "$addr_file" ] || continue
  65. mac=$(tr -d ':' < "$addr_file" 2>/dev/null)
  66. if [ -n "$mac" ] && [ "$mac" != "000000000000" ]; then
  67. DEVICE_ID="sb-$mac"
  68. break
  69. fi
  70. done
  71. fi
  72. TIMEOUT="$DEFAULT_TIMEOUT"
  73. if [ -n "$BACKEND_URL" ] && [ -n "$API_KEY" ] && [ -n "$DEVICE_ID" ]; then
  74. response=$(curl -fsS --max-time 10 \
  75. -H "Authorization: Bearer $API_KEY" \
  76. "$BACKEND_URL/api/v1/spoolbuddy/devices/$DEVICE_ID/display" 2>/dev/null || true)
  77. if [ -n "$response" ]; then
  78. fetched=$(printf '%s' "$response" | jq -r '.blank_timeout // empty' 2>/dev/null || true)
  79. if [ -n "$fetched" ] && [ "$fetched" -eq "$fetched" ] 2>/dev/null; then
  80. TIMEOUT="$fetched"
  81. fi
  82. fi
  83. fi
  84. # FIFO for the SpoolBuddy daemon to talk to this watchdog from outside the
  85. # Wayland session. Two messages are understood:
  86. # wake — turn the display on (NFC tag scan, scale weight change)
  87. # reload-timeout N — kill swayidle and restart it with timeout=N
  88. WAKE_FIFO="/tmp/spoolbuddy-wake"
  89. rm -f "$WAKE_FIFO"
  90. mkfifo -m 622 "$WAKE_FIFO"
  91. echo "wake FIFO created at $WAKE_FIFO"
  92. SWAYIDLE_PID=""
  93. REBLANK_PID=""
  94. start_swayidle() {
  95. [ "$TIMEOUT" -gt 0 ] || return 0
  96. swayidle -w \
  97. timeout "$TIMEOUT" "wlopm --off $OUTPUT" \
  98. resume "wlopm --on $OUTPUT" &
  99. SWAYIDLE_PID=$!
  100. echo "swayidle started (pid=$SWAYIDLE_PID, timeout=$TIMEOUT, output=$OUTPUT)"
  101. }
  102. stop_swayidle() {
  103. if [ -n "$SWAYIDLE_PID" ]; then
  104. kill "$SWAYIDLE_PID" 2>/dev/null || true
  105. wait "$SWAYIDLE_PID" 2>/dev/null || true
  106. SWAYIDLE_PID=""
  107. fi
  108. if [ -n "$REBLANK_PID" ]; then
  109. kill "$REBLANK_PID" 2>/dev/null || true
  110. REBLANK_PID=""
  111. fi
  112. }
  113. cleanup() {
  114. stop_swayidle
  115. rm -f "$WAKE_FIFO"
  116. exit 0
  117. }
  118. trap cleanup TERM INT HUP
  119. start_swayidle
  120. # Open the FIFO read+write so EOF never arrives even when the daemon
  121. # (the writer) momentarily disconnects between messages — without this,
  122. # `read` would return immediately the first time the daemon closes its
  123. # write end and the loop would spin.
  124. exec 3<>"$WAKE_FIFO"
  125. while IFS= read -r line <&3; do
  126. case "$line" in
  127. wake)
  128. wlopm --on "$OUTPUT" 2>/dev/null || true
  129. # Cancel any pending re-blank timer, then start a new one
  130. # at the *current* timeout (swayidle doesn't know about
  131. # FIFO wakes so it won't re-blank on its own).
  132. [ -n "$REBLANK_PID" ] && kill "$REBLANK_PID" 2>/dev/null || true
  133. REBLANK_PID=""
  134. if [ "$TIMEOUT" -gt 0 ]; then
  135. (sleep "$TIMEOUT" && wlopm --off "$OUTPUT" 2>/dev/null) &
  136. REBLANK_PID=$!
  137. fi
  138. ;;
  139. reload-timeout\ *)
  140. new_timeout="${line#reload-timeout }"
  141. # Validate: must be a non-negative integer.
  142. if [ "$new_timeout" -eq "$new_timeout" ] 2>/dev/null && [ "$new_timeout" -ge 0 ]; then
  143. if [ "$new_timeout" != "$TIMEOUT" ]; then
  144. echo "reload-timeout: $TIMEOUT -> $new_timeout"
  145. stop_swayidle
  146. TIMEOUT="$new_timeout"
  147. start_swayidle
  148. # Bring the display back on so the user sees the
  149. # change took effect (a setting saved while the
  150. # screen was already blanked would otherwise look
  151. # ignored until the next touch).
  152. wlopm --on "$OUTPUT" 2>/dev/null || true
  153. fi
  154. else
  155. echo "ignoring invalid reload-timeout payload: $new_timeout"
  156. fi
  157. ;;
  158. '')
  159. : # ignore empty lines (e.g. opening the FIFO with no payload)
  160. ;;
  161. *)
  162. echo "unknown FIFO message: $line"
  163. ;;
  164. esac
  165. done
  166. cleanup