fix: timing flash + couleur idle LED + diagnostic auto-brightness
This commit is contained in:
@@ -32,6 +32,8 @@ filename="${1:-}"
|
||||
media_type="${2:-}"
|
||||
action_config_name="${3:-}"
|
||||
copies="${4:-1}"
|
||||
# Liste CSV d'imprimantes à exclure du load-balancing (optionnel, passé par JH-Photomaton)
|
||||
skip_printers="${5:-}"
|
||||
|
||||
# --- Noms des imprimantes CUPS (WiFi) ---
|
||||
PRINTER_1="Selphy_Blanche_WiFi"
|
||||
@@ -90,24 +92,44 @@ fi
|
||||
printer_1_status=$(lpstat -p "$PRINTER_1" 2>/dev/null || echo "not found")
|
||||
printer_2_status=$(lpstat -p "$PRINTER_2" 2>/dev/null || echo "not found")
|
||||
|
||||
# Verifie si une imprimante est dans la liste skip_printers (CSV)
|
||||
is_skipped() { [[ ",$skip_printers," == *",$1,"* ]]; }
|
||||
|
||||
# Une imprimante est utilisable si CUPS la connait ET qu'elle n'est pas exclue
|
||||
p1_ok=false
|
||||
p2_ok=false
|
||||
[[ "$printer_1_status" != "not found" ]] && ! is_skipped "$PRINTER_1" && p1_ok=true
|
||||
[[ "$printer_2_status" != "not found" ]] && ! is_skipped "$PRINTER_2" && p2_ok=true
|
||||
|
||||
SELECTED_PRINTER=""
|
||||
|
||||
if [[ "$printer_1_status" == *"idle"* ]]; then
|
||||
if $p1_ok && [[ "$printer_1_status" == *"idle"* ]]; then
|
||||
SELECTED_PRINTER="$PRINTER_1"
|
||||
log_info "Imprimante selectionnee: $PRINTER_1 (idle)"
|
||||
elif [[ "$printer_2_status" == *"idle"* ]]; then
|
||||
elif $p2_ok && [[ "$printer_2_status" == *"idle"* ]]; then
|
||||
SELECTED_PRINTER="$PRINTER_2"
|
||||
log_info "Imprimante selectionnee: $PRINTER_2 (idle)"
|
||||
else
|
||||
elif $p1_ok && $p2_ok; then
|
||||
# Les deux sont accessibles mais aucune n'est idle — load balance sur les jobs
|
||||
jobs_1=$(lpstat -o "$PRINTER_1" 2>/dev/null | wc -l)
|
||||
jobs_2=$(lpstat -o "$PRINTER_2" 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$jobs_1" -le "$jobs_2" ] 2>/dev/null; then
|
||||
SELECTED_PRINTER="$PRINTER_1"
|
||||
else
|
||||
SELECTED_PRINTER="$PRINTER_2"
|
||||
fi
|
||||
log_info "Aucune imprimante idle, file sur: $SELECTED_PRINTER (P1=$jobs_1 jobs, P2=$jobs_2 jobs)"
|
||||
log_info "Load balancing: $SELECTED_PRINTER (P1=$jobs_1 jobs, P2=$jobs_2 jobs)"
|
||||
elif $p1_ok; then
|
||||
SELECTED_PRINTER="$PRINTER_1"
|
||||
log_info "Seule imprimante disponible: $PRINTER_1 (P2 inaccessible ou exclue)"
|
||||
elif $p2_ok; then
|
||||
SELECTED_PRINTER="$PRINTER_2"
|
||||
log_info "Seule imprimante disponible: $PRINTER_2 (P1 inaccessible ou exclue)"
|
||||
else
|
||||
log_error "Aucune imprimante accessible dans CUPS"
|
||||
echo "PRINT_ERROR::no_printer_available"
|
||||
[ -f "$filename_print" ] && rm -f "$filename_print" 2>/dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Impression ---
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""API de contrôle des LEDs WS2812b."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -103,6 +104,60 @@ async def get_effects(request: Request):
|
||||
}
|
||||
|
||||
|
||||
@router.get("/leds/ambient-lux")
|
||||
async def get_ambient_lux(request: Request):
|
||||
"""Retourne la luminosité ambiante mesurée (dernière valeur connue)."""
|
||||
led = request.app.state.led_service
|
||||
lux = led._ambient_lux
|
||||
return {
|
||||
"ambient_lux": lux,
|
||||
"known": lux >= 0,
|
||||
"auto_brightness": request.app.state.config.leds.auto_brightness,
|
||||
"description": (
|
||||
"sombre (flash plein)" if lux < 50 and lux >= 0 else
|
||||
"luminosité moyenne (flash adapté)" if lux < 180 and lux >= 0 else
|
||||
"clair (flash réduit)" if lux >= 180 else
|
||||
"non mesuré"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/leds/measure-ambient")
|
||||
async def measure_ambient_lux(request: Request):
|
||||
"""Déclenche une mesure de luminosité ambiante via snapshot liveview.
|
||||
Utile pour diagnostiquer si le flux liveview est accessible depuis JH-Photomaton.
|
||||
"""
|
||||
pb = request.app.state.photobooth_service
|
||||
led = request.app.state.led_service
|
||||
|
||||
snapshot = await pb.get_liveview_snapshot()
|
||||
if not snapshot:
|
||||
return JSONResponse({
|
||||
"ok": False,
|
||||
"error": "Aucun endpoint liveview accessible sur photobooth-app. "
|
||||
"Vérifiez que photobooth-app est démarré et que le flux vidéo est actif.",
|
||||
}, status_code=503)
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
import io
|
||||
img = Image.open(io.BytesIO(snapshot)).convert("L").resize((80, 60))
|
||||
pixels = list(img.getdata())
|
||||
avg = sum(pixels) / len(pixels)
|
||||
led.set_ambient_lux(avg)
|
||||
return {
|
||||
"ok": True,
|
||||
"ambient_lux": round(avg, 1),
|
||||
"description": (
|
||||
"sombre → flash plein (255)" if avg < 50 else
|
||||
f"moyen → flash {int(255 - (avg - 50) / 130 * 135)}" if avg < 180 else
|
||||
"clair → flash réduit (80)"
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.put("/leds/effect/{effect_name}")
|
||||
async def update_effect(
|
||||
request: Request,
|
||||
|
||||
@@ -168,8 +168,6 @@ async def gallery_print_request(request: Request, body: dict = Body(...)):
|
||||
"""Demande d'impression depuis la galerie publique.
|
||||
Vérifie que l'impression est activée et que le quota session n'est pas dépassé.
|
||||
"""
|
||||
from pathlib import Path as _Path
|
||||
|
||||
cfg_print = request.app.state.config.print
|
||||
|
||||
# ── Activation ────────────────────────────────────────────────────────────
|
||||
@@ -191,15 +189,21 @@ async def gallery_print_request(request: Request, body: dict = Body(...)):
|
||||
"quota_exceeded": True,
|
||||
}, status_code=429)
|
||||
|
||||
# ── Résolution du fichier ─────────────────────────────────────────────────
|
||||
media_dir = _Path(request.app.state.config.photobooth.media_dir)
|
||||
candidates = sorted(media_dir.glob(f"{photo_id}*"))
|
||||
if not candidates:
|
||||
# ── Résolution du fichier via l'API photobooth-app ───────────────────────
|
||||
# On passe par get_media_item() comme le fait admin_print_photo — le champ
|
||||
# "processed" contient le chemin réel sur le disque (ex: media/processed_full/xxx.jpg).
|
||||
# Un simple glob sur photo_id ne marche pas car l'id interne ≠ nom de fichier.
|
||||
pb = request.app.state.photobooth_service
|
||||
item = await pb.get_media_item(photo_id)
|
||||
if not item:
|
||||
return JSONResponse({"error": "Photo introuvable"}, status_code=404)
|
||||
filepath = str(candidates[0])
|
||||
file_path = pb.media_file_path(item)
|
||||
if not file_path or not file_path.exists():
|
||||
logger.error("Fichier manquant pour %s: %s", photo_id, file_path)
|
||||
return JSONResponse({"error": "Fichier photo introuvable sur le disque"}, status_code=404)
|
||||
filepath = str(file_path)
|
||||
|
||||
# ── Enqueue ───────────────────────────────────────────────────────────────
|
||||
pb = request.app.state.photobooth_service
|
||||
printer_svc = request.app.state.printer_service
|
||||
ws = request.app.state.ws_manager
|
||||
led = request.app.state.led_service
|
||||
@@ -234,6 +238,16 @@ async def gallery_print_request(request: Request, body: dict = Body(...)):
|
||||
|
||||
# ── Gestion avancée des imprimantes CUPS ──────────────────────────────────────
|
||||
|
||||
@router.post("/print/printers/{printer_name}/set-excluded")
|
||||
async def set_printer_excluded(request: Request, printer_name: str, excluded: bool = Query(...)):
|
||||
"""Exclut (ou réintègre) une imprimante du load-balancing JH-Photomaton.
|
||||
N'agit pas sur CUPS — sert uniquement à ignorer une imprimante temporairement hors ligne."""
|
||||
config_svc = request.app.state.config_service
|
||||
config_svc.save_printer_excluded(printer_name, excluded)
|
||||
logger.info("Imprimante %s : excluded=%s", printer_name, excluded)
|
||||
return {"ok": True, "printer": printer_name, "excluded": excluded}
|
||||
|
||||
|
||||
@router.post("/print/printers/{printer_name}/enable")
|
||||
async def printer_enable(request: Request, printer_name: str):
|
||||
"""Active une imprimante CUPS (cupsenable + cupsaccept)."""
|
||||
|
||||
@@ -228,6 +228,24 @@ class ConfigService:
|
||||
self._config.print.user_print_quota = quota
|
||||
self._config.print.user_print_max_copies = max_copies
|
||||
|
||||
def save_printer_excluded(self, printer_name: str, excluded: bool):
|
||||
"""Marque (ou démarque) une imprimante comme exclue du load-balancing."""
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f) or {}
|
||||
printers = raw.get("print", {}).get("printers", [])
|
||||
for p in printers:
|
||||
if isinstance(p, dict) and p.get("name") == printer_name:
|
||||
p["excluded"] = excluded
|
||||
break
|
||||
raw.setdefault("print", {})["printers"] = printers
|
||||
with open(self.path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
|
||||
if self._config:
|
||||
for p in self._config.print.printers:
|
||||
if isinstance(p, dict) and p.get("name") == printer_name:
|
||||
p["excluded"] = excluded
|
||||
break
|
||||
|
||||
def save_print_mode(self, mode: str):
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f) or {}
|
||||
|
||||
@@ -191,26 +191,40 @@ class PhotoboothService:
|
||||
"""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
|
||||
1. Endpoints snapshot direct (retour JPEG immédiat)
|
||||
2. Flux MJPEG — extrait la premiere frame
|
||||
|
||||
Retourne des bytes JPEG ou None si rien n'est disponible.
|
||||
"""
|
||||
# 1. Snapshot direct
|
||||
for path in ("/api/stream/snapshot",):
|
||||
# 1. Endpoints snapshot direct (differentes versions de photobooth-app)
|
||||
snapshot_paths = (
|
||||
"/api/stream/snapshot",
|
||||
"/api/video/preview/snapshot",
|
||||
"/api/livestream/snapshot",
|
||||
"/api/cam/stream/snapshot",
|
||||
)
|
||||
for path in snapshot_paths:
|
||||
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:
|
||||
logger.debug("Snapshot OK via %s", path)
|
||||
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"):
|
||||
# 2. Premiere frame d'un flux MJPEG (differentes versions)
|
||||
mjpeg_paths = (
|
||||
"/stream.mjpg",
|
||||
"/api/stream",
|
||||
"/api/video/stream",
|
||||
"/api/video/preview",
|
||||
"/api/livestream",
|
||||
"/livestream",
|
||||
)
|
||||
for path in mjpeg_paths:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=3.0) as client:
|
||||
async with client.stream("GET", f"{self._base}{path}") as resp:
|
||||
@@ -223,12 +237,14 @@ class PhotoboothService:
|
||||
if start >= 0:
|
||||
end = buf.find(b"\xff\xd9", start)
|
||||
if end >= 0:
|
||||
logger.debug("MJPEG frame OK via %s", path)
|
||||
return buf[start:end + 2]
|
||||
if len(buf) > 500_000:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug("MJPEG %s: %s", path, e)
|
||||
|
||||
logger.debug("Aucun endpoint liveview disponible sur %s", self._base)
|
||||
return None
|
||||
|
||||
async def close(self):
|
||||
|
||||
@@ -244,7 +244,13 @@ class PrinterService:
|
||||
if not Path(filename).exists():
|
||||
return {"success": False, "error": f"Fichier introuvable: {filename}"}
|
||||
|
||||
cmd = ["/bin/bash", script, filename, "image", "default", str(copies)]
|
||||
# Imprimantes exclues du load-balancing (flag excluded dans config)
|
||||
excluded_list = ",".join(
|
||||
self._printer_name(p)
|
||||
for p in self._cfg.printers
|
||||
if isinstance(p, dict) and p.get("excluded", False)
|
||||
)
|
||||
cmd = ["/bin/bash", script, filename, "image", "default", str(copies), excluded_list]
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=120
|
||||
@@ -361,11 +367,13 @@ class PrinterService:
|
||||
statuses = []
|
||||
for p in self._cfg.printers:
|
||||
name = self._printer_name(p)
|
||||
excluded = isinstance(p, dict) and bool(p.get("excluded", False))
|
||||
status = await asyncio.to_thread(self._get_printer_status, name)
|
||||
stats = await self._get_printer_db_stats(name)
|
||||
statuses.append({
|
||||
"name": name,
|
||||
"label": self._printer_label(p),
|
||||
"excluded": excluded,
|
||||
**status,
|
||||
**stats,
|
||||
})
|
||||
@@ -619,8 +627,8 @@ class PrinterService:
|
||||
async def enable_printer(self, printer_name: str) -> bool:
|
||||
"""Active l'imprimante CUPS (cupsenable) et accepte les nouveaux jobs."""
|
||||
try:
|
||||
r1 = subprocess.run(["cupsenable", printer_name], capture_output=True, timeout=10)
|
||||
r2 = subprocess.run(["cupsaccept", printer_name], capture_output=True, timeout=10)
|
||||
r1 = subprocess.run(["sudo", "cupsenable", printer_name], capture_output=True, timeout=10)
|
||||
r2 = subprocess.run(["sudo", "cupsaccept", printer_name], capture_output=True, timeout=10)
|
||||
return r1.returncode == 0 and r2.returncode == 0
|
||||
except Exception as e:
|
||||
logger.error("Erreur enable printer %s: %s", printer_name, e)
|
||||
@@ -629,7 +637,7 @@ class PrinterService:
|
||||
async def disable_printer(self, printer_name: str) -> bool:
|
||||
"""Désactive l'imprimante CUPS (cupsdisable)."""
|
||||
try:
|
||||
result = subprocess.run(["cupsdisable", printer_name], capture_output=True, timeout=10)
|
||||
result = subprocess.run(["sudo", "cupsdisable", printer_name], capture_output=True, timeout=10)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
logger.error("Erreur disable printer %s: %s", printer_name, e)
|
||||
@@ -638,7 +646,7 @@ class PrinterService:
|
||||
async def reject_jobs(self, printer_name: str) -> bool:
|
||||
"""Refuse les nouveaux jobs (cupsreject) sans stopper l'impression en cours."""
|
||||
try:
|
||||
result = subprocess.run(["cupsreject", printer_name], capture_output=True, timeout=10)
|
||||
result = subprocess.run(["sudo", "cupsreject", printer_name], capture_output=True, timeout=10)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
logger.error("Erreur reject printer %s: %s", printer_name, e)
|
||||
|
||||
@@ -83,9 +83,9 @@ leds:
|
||||
mode: solid
|
||||
idle:
|
||||
color:
|
||||
- 0
|
||||
- 50
|
||||
- 30
|
||||
- 80
|
||||
- 10
|
||||
mode: solid
|
||||
speed: 0.025
|
||||
printing:
|
||||
|
||||
@@ -52,10 +52,13 @@
|
||||
.printer-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||||
.btn-xs { padding: .3rem .65rem; font-size: .8rem; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: opacity .2s; }
|
||||
.btn-xs:hover { opacity: .8; }
|
||||
.btn-enable { background: #1a7340; color: #fff; }
|
||||
.btn-disable { background: #784700; color: #fff; }
|
||||
.btn-clear { background: #7a0000; color: #fff; }
|
||||
.btn-reject { background: #555; color: #fff; }
|
||||
.btn-enable { background: #1a7340; color: #fff; }
|
||||
.btn-disable { background: #784700; color: #fff; }
|
||||
.btn-clear { background: #7a0000; color: #fff; }
|
||||
.btn-reject { background: #555; color: #fff; }
|
||||
.btn-exclude { background: #4a2080; color: #fff; }
|
||||
.btn-include { background: #205080; color: #fff; }
|
||||
.bs-excluded { background: rgba(100,40,160,.2); color: #b085f0; border: 1px solid rgba(100,40,160,.3); }
|
||||
|
||||
/* Jobs CUPS détaillés */
|
||||
.cups-jobs { font-size: .82rem; }
|
||||
@@ -131,10 +134,13 @@
|
||||
<span class="badge-printer badge-state bs-{{ p.state }}">
|
||||
{% if p.state == 'idle' %}✅ Disponible
|
||||
{% elif p.state == 'printing' %}🖨 En impression
|
||||
{% elif p.state == 'disabled' %}⏸ Désactivée
|
||||
{% elif p.state == 'disabled' %}⏸ Désactivée CUPS
|
||||
{% elif p.state == 'offline' %}❌ Hors ligne
|
||||
{% else %}❓ {{ p.state }}{% endif %}
|
||||
</span>
|
||||
{% if p.excluded %}
|
||||
<span class="badge-printer bs-excluded">🚫 Exclue du load-balancing</span>
|
||||
{% endif %}
|
||||
<span class="badge-printer badge-accept {{ 'bs-accept' if p.accepting else 'bs-reject' }}">
|
||||
{{ '✓ Accepte les jobs' if p.accepting else '✗ Refuse les jobs' }}
|
||||
</span>
|
||||
@@ -193,15 +199,20 @@
|
||||
<!-- Actions imprimante -->
|
||||
<div class="printer-actions">
|
||||
{% if p.state == 'disabled' %}
|
||||
<button class="btn-xs btn-enable" onclick="printerAction('{{ p.name }}','enable')">▶ Activer</button>
|
||||
<button class="btn-xs btn-enable" onclick="printerAction('{{ p.name }}','enable')">▶ Activer CUPS</button>
|
||||
{% else %}
|
||||
<button class="btn-xs btn-disable" onclick="printerAction('{{ p.name }}','disable')">⏸ Désactiver</button>
|
||||
<button class="btn-xs btn-disable" onclick="printerAction('{{ p.name }}','disable')">⏸ Désactiver CUPS</button>
|
||||
{% endif %}
|
||||
{% if p.accepting %}
|
||||
<button class="btn-xs btn-reject" onclick="printerAction('{{ p.name }}','reject')">🚫 Refuser jobs</button>
|
||||
{% else %}
|
||||
<button class="btn-xs btn-enable" onclick="printerAction('{{ p.name }}','enable')">✓ Accepter jobs</button>
|
||||
{% endif %}
|
||||
{% if p.excluded %}
|
||||
<button class="btn-xs btn-include" onclick="setExcluded('{{ p.name }}', false)">✓ Réintégrer</button>
|
||||
{% else %}
|
||||
<button class="btn-xs btn-exclude" onclick="setExcluded('{{ p.name }}', true)">🚫 Exclure</button>
|
||||
{% endif %}
|
||||
{% if p.jobs_count %}
|
||||
<button class="btn-xs btn-clear" onclick="clearJobs('{{ p.name }}')">✕ Vider file</button>
|
||||
{% endif %}
|
||||
@@ -426,16 +437,33 @@ async function refreshPrinters() {
|
||||
acceptBadge.textContent = p.accepting ? '✓ Accepte les jobs' : '✗ Refuse les jobs';
|
||||
}
|
||||
|
||||
// Badge excluded
|
||||
const statesDiv = card.querySelector('.printer-states');
|
||||
if (statesDiv) {
|
||||
const excBadge = statesDiv.querySelector('.bs-excluded');
|
||||
if (p.excluded && !excBadge) {
|
||||
const b = document.createElement('span');
|
||||
b.className = 'badge-printer bs-excluded';
|
||||
b.textContent = '🚫 Exclue du load-balancing';
|
||||
statesDiv.appendChild(b);
|
||||
} else if (!p.excluded && excBadge) {
|
||||
excBadge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Boutons actions
|
||||
const actionsDiv = card.querySelector('.printer-actions');
|
||||
if (actionsDiv) {
|
||||
actionsDiv.innerHTML = `
|
||||
${p.state === 'disabled'
|
||||
? `<button class="btn-xs btn-enable" onclick="printerAction('${p.name}','enable')">▶ Activer</button>`
|
||||
: `<button class="btn-xs btn-disable" onclick="printerAction('${p.name}','disable')">⏸ Désactiver</button>`}
|
||||
? `<button class="btn-xs btn-enable" onclick="printerAction('${p.name}','enable')">▶ Activer CUPS</button>`
|
||||
: `<button class="btn-xs btn-disable" onclick="printerAction('${p.name}','disable')">⏸ Désactiver CUPS</button>`}
|
||||
${p.accepting
|
||||
? `<button class="btn-xs btn-reject" onclick="printerAction('${p.name}','reject')">🚫 Refuser jobs</button>`
|
||||
: `<button class="btn-xs btn-enable" onclick="printerAction('${p.name}','enable')">✓ Accepter jobs</button>`}
|
||||
${p.excluded
|
||||
? `<button class="btn-xs btn-include" onclick="setExcluded('${p.name}',false)">✓ Réintégrer</button>`
|
||||
: `<button class="btn-xs btn-exclude" onclick="setExcluded('${p.name}',true)">🚫 Exclure</button>`}
|
||||
${p.jobs_count ? `<button class="btn-xs btn-clear" onclick="clearJobs('${p.name}')">✕ Vider file</button>` : ''}
|
||||
`;
|
||||
}
|
||||
@@ -472,6 +500,14 @@ async function printerAction(name, action) {
|
||||
} catch(err) { showToast('Erreur : ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function setExcluded(name, excluded) {
|
||||
try {
|
||||
await api('POST', `/api/print/printers/${name}/set-excluded?excluded=${excluded}`);
|
||||
showToast(excluded ? `🚫 ${name} exclue du load-balancing` : `✓ ${name} réintégrée`, excluded ? 'info' : 'success');
|
||||
setTimeout(refreshPrinters, 400);
|
||||
} catch(err) { showToast('Erreur : ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function clearJobs(printer) {
|
||||
if (!confirm(`Vider toute la file CUPS de ${printer} ?`)) return;
|
||||
try {
|
||||
|
||||
@@ -394,6 +394,16 @@ select.field {
|
||||
<p id="auto-brightness-status" style="font-size:.78rem; color:var(--text-muted); margin-top:.5rem;">
|
||||
⏳ Chargement…
|
||||
</p>
|
||||
<div style="display:flex; gap:.5rem; align-items:center; margin-top:.5rem; flex-wrap:wrap;">
|
||||
<button class="btn btn-ghost btn-sm" onclick="measureAmbient()" id="btn-measure-lux">
|
||||
📷 Tester la mesure
|
||||
</button>
|
||||
<span id="lux-status" style="font-size:.8rem; color:var(--text-muted);"></span>
|
||||
</div>
|
||||
<div id="lux-info" style="font-size:.78rem; margin-top:.4rem; color:var(--text-muted); display:none;">
|
||||
Luminosité mesurée : <strong id="lux-val">—</strong> / 255 →
|
||||
<span id="lux-desc">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -875,6 +885,37 @@ async function loadFlashConfig() {
|
||||
} catch(e) { console.warn('loadFlashConfig:', e); }
|
||||
}
|
||||
|
||||
async function measureAmbient() {
|
||||
const btn = document.getElementById('btn-measure-lux');
|
||||
const status = document.getElementById('lux-status');
|
||||
const info = document.getElementById('lux-info');
|
||||
btn.disabled = true;
|
||||
status.textContent = '⏳ Mesure en cours…';
|
||||
status.style.color = 'var(--text-muted)';
|
||||
try {
|
||||
const r = await api('POST', '/api/leds/measure-ambient');
|
||||
if (r.ok) {
|
||||
document.getElementById('lux-val').textContent = r.ambient_lux;
|
||||
document.getElementById('lux-desc').textContent = r.description;
|
||||
info.style.display = '';
|
||||
status.textContent = '✅ Mesure réussie';
|
||||
status.style.color = '#4caf50';
|
||||
showToast(`✅ Luminosité : ${r.ambient_lux} — ${r.description}`, 'success');
|
||||
} else {
|
||||
status.textContent = '❌ ' + (r.error || 'Erreur inconnue');
|
||||
status.style.color = '#e05050';
|
||||
showToast('❌ Mesure impossible : ' + r.error, 'error');
|
||||
}
|
||||
} catch(e) {
|
||||
status.textContent = '❌ Liveview inaccessible';
|
||||
status.style.color = '#e05050';
|
||||
showToast('❌ Liveview inaccessible (endpoint 404). Vérifiez que photobooth-app est démarré.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
setTimeout(() => { status.textContent = ''; }, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
async function setAutoBrightness(enabled) {
|
||||
try {
|
||||
await api('PUT', `/api/leds/auto-brightness?enabled=${enabled}`);
|
||||
|
||||
@@ -503,7 +503,7 @@
|
||||
"backends": {
|
||||
"enable_livestream": true,
|
||||
"retry_capture": 3,
|
||||
"countdown_camera_capture_offset": 0.2,
|
||||
"countdown_camera_capture_offset": 0.5,
|
||||
"index_backend_stills": 0,
|
||||
"index_backend_video": 0,
|
||||
"index_backend_multicam": 0,
|
||||
|
||||
Reference in New Issue
Block a user