1
0

screen_controller.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """Screen (LCD backlight) controller via Linux sysfs.
  2. Mirrors the sysfs approach used in dune-weaver-touch/backend.py so the main
  3. FastAPI backend can control the attached touchscreen independently. On dev
  4. machines without /sys/class/backlight the controller reports available=False
  5. and all commands no-op gracefully.
  6. """
  7. import logging
  8. import shutil
  9. import subprocess
  10. from pathlib import Path
  11. logger = logging.getLogger(__name__)
  12. SUDO = shutil.which("sudo") or "/usr/bin/sudo"
  13. SH = shutil.which("sh") or "/bin/sh"
  14. class ScreenController:
  15. def __init__(self):
  16. self.brightness_path: str = ""
  17. self.max_brightness: int = 255
  18. self._current_brightness: int = 0
  19. self._power_on: bool = True
  20. self.available: bool = False
  21. self._detect_backlight()
  22. # ── Detection ──────────────────────────────────────────────
  23. def _detect_backlight(self):
  24. """Auto-detect the sysfs backlight device, path, and max brightness."""
  25. backlight_base = Path("/sys/class/backlight")
  26. if not backlight_base.exists():
  27. logger.info("No /sys/class/backlight found — screen control unavailable")
  28. return
  29. try:
  30. devices = [d.name for d in backlight_base.iterdir() if d.is_dir()]
  31. except Exception as e:
  32. logger.warning(f"Failed to list backlight devices: {e}")
  33. return
  34. if not devices:
  35. logger.info("No backlight devices found")
  36. return
  37. device = devices[0]
  38. self.brightness_path = f"/sys/class/backlight/{device}/brightness"
  39. logger.info(f"Auto-detected backlight device: {device}")
  40. # Read max_brightness
  41. max_path = f"/sys/class/backlight/{device}/max_brightness"
  42. try:
  43. self.max_brightness = int(Path(max_path).read_text().strip())
  44. logger.info(f"Max brightness: {self.max_brightness}")
  45. except Exception as e:
  46. self.max_brightness = 255
  47. logger.warning(f"Failed to read max_brightness, defaulting to 255: {e}")
  48. # Read current brightness
  49. try:
  50. self._current_brightness = int(Path(self.brightness_path).read_text().strip())
  51. self._power_on = self._current_brightness > 0
  52. logger.info(f"Current brightness: {self._current_brightness}/{self.max_brightness}")
  53. except Exception as e:
  54. logger.warning(f"Failed to read current brightness: {e}")
  55. self.available = True
  56. # ── Public API ─────────────────────────────────────────────
  57. def set_brightness(self, value: int) -> dict:
  58. """Set backlight brightness (0 to max_brightness)."""
  59. if not self.available:
  60. return {"success": False, "message": "Screen control not available"}
  61. value = max(0, min(value, self.max_brightness))
  62. try:
  63. subprocess.run(
  64. [SUDO, SH, "-c", f"echo {value} > {self.brightness_path}"],
  65. check=True, timeout=5
  66. )
  67. self._current_brightness = value
  68. if value > 0:
  69. self._power_on = True
  70. return {"success": True, "brightness": value}
  71. except Exception as e:
  72. logger.error(f"Failed to set brightness: {e}")
  73. return {"success": False, "message": str(e)}
  74. def set_power(self, on: bool) -> dict:
  75. """Turn screen on or off via framebuffer blank + backlight."""
  76. if not self.available:
  77. return {"success": False, "message": "Screen control not available"}
  78. try:
  79. if on:
  80. # Restore brightness + unblank framebuffer
  81. restore = self._current_brightness if self._current_brightness > 0 else self.max_brightness
  82. subprocess.run(
  83. [SUDO, SH, "-c",
  84. f"echo 0 > /sys/class/graphics/fb0/blank && echo {restore} > {self.brightness_path}"],
  85. check=True, timeout=5
  86. )
  87. self._current_brightness = restore
  88. self._power_on = True
  89. else:
  90. # Zero brightness + blank framebuffer
  91. subprocess.run(
  92. [SUDO, SH, "-c",
  93. f"echo 0 > {self.brightness_path} && echo 1 > /sys/class/graphics/fb0/blank"],
  94. check=True, timeout=5
  95. )
  96. self._power_on = False
  97. return {"success": True, "power_on": self._power_on}
  98. except Exception as e:
  99. logger.error(f"Failed to set screen power: {e}")
  100. return {"success": False, "message": str(e)}
  101. def get_status(self) -> dict:
  102. """Return current screen state."""
  103. return {
  104. "available": self.available,
  105. "power_on": self._power_on,
  106. "brightness": self._current_brightness,
  107. "max_brightness": self.max_brightness,
  108. }