mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 07:01:40 +02:00
Spoolbuddy
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.
This commit is contained in:
@@ -1,16 +1,30 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { SpoolBuddyTopBar } from './SpoolBuddyTopBar';
|
||||
import { SpoolBuddyBottomNav } from './SpoolBuddyBottomNav';
|
||||
import { SpoolBuddyStatusBar } from './SpoolBuddyStatusBar';
|
||||
import { useSpoolBuddyState } from '../../hooks/useSpoolBuddyState';
|
||||
import { spoolbuddyApi } from '../../api/client';
|
||||
import { VirtualKeyboard } from '../VirtualKeyboard';
|
||||
|
||||
export function SpoolBuddyLayout() {
|
||||
const [selectedPrinterId, setSelectedPrinterId] = useState<number | null>(null);
|
||||
const [alert, setAlert] = useState<{ type: 'warning' | 'error' | 'info'; message: string } | null>(null);
|
||||
const [blanked, setBlanked] = useState(false);
|
||||
const lastActivityRef = useRef(Date.now());
|
||||
const sbState = useSpoolBuddyState();
|
||||
|
||||
// Query device data for display settings (brightness + blank timeout)
|
||||
const { data: devices = [] } = useQuery({
|
||||
queryKey: ['spoolbuddy-devices'],
|
||||
queryFn: () => spoolbuddyApi.getDevices(),
|
||||
refetchInterval: 15000,
|
||||
});
|
||||
const device = devices[0];
|
||||
const brightness = device?.display_brightness ?? 100;
|
||||
const blankTimeout = device?.display_blank_timeout ?? 0;
|
||||
|
||||
// Force dark theme on mount, restore on unmount
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
@@ -30,22 +44,84 @@ export function SpoolBuddyLayout() {
|
||||
}
|
||||
}, [sbState.deviceOnline]);
|
||||
|
||||
// Track user activity for screen blank
|
||||
const resetActivity = useCallback(() => {
|
||||
lastActivityRef.current = Date.now();
|
||||
setBlanked(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('pointerdown', resetActivity);
|
||||
window.addEventListener('keydown', resetActivity);
|
||||
return () => {
|
||||
window.removeEventListener('pointerdown', resetActivity);
|
||||
window.removeEventListener('keydown', resetActivity);
|
||||
};
|
||||
}, [resetActivity]);
|
||||
|
||||
// Reset on NFC/scale activity (WebSocket events)
|
||||
const prevWeightRef = useRef(sbState.weight);
|
||||
const prevSpoolRef = useRef(sbState.matchedSpool);
|
||||
const prevTagRef = useRef(sbState.unknownTagUid);
|
||||
useEffect(() => {
|
||||
if (
|
||||
sbState.weight !== prevWeightRef.current ||
|
||||
sbState.matchedSpool !== prevSpoolRef.current ||
|
||||
sbState.unknownTagUid !== prevTagRef.current
|
||||
) {
|
||||
prevWeightRef.current = sbState.weight;
|
||||
prevSpoolRef.current = sbState.matchedSpool;
|
||||
prevTagRef.current = sbState.unknownTagUid;
|
||||
lastActivityRef.current = Date.now();
|
||||
setBlanked(false);
|
||||
}
|
||||
}, [sbState.weight, sbState.matchedSpool, sbState.unknownTagUid]);
|
||||
|
||||
// Screen blank timer
|
||||
useEffect(() => {
|
||||
if (blankTimeout <= 0) return;
|
||||
const interval = setInterval(() => {
|
||||
if (Date.now() - lastActivityRef.current >= blankTimeout * 1000) {
|
||||
setBlanked(true);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [blankTimeout]);
|
||||
|
||||
// CSS brightness filter (software dimming for HDMI displays)
|
||||
const brightnessStyle = brightness < 100
|
||||
? { filter: `brightness(${brightness / 100})` } as const
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen bg-bambu-dark text-white flex flex-col overflow-hidden">
|
||||
<SpoolBuddyTopBar
|
||||
selectedPrinterId={selectedPrinterId}
|
||||
onPrinterChange={setSelectedPrinterId}
|
||||
deviceOnline={sbState.deviceOnline}
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
className="w-screen h-screen bg-bambu-dark text-white flex flex-col overflow-hidden"
|
||||
style={brightnessStyle}
|
||||
>
|
||||
<SpoolBuddyTopBar
|
||||
selectedPrinterId={selectedPrinterId}
|
||||
onPrinterChange={setSelectedPrinterId}
|
||||
deviceOnline={sbState.deviceOnline}
|
||||
/>
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<Outlet context={{ selectedPrinterId, setSelectedPrinterId, sbState, setAlert }} />
|
||||
</main>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<Outlet context={{ selectedPrinterId, setSelectedPrinterId, sbState, setAlert }} />
|
||||
</main>
|
||||
|
||||
<SpoolBuddyStatusBar alert={alert} />
|
||||
<SpoolBuddyBottomNav />
|
||||
<VirtualKeyboard />
|
||||
</div>
|
||||
<SpoolBuddyStatusBar alert={alert} />
|
||||
<SpoolBuddyBottomNav />
|
||||
<VirtualKeyboard />
|
||||
</div>
|
||||
|
||||
{/* Screen blank overlay — touch to wake */}
|
||||
{blanked && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black z-[9999]"
|
||||
onPointerDown={(e) => { e.stopPropagation(); resetActivity(); }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,14 +46,7 @@ function DeviceTab({ device }: { device: SpoolBuddyDevice }) {
|
||||
<img src="/img/spoolbuddy_logo_dark_small.png" alt="SpoolBuddy" className="h-7 w-auto" />
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 mb-1">Part of Bambuddy</p>
|
||||
<a
|
||||
href="https://github.com/maziggy/bambuddy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
github.com/maziggy/bambuddy
|
||||
</a>
|
||||
<span className="text-xs text-zinc-500">github.com/maziggy/bambuddy</span>
|
||||
</div>
|
||||
|
||||
{/* NFC Reader + Device Info side by side */}
|
||||
@@ -241,7 +234,7 @@ function DisplayTab({ device }: { device: SpoolBuddyDevice }) {
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-zinc-600 text-center">
|
||||
{t('spoolbuddy.settings.displayNote', 'Display settings are applied by the daemon on the next heartbeat cycle.')}
|
||||
{t('spoolbuddy.settings.displayNote', 'Brightness is applied as a software filter. Screen blank activates after inactivity — touch to wake.')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -249,6 +242,43 @@ function DisplayTab({ device }: { device: SpoolBuddyDevice }) {
|
||||
|
||||
// --- Scale Tab ---
|
||||
|
||||
function StepIndicator({ step }: { step: 'tare' | 'weight' }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center w-16 shrink-0 pt-1">
|
||||
{/* Step 1 circle */}
|
||||
<div className={`flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold ${
|
||||
step === 'tare'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-green-600/20 text-green-400'
|
||||
}`}>
|
||||
{step === 'weight' ? (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : '1'}
|
||||
</div>
|
||||
<span className={`text-[10px] mt-0.5 ${step === 'tare' ? 'text-green-400 font-medium' : 'text-green-400/60'}`}>
|
||||
Tare
|
||||
</span>
|
||||
|
||||
{/* Connector line */}
|
||||
<div className={`w-px h-5 my-1 ${step === 'weight' ? 'bg-green-600/40' : 'bg-zinc-700'}`} />
|
||||
|
||||
{/* Step 2 circle */}
|
||||
<div className={`flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold ${
|
||||
step === 'weight'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-zinc-700 text-zinc-500'
|
||||
}`}>
|
||||
2
|
||||
</div>
|
||||
<span className={`text-[10px] mt-0.5 ${step === 'weight' ? 'text-green-400 font-medium' : 'text-zinc-600'}`}>
|
||||
Weight
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScaleTab({ device, weight, weightStable, rawAdc }: {
|
||||
device: SpoolBuddyDevice;
|
||||
weight: number | null;
|
||||
@@ -316,10 +346,11 @@ function ScaleTab({ device, weight, weightStable, rawAdc }: {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Weight + info row — hidden during weight entry to maximize numpad space */}
|
||||
{calStep !== 'weight' && (
|
||||
// --- Idle state: weight card + buttons ---
|
||||
if (calStep === 'idle') {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Weight + info card */}
|
||||
<div className="bg-zinc-800 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -340,19 +371,17 @@ function ScaleTab({ device, weight, weightStable, rawAdc }: {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status message */}
|
||||
{status && (
|
||||
<div className={`rounded-lg px-3 py-2 mb-3 text-sm ${
|
||||
status.type === 'ok' ? 'bg-green-900/30 text-green-300 border border-green-800' : 'bg-red-900/30 text-red-300 border border-red-800'
|
||||
}`}>
|
||||
{status.msg}
|
||||
</div>
|
||||
)}
|
||||
{/* Status message */}
|
||||
{status && (
|
||||
<div className={`rounded-lg px-3 py-2 mb-3 text-sm ${
|
||||
status.type === 'ok' ? 'bg-green-900/30 text-green-300 border border-green-800' : 'bg-red-900/30 text-red-300 border border-red-800'
|
||||
}`}>
|
||||
{status.msg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Calibration flow */}
|
||||
{calStep === 'idle' ? (
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleTare}
|
||||
@@ -374,74 +403,94 @@ function ScaleTab({ device, weight, weightStable, rawAdc }: {
|
||||
{t('spoolbuddy.weight.calibrate', 'Calibrate')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{/* Step header with inline live weight */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm font-medium text-zinc-200">
|
||||
{calStep === 'tare'
|
||||
? t('spoolbuddy.settings.calStep1', 'Step 1: Remove all items from scale')
|
||||
: t('spoolbuddy.settings.calStep2', 'Step 2: Place known weight on scale')}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2 shrink-0">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${weightStable ? 'bg-green-500' : 'bg-amber-500 animate-pulse'}`} />
|
||||
<span className="text-xs font-mono text-zinc-400">
|
||||
{weight !== null ? `${weight.toFixed(1)}g` : '--'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{calStep === 'weight' && (
|
||||
<>
|
||||
{/* Weight input + numpad */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-zinc-400">{t('spoolbuddy.settings.knownWeight', 'Weight (g)')}</span>
|
||||
<div className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-3 py-1.5 text-right text-lg font-mono text-zinc-100">
|
||||
{knownWeight || '0'}<span className="text-zinc-500 ml-1">g</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1.5 mb-2">
|
||||
{['7','8','9','backspace','4','5','6','.','1','2','3','0'].map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => numpadPress(key)}
|
||||
className={`rounded text-lg font-medium transition-colors min-h-[56px] active:scale-95 ${
|
||||
key === 'backspace'
|
||||
? 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'
|
||||
: 'bg-zinc-800 text-zinc-100 hover:bg-zinc-700 border border-zinc-700'
|
||||
}`}
|
||||
>
|
||||
{key === 'backspace' ? '\u232B' : key}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
// --- Calibration wizard: step indicator left + content right ---
|
||||
return (
|
||||
<div className="flex h-full gap-3">
|
||||
{/* Left: step indicator */}
|
||||
<StepIndicator step={calStep} />
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2 mt-auto">
|
||||
<button
|
||||
onClick={() => { setCalStep('idle'); setStatus(null); }}
|
||||
className="flex-1 px-4 py-2.5 rounded-lg text-sm bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors min-h-[44px]"
|
||||
>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCalStep}
|
||||
disabled={busy}
|
||||
className="flex-1 px-4 py-2.5 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 disabled:opacity-40 transition-colors min-h-[44px] flex items-center justify-center gap-2"
|
||||
>
|
||||
{busy && (
|
||||
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
)}
|
||||
{calStep === 'tare' ? t('spoolbuddy.settings.setZero', 'Set Zero') : t('spoolbuddy.settings.calibrateNow', 'Calibrate')}
|
||||
</button>
|
||||
</div>
|
||||
{/* Right: content */}
|
||||
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
{/* Live weight bar */}
|
||||
<div className="flex items-center gap-2 bg-zinc-800 rounded-lg px-3 py-2 mb-2">
|
||||
<div className={`w-2 h-2 rounded-full shrink-0 ${weightStable ? 'bg-green-500' : 'bg-amber-500 animate-pulse'}`} />
|
||||
<span className="text-sm font-mono text-zinc-200">
|
||||
{weight !== null ? `${weight.toFixed(1)} g` : '-- g'}
|
||||
</span>
|
||||
<span className={`text-xs ml-auto ${weightStable ? 'text-green-400' : 'text-amber-400'}`}>
|
||||
{weightStable ? 'Stable' : 'Settling...'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status message */}
|
||||
{status && (
|
||||
<div className={`rounded-lg px-3 py-1.5 mb-2 text-sm ${
|
||||
status.type === 'ok' ? 'bg-green-900/30 text-green-300 border border-green-800' : 'bg-red-900/30 text-red-300 border border-red-800'
|
||||
}`}>
|
||||
{status.msg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step content */}
|
||||
{calStep === 'tare' ? (
|
||||
<div className="flex-1 flex flex-col">
|
||||
<p className="text-sm text-zinc-300 mb-4">
|
||||
{t('spoolbuddy.settings.calStep1', 'Remove all items from the scale and press Set Zero.')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-zinc-400 shrink-0">{t('spoolbuddy.settings.knownWeight', 'Known weight')}</span>
|
||||
<div className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-3 py-1.5 text-right text-lg font-mono text-zinc-100">
|
||||
{knownWeight || '0'}<span className="text-zinc-500 ml-1">g</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1.5 mb-2">
|
||||
{['7','8','9','backspace','4','5','6','.','1','2','3','0'].map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => numpadPress(key)}
|
||||
className={`rounded text-lg font-medium transition-colors min-h-[52px] active:scale-95 ${
|
||||
key === 'backspace'
|
||||
? 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'
|
||||
: 'bg-zinc-800 text-zinc-100 hover:bg-zinc-700 border border-zinc-700'
|
||||
}`}
|
||||
>
|
||||
{key === 'backspace' ? '\u232B' : key}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2 mt-auto">
|
||||
<button
|
||||
onClick={() => { setCalStep('idle'); setStatus(null); }}
|
||||
className="flex-1 px-4 py-2 rounded-lg text-sm bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors min-h-[44px]"
|
||||
>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCalStep}
|
||||
disabled={busy}
|
||||
className="flex-1 px-4 py-2 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 disabled:opacity-40 transition-colors min-h-[44px] flex items-center justify-center gap-2"
|
||||
>
|
||||
{busy && (
|
||||
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
)}
|
||||
{calStep === 'tare' ? t('spoolbuddy.settings.setZero', 'Set Zero') : t('spoolbuddy.settings.calibrateNow', 'Calibrate')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Display brightness and screen blanking control for SpoolBuddy kiosk.
|
||||
|
||||
Brightness: controlled via sysfs /sys/class/backlight/*/brightness (DSI displays only).
|
||||
Blanking: uses vcgencmd display_power (RPi firmware-level, works for both HDMI and DSI
|
||||
without needing Wayland socket access).
|
||||
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 shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -20,7 +19,6 @@ class DisplayControl:
|
||||
def __init__(self):
|
||||
self._backlight_path = self._find_backlight()
|
||||
self._max_brightness = self._read_max_brightness()
|
||||
self._has_vcgencmd = shutil.which("vcgencmd") is not None
|
||||
self._blank_timeout = 0 # seconds, 0 = disabled
|
||||
self._last_activity = time.monotonic()
|
||||
self._blanked = False
|
||||
@@ -28,12 +26,7 @@ class DisplayControl:
|
||||
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 unavailable")
|
||||
|
||||
if self._has_vcgencmd:
|
||||
logger.info("vcgencmd available, screen blanking enabled")
|
||||
else:
|
||||
logger.warning("vcgencmd not found, screen blanking unavailable")
|
||||
logger.info("No DSI backlight found, brightness control via frontend CSS")
|
||||
|
||||
def _find_backlight(self) -> Path | None:
|
||||
if not BACKLIGHT_BASE.exists():
|
||||
@@ -94,21 +87,9 @@ class DisplayControl:
|
||||
self._blank()
|
||||
|
||||
def _blank(self):
|
||||
if not self._has_vcgencmd:
|
||||
return
|
||||
try:
|
||||
subprocess.run(["vcgencmd", "display_power", "0"], capture_output=True, timeout=5)
|
||||
self._blanked = True
|
||||
logger.debug("Screen blanked via vcgencmd")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to blank screen: %s", e)
|
||||
self._blanked = True
|
||||
logger.debug("Screen idle timeout reached (frontend handles blanking)")
|
||||
|
||||
def _unblank(self):
|
||||
if not self._has_vcgencmd:
|
||||
return
|
||||
try:
|
||||
subprocess.run(["vcgencmd", "display_power", "1"], capture_output=True, timeout=5)
|
||||
self._blanked = False
|
||||
logger.debug("Screen unblanked via vcgencmd")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to unblank screen: %s", e)
|
||||
self._blanked = False
|
||||
logger.debug("Activity detected (frontend handles unblanking)")
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-BgnIB0Ee.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CRHdzSgp.css">
|
||||
<script type="module" crossorigin src="/assets/index-Dmdvp8GM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BaDzX3Lp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user