display_control.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. """Display brightness and screen blanking control for SpoolBuddy kiosk.
  2. Brightness: controlled via sysfs /sys/class/backlight/*/brightness (DSI displays only).
  3. Blanking: uses vcgencmd display_power (RPi firmware-level, works for both HDMI and DSI
  4. without needing Wayland socket access).
  5. """
  6. import logging
  7. import shutil
  8. import subprocess
  9. import time
  10. from pathlib import Path
  11. logger = logging.getLogger(__name__)
  12. BACKLIGHT_BASE = Path("/sys/class/backlight")
  13. class DisplayControl:
  14. def __init__(self):
  15. self._backlight_path = self._find_backlight()
  16. self._max_brightness = self._read_max_brightness()
  17. self._has_vcgencmd = shutil.which("vcgencmd") is not None
  18. self._blank_timeout = 0 # seconds, 0 = disabled
  19. self._last_activity = time.monotonic()
  20. self._blanked = False
  21. if self._backlight_path:
  22. logger.info("Backlight found: %s (max=%d)", self._backlight_path, self._max_brightness)
  23. else:
  24. logger.info("No DSI backlight found, brightness control unavailable")
  25. if self._has_vcgencmd:
  26. logger.info("vcgencmd available, screen blanking enabled")
  27. else:
  28. logger.warning("vcgencmd not found, screen blanking unavailable")
  29. def _find_backlight(self) -> Path | None:
  30. if not BACKLIGHT_BASE.exists():
  31. return None
  32. for entry in BACKLIGHT_BASE.iterdir():
  33. brightness_file = entry / "brightness"
  34. if brightness_file.exists():
  35. return entry
  36. return None
  37. def _read_max_brightness(self) -> int:
  38. if not self._backlight_path:
  39. return 100
  40. try:
  41. return int((self._backlight_path / "max_brightness").read_text().strip())
  42. except Exception:
  43. return 255
  44. @property
  45. def has_backlight(self) -> bool:
  46. return self._backlight_path is not None
  47. def set_brightness(self, pct: int):
  48. """Set backlight brightness (0-100%). No-op if no backlight."""
  49. if not self._backlight_path:
  50. return
  51. pct = max(0, min(100, pct))
  52. value = round(self._max_brightness * pct / 100)
  53. try:
  54. (self._backlight_path / "brightness").write_text(str(value))
  55. logger.debug("Brightness set to %d%% (%d/%d)", pct, value, self._max_brightness)
  56. except PermissionError:
  57. logger.warning(
  58. "Permission denied writing to %s/brightness. Ensure spoolbuddy user is in the 'video' group.",
  59. self._backlight_path,
  60. )
  61. except Exception as e:
  62. logger.warning("Failed to set brightness: %s", e)
  63. def set_blank_timeout(self, seconds: int):
  64. """Set screen blank timeout in seconds. 0 = disabled."""
  65. self._blank_timeout = max(0, seconds)
  66. def wake(self):
  67. """Wake screen on activity (NFC tag, scale weight change)."""
  68. self._last_activity = time.monotonic()
  69. if self._blanked:
  70. self._unblank()
  71. def tick(self):
  72. """Called periodically from heartbeat loop. Blanks screen if idle."""
  73. if self._blank_timeout <= 0:
  74. if self._blanked:
  75. self._unblank()
  76. return
  77. idle = time.monotonic() - self._last_activity
  78. if not self._blanked and idle >= self._blank_timeout:
  79. self._blank()
  80. def _blank(self):
  81. if not self._has_vcgencmd:
  82. return
  83. try:
  84. subprocess.run(["vcgencmd", "display_power", "0"], capture_output=True, timeout=5)
  85. self._blanked = True
  86. logger.debug("Screen blanked via vcgencmd")
  87. except Exception as e:
  88. logger.warning("Failed to blank screen: %s", e)
  89. def _unblank(self):
  90. if not self._has_vcgencmd:
  91. return
  92. try:
  93. subprocess.run(["vcgencmd", "display_power", "1"], capture_output=True, timeout=5)
  94. self._blanked = False
  95. logger.debug("Screen unblanked via vcgencmd")
  96. except Exception as e:
  97. logger.warning("Failed to unblank screen: %s", e)