feat: relay inversé, LED idle solid, luminosité manuelle, auto-flash lux
This commit is contained in:
@@ -57,8 +57,8 @@ class ButtonService:
|
||||
try:
|
||||
self._relay = OutputDevice(
|
||||
self._cfg.relay_pin,
|
||||
active_high=True,
|
||||
initial_value=True,
|
||||
active_high=False,
|
||||
initial_value=False,
|
||||
)
|
||||
self._button = Button(
|
||||
self._cfg.pin,
|
||||
|
||||
@@ -58,6 +58,7 @@ class LEDConfig:
|
||||
pin: int = 18
|
||||
count: int = 35
|
||||
brightness: int = 180
|
||||
auto_brightness: bool = False
|
||||
freq_hz: int = 800000
|
||||
dma: int = 10
|
||||
strip_type: str = "WS2812"
|
||||
@@ -140,6 +141,7 @@ class ConfigService:
|
||||
pin=led_raw.get("pin", 18),
|
||||
count=led_raw.get("count", 35),
|
||||
brightness=led_raw.get("brightness", 180),
|
||||
auto_brightness=led_raw.get("auto_brightness", False),
|
||||
freq_hz=led_raw.get("freq_hz", 800000),
|
||||
dma=led_raw.get("dma", 10),
|
||||
strip_type=led_raw.get("strip_type", "WS2812"),
|
||||
@@ -221,6 +223,24 @@ class ConfigService:
|
||||
self.load()
|
||||
return self._config
|
||||
|
||||
def save_led_brightness(self, brightness: int):
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f) or {}
|
||||
raw.setdefault("leds", {})["brightness"] = brightness
|
||||
with open(self.path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
|
||||
if self._config:
|
||||
self._config.leds.brightness = brightness
|
||||
|
||||
def save_led_auto_brightness(self, enabled: bool):
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f) or {}
|
||||
raw.setdefault("leds", {})["auto_brightness"] = enabled
|
||||
with open(self.path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
|
||||
if self._config:
|
||||
self._config.leds.auto_brightness = enabled
|
||||
|
||||
def save_led_effect(self, effect_name: str, effect_data: dict):
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f) or {}
|
||||
|
||||
@@ -58,6 +58,7 @@ class LEDService:
|
||||
self._lock = threading.Lock()
|
||||
self._on_change_cb: Callable | None = None
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
self._ambient_lux: float = -1.0 # -1 = inconnu
|
||||
|
||||
# ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -111,6 +112,20 @@ class LEDService:
|
||||
def set_color(self, r: int, g: int, b: int):
|
||||
self._fill(WS_Color(r, g, b))
|
||||
|
||||
def set_brightness(self, value: int):
|
||||
"""Regle la luminosite globale (0-255) et l'applique immediatement."""
|
||||
value = max(0, min(255, value))
|
||||
self._cfg.brightness = value
|
||||
if HAS_WS281X and self._strip:
|
||||
self._strip.setBrightness(value)
|
||||
self._strip.show()
|
||||
logger.info("Luminosite LED: %d", value)
|
||||
|
||||
def set_ambient_lux(self, lux: float):
|
||||
"""Definit la luminosite ambiante mesuree (0-255 echelle pixel)."""
|
||||
self._ambient_lux = lux
|
||||
logger.debug("Lux ambiant: %.1f", lux)
|
||||
|
||||
# ── Thread principal ──────────────────────────────────────────────────────
|
||||
|
||||
def _run(self):
|
||||
@@ -154,8 +169,14 @@ class LEDService:
|
||||
def _effect_idle(self):
|
||||
cfg = self._cfg.get_effect("idle")
|
||||
c = cfg.color
|
||||
speed = cfg.speed
|
||||
|
||||
if cfg.mode == "solid":
|
||||
self._fill(WS_Color(c[0], c[1], c[2]))
|
||||
self._cmd_event.wait()
|
||||
return
|
||||
|
||||
# mode "breathe" (defaut)
|
||||
speed = cfg.speed
|
||||
step = 0
|
||||
while not self._cmd_event.is_set() and not self._stop_event.is_set():
|
||||
t = (1 + math.sin(step * 0.1)) / 2
|
||||
@@ -202,9 +223,23 @@ class LEDService:
|
||||
flash_color = WS_Color(cfg.color[0], cfg.color[1], cfg.color[2])
|
||||
off = WS_Color(0, 0, 0)
|
||||
|
||||
# Boost luminosite max pendant le flash
|
||||
# Calcul de la luminosite du flash
|
||||
# Si auto_brightness activee et lux connu, on adapte. Sinon boost a 255.
|
||||
if self._cfg.auto_brightness and self._ambient_lux >= 0:
|
||||
lux = self._ambient_lux
|
||||
if lux < 50:
|
||||
flash_brightness = 255 # salle sombre → flash plein
|
||||
elif lux < 180:
|
||||
# lineaire : 255 a 50 lux → 120 a 180 lux
|
||||
flash_brightness = int(255 - (lux - 50) / 130 * 135)
|
||||
else:
|
||||
flash_brightness = 80 # salle tres claire → flash reduit
|
||||
logger.debug("Flash brightness auto: %d (lux=%.1f)", flash_brightness, lux)
|
||||
else:
|
||||
flash_brightness = 255
|
||||
|
||||
if HAS_WS281X and self._strip:
|
||||
self._strip.setBrightness(255)
|
||||
self._strip.setBrightness(flash_brightness)
|
||||
|
||||
try:
|
||||
for _ in range(cfg.flashes):
|
||||
|
||||
@@ -162,5 +162,49 @@ class PhotoboothService:
|
||||
"""Lit l'état actuel depuis le fichier private.css (ou depuis la config)."""
|
||||
return self._cfg.show_delete_button
|
||||
|
||||
async def get_liveview_snapshot(self) -> bytes | None:
|
||||
"""Tente de recuperer une frame JPEG du liveview de photobooth-app.
|
||||
|
||||
Essaie dans l'ordre :
|
||||
1. /api/stream/snapshot — endpoint snapshot direct (si disponible)
|
||||
2. /stream.mjpg — flux MJPEG classique, extrait la premiere frame
|
||||
3. /api/stream — flux MJPEG alternatif
|
||||
|
||||
Retourne des bytes JPEG ou None si rien n'est disponible.
|
||||
"""
|
||||
# 1. Snapshot direct
|
||||
for path in ("/api/stream/snapshot",):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=2.0) as client:
|
||||
r = await client.get(f"{self._base}{path}")
|
||||
if r.status_code == 200:
|
||||
ct = r.headers.get("content-type", "")
|
||||
if "jpeg" in ct or "image" in ct:
|
||||
return r.content
|
||||
except Exception as e:
|
||||
logger.debug("Snapshot %s: %s", path, e)
|
||||
|
||||
# 2. Premiere frame d'un flux MJPEG
|
||||
for path in ("/stream.mjpg", "/api/stream"):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=3.0) as client:
|
||||
async with client.stream("GET", f"{self._base}{path}") as resp:
|
||||
if resp.status_code != 200:
|
||||
continue
|
||||
buf = b""
|
||||
async for chunk in resp.aiter_bytes(4096):
|
||||
buf += chunk
|
||||
start = buf.find(b"\xff\xd8")
|
||||
if start >= 0:
|
||||
end = buf.find(b"\xff\xd9", start)
|
||||
if end >= 0:
|
||||
return buf[start:end + 2]
|
||||
if len(buf) > 500_000:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug("MJPEG %s: %s", path, e)
|
||||
|
||||
return None
|
||||
|
||||
async def close(self):
|
||||
await self._client.aclose()
|
||||
|
||||
Reference in New Issue
Block a user