display_control.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. """Display brightness and screen blanking control for SpoolBuddy kiosk."""
  2. import logging
  3. import subprocess
  4. import time
  5. from pathlib import Path
  6. logger = logging.getLogger(__name__)
  7. BACKLIGHT_BASE = Path("/sys/class/backlight")
  8. class DisplayControl:
  9. def __init__(self):
  10. self._backlight_path = self._find_backlight()
  11. self._max_brightness = self._read_max_brightness()
  12. self._blank_timeout = 0 # seconds, 0 = disabled
  13. self._last_activity = time.monotonic()
  14. self._blanked = False
  15. if self._backlight_path:
  16. logger.info("Backlight found: %s (max=%d)", self._backlight_path, self._max_brightness)
  17. else:
  18. logger.info("No DSI backlight found, brightness control unavailable")
  19. def _find_backlight(self) -> Path | None:
  20. if not BACKLIGHT_BASE.exists():
  21. return None
  22. for entry in BACKLIGHT_BASE.iterdir():
  23. brightness_file = entry / "brightness"
  24. if brightness_file.exists():
  25. return entry
  26. return None
  27. def _read_max_brightness(self) -> int:
  28. if not self._backlight_path:
  29. return 100
  30. try:
  31. return int((self._backlight_path / "max_brightness").read_text().strip())
  32. except Exception:
  33. return 255
  34. @property
  35. def has_backlight(self) -> bool:
  36. return self._backlight_path is not None
  37. def set_brightness(self, pct: int):
  38. """Set backlight brightness (0-100%). No-op if no backlight."""
  39. if not self._backlight_path:
  40. return
  41. pct = max(0, min(100, pct))
  42. value = round(self._max_brightness * pct / 100)
  43. try:
  44. (self._backlight_path / "brightness").write_text(str(value))
  45. logger.debug("Brightness set to %d%% (%d/%d)", pct, value, self._max_brightness)
  46. except Exception as e:
  47. logger.warning("Failed to set brightness: %s", e)
  48. def set_blank_timeout(self, seconds: int):
  49. """Set screen blank timeout in seconds. 0 = disabled."""
  50. self._blank_timeout = max(0, seconds)
  51. def wake(self):
  52. """Wake screen on activity (NFC tag, scale weight change)."""
  53. self._last_activity = time.monotonic()
  54. if self._blanked:
  55. self._unblank()
  56. def tick(self):
  57. """Called periodically from heartbeat loop. Blanks screen if idle."""
  58. if self._blank_timeout <= 0:
  59. if self._blanked:
  60. self._unblank()
  61. return
  62. idle = time.monotonic() - self._last_activity
  63. if not self._blanked and idle >= self._blank_timeout:
  64. self._blank()
  65. def _blank(self):
  66. try:
  67. subprocess.run(["wlopm", "--off", "*"], capture_output=True, timeout=5)
  68. self._blanked = True
  69. logger.debug("Screen blanked after idle timeout")
  70. except Exception as e:
  71. logger.warning("Failed to blank screen: %s", e)
  72. def _unblank(self):
  73. try:
  74. subprocess.run(["wlopm", "--on", "*"], capture_output=True, timeout=5)
  75. self._blanked = False
  76. logger.debug("Screen unblanked")
  77. except Exception as e:
  78. logger.warning("Failed to unblank screen: %s", e)