screen_controller.py 5.0 KB

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