mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
1. Device tab — repo URL unlinked Replaced the <a> tag with a <span> so the GitHub URL is displayed as plain text (no clickable link in kiosk mode). 2. Display controls — moved to frontend Root cause: The daemon runs as a system user without Wayland access, and the kiosk uses an HDMI display (no sysfs backlight). Neither wlopm, vcgencmd, nor sysfs writes can work in this setup. Fix — frontend-controlled display in SpoolBuddyLayout.tsx: - Brightness: CSS filter: brightness(X) applied to the layout root. Immediate visual dimming that works on any display type. - Screen blanking: Full-screen black overlay (z-[9999]) shown after the configured inactivity timeout. Touch anywhere to wake. - Activity tracking: Resets on pointerdown/keydown events AND on WebSocket-driven NFC/scale changes (weight update, tag scan). - Queries device data every 15s to read the current brightness and blank timeout values. Daemon display_control.py — Simplified: - Removed vcgencmd/wlopm/subprocess usage entirely - _blank/_unblank are now state-tracking no-ops (log only) - DSI backlight brightness via sysfs is preserved as a bonus for DSI displays - Updated docstring to explain the architecture 3. Scale tab — redesigned with step indicator Idle state: Same as before — weight card with tare/calibrate buttons. Calibration wizard: Two-column layout: - Left column (~64px): Vertical step indicator with numbered circles, checkmark for completed steps, and a connecting line - Right column: Live weight bar (with stable/settling indicator), step-specific content (instructions or numpad), and action buttons pinned to bottom via mt-auto The numpad buttons are min-h-[52px] with text-lg — large enough for touch on 1024x600. The weight info card is hidden during the weight entry step to maximize vertical space.
96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""Display brightness and screen blanking control for SpoolBuddy kiosk.
|
|
|
|
Brightness: DSI backlights are controlled via sysfs /sys/class/backlight/*/brightness.
|
|
HDMI brightness is handled by the frontend via CSS filter.
|
|
Blanking: Handled entirely by the frontend (CSS black overlay with touch-to-wake).
|
|
The daemon tracks idle state but does not control the physical display.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
BACKLIGHT_BASE = Path("/sys/class/backlight")
|
|
|
|
|
|
class DisplayControl:
|
|
def __init__(self):
|
|
self._backlight_path = self._find_backlight()
|
|
self._max_brightness = self._read_max_brightness()
|
|
self._blank_timeout = 0 # seconds, 0 = disabled
|
|
self._last_activity = time.monotonic()
|
|
self._blanked = False
|
|
|
|
if self._backlight_path:
|
|
logger.info("Backlight found: %s (max=%d)", self._backlight_path, self._max_brightness)
|
|
else:
|
|
logger.info("No DSI backlight found, brightness control via frontend CSS")
|
|
|
|
def _find_backlight(self) -> Path | None:
|
|
if not BACKLIGHT_BASE.exists():
|
|
return None
|
|
for entry in BACKLIGHT_BASE.iterdir():
|
|
brightness_file = entry / "brightness"
|
|
if brightness_file.exists():
|
|
return entry
|
|
return None
|
|
|
|
def _read_max_brightness(self) -> int:
|
|
if not self._backlight_path:
|
|
return 100
|
|
try:
|
|
return int((self._backlight_path / "max_brightness").read_text().strip())
|
|
except Exception:
|
|
return 255
|
|
|
|
@property
|
|
def has_backlight(self) -> bool:
|
|
return self._backlight_path is not None
|
|
|
|
def set_brightness(self, pct: int):
|
|
"""Set backlight brightness (0-100%). No-op if no backlight."""
|
|
if not self._backlight_path:
|
|
return
|
|
pct = max(0, min(100, pct))
|
|
value = round(self._max_brightness * pct / 100)
|
|
try:
|
|
(self._backlight_path / "brightness").write_text(str(value))
|
|
logger.debug("Brightness set to %d%% (%d/%d)", pct, value, self._max_brightness)
|
|
except PermissionError:
|
|
logger.warning(
|
|
"Permission denied writing to %s/brightness. Ensure spoolbuddy user is in the 'video' group.",
|
|
self._backlight_path,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Failed to set brightness: %s", e)
|
|
|
|
def set_blank_timeout(self, seconds: int):
|
|
"""Set screen blank timeout in seconds. 0 = disabled."""
|
|
self._blank_timeout = max(0, seconds)
|
|
|
|
def wake(self):
|
|
"""Wake screen on activity (NFC tag, scale weight change)."""
|
|
self._last_activity = time.monotonic()
|
|
if self._blanked:
|
|
self._unblank()
|
|
|
|
def tick(self):
|
|
"""Called periodically from heartbeat loop. Blanks screen if idle."""
|
|
if self._blank_timeout <= 0:
|
|
if self._blanked:
|
|
self._unblank()
|
|
return
|
|
idle = time.monotonic() - self._last_activity
|
|
if not self._blanked and idle >= self._blank_timeout:
|
|
self._blank()
|
|
|
|
def _blank(self):
|
|
self._blanked = True
|
|
logger.debug("Screen idle timeout reached (frontend handles blanking)")
|
|
|
|
def _unblank(self):
|
|
self._blanked = False
|
|
logger.debug("Activity detected (frontend handles unblanking)")
|