Fiabilite Canon : veille/reveil relais, sequence boot, stats diagnostic, fin evenement, compteur consommables

- Preview thread : poll uniquement quand ecran capture actif, keepalive 30s sinon
- Sequence boot Canon : power cycle relais si absent USB au demarrage
- Veille auto 30min : Canon coupe par relais apres inactivite, reveil au preview_start
- Stats diagnostic /api/canon/stats : keepalive, preview, captures, connexions
- Escalade reconnexion adoucie : pas d'USB reset les 2 premieres tentatives
- Hard reset relais 10s (resistance decharge 990ohm sur dummy battery 8V)
- Gestion fin evenement : date_fin + bouton terminer + guard 403 impression
- Compteur base sur min(papier, ruban) avec cap evenement optionnel
- Pre-eclairage countdown : projecteurs ON 3s avant capture pour mesure expo
- Canon _configurer_init() : ISO 800, drivemode Single, viewfinder, EXIF preserve
- Admin : detail evenement, compteur consommables, galerie impression

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyN9xFp84P9VaWT3Qe2DXu
This commit is contained in:
2026-08-14 19:33:28 +02:00
parent a6766f529e
commit 4e267492c2
16 changed files with 1424 additions and 162 deletions

View File

@@ -88,11 +88,12 @@ class Camera:
pass pass
cam.init() cam.init()
self.camera = cam self.camera = cam
self._desactiver_veille_init() time.sleep(1)
self._activer_viewfinder_init() self._configurer_init()
self.connectee = True self.connectee = True
self.mode = "gphoto2" self.mode = "gphoto2"
self.preview_dslr_ok = True self.preview_dslr_ok = True
self._log_reglages()
log.info("Camera DSLR connectee (LiveView actif)") log.info("Camera DSLR connectee (LiveView actif)")
return True return True
except Exception as e: except Exception as e:
@@ -190,23 +191,36 @@ class Camera:
vf = cfg.get_child_by_name("viewfinder") vf = cfg.get_child_by_name("viewfinder")
vf.set_value(0) vf.set_value(0)
self.camera.set_config(cfg) self.camera.set_config(cfg)
time.sleep(0.8)
except Exception: except Exception:
pass pass
log.info(f"Déclenchement capture DSLR (tentative {attempt+1}/3)") log.info(f"Déclenchement capture DSLR (tentative {attempt+1}/3)")
t0 = time.time()
chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE) chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE)
t1 = time.time()
fichier_camera = gp.CameraFile() fichier_camera = gp.CameraFile()
self.camera.file_get( self.camera.file_get(
chemin_camera.folder, chemin_camera.name, gp.GP_FILE_TYPE_NORMAL, fichier_camera chemin_camera.folder, chemin_camera.name, gp.GP_FILE_TYPE_NORMAL, fichier_camera
) )
t2 = time.time()
tmp_path = str(chemin_dest) + ".tmp" tmp_path = str(chemin_dest) + ".tmp"
fichier_camera.save(tmp_path) fichier_camera.save(tmp_path)
log.info(f"Capture: shutter={t1-t0:.1f}s download={t2-t1:.1f}s")
threading.Thread(target=self._post_capture_warmup, daemon=True).start() threading.Thread(target=self._post_capture_warmup, daemon=True).start()
from PIL import ImageOps as PILImageOps from PIL import ImageOps as PILImageOps
img = PILImageOps.exif_transpose(PILImage.open(tmp_path)) t3 = time.time()
pil_img = PILImage.open(tmp_path)
exif_data = pil_img.info.get("exif")
img = PILImageOps.exif_transpose(pil_img)
if img.width > 4000: if img.width > 4000:
ratio = 4000 / img.width ratio = 4000 / img.width
img = img.resize((4000, int(img.height * ratio)), PILImage.LANCZOS) img = img.resize((4000, int(img.height * ratio)), PILImage.BILINEAR)
img.save(str(chemin_dest), "JPEG", quality=92) save_kwargs = {"quality": 92}
if exif_data:
save_kwargs["exif"] = exif_data
img.save(str(chemin_dest), "JPEG", **save_kwargs)
t4 = time.time()
log.info(f"Post-process: {t4-t3:.1f}s")
Path(tmp_path).unlink(missing_ok=True) Path(tmp_path).unlink(missing_ok=True)
log.info(f"Photo capturee (DSLR) : {chemin_dest} ({img.width}x{img.height})") log.info(f"Photo capturee (DSLR) : {chemin_dest} ({img.width}x{img.height})")
return chemin_dest return chemin_dest
@@ -292,35 +306,48 @@ class Camera:
except Exception as e: except Exception as e:
log.debug(f"configurer_flash : {e}") log.debug(f"configurer_flash : {e}")
def _desactiver_veille_init(self): def _configurer_init(self):
"""Desactive la mise en veille auto du boitier (Canon : autopoweroff en minutes, 0=jamais). """Configure le Canon : veille OFF, ISO, drivemode, viewfinder (appels séparés)."""
Sans ca, le DSLR s'eteint seul apres quelques minutes d'inactivite et apparait deconnecte.""" reglages = [
candidats = ["autopoweroff", "auto_power_off", "/main/settings/autopoweroff"] ("autopoweroff", 0),
("viewfinder", 1),
("iso", "800"),
("drivemode", "Single"),
]
for nom, valeur in reglages:
for tentative in range(3):
try: try:
cfg = self.camera.get_config() cfg = self.camera.get_config()
for nom in candidats: w = cfg.get_child_by_name(nom)
try: w.set_value(valeur)
widget = cfg.get_child_by_name(nom)
widget.set_value(0)
self.camera.set_config(cfg) self.camera.set_config(cfg)
log.info(f"Mise en veille DSLR désactivée ({nom})") log.info(f"Canon init: {nom} = {valeur}")
return break
except gp.GPhoto2Error as e:
if tentative < 2:
time.sleep(0.5)
else:
log.warning(f"Canon init {nom}: {e}")
except Exception: except Exception:
continue break
log.debug("Widget autopoweroff non trouvé sur ce modèle")
except Exception as e:
log.debug(f"_desactiver_veille_init : {e}")
def _activer_viewfinder_init(self): def _log_reglages(self):
"""Active le LiveView pendant l'init (pas de lock, appelé avant que le thread démarre).""" """Log les réglages Canon actuels pour diagnostic."""
try: try:
cfg = self.camera.get_config() cfg = self.camera.get_config()
vf = cfg.get_child_by_name("viewfinder") vals = {}
vf.set_value(1) for nom in ["autoexposuremode", "iso", "shutterspeed", "aperture", "meteringmode"]:
self.camera.set_config(cfg) try:
log.info("Viewfinder activé (init)") w = cfg.get_child_by_name(nom)
except Exception as e: vals[nom] = w.get_value()
log.warning(f"Viewfinder non supporté : {e}") log.info(f"Canon actuel: {nom} = {vals[nom]}")
except Exception:
pass
mode = vals.get("autoexposuremode", "")
if mode in ("Flash Off", "Auto", "Night Portrait", "Landscape", "Portrait", "Sports"):
log.warning(f"Canon en mode scène '{mode}' — ISO/vitesse non modifiables. Tourner le dial sur M ou Av.")
except Exception:
pass
def activer_viewfinder(self): def activer_viewfinder(self):
"""Active le LiveView (miroir levé) depuis le thread — thread-safe.""" """Active le LiveView (miroir levé) depuis le thread — thread-safe."""

View File

@@ -203,16 +203,28 @@ def detecter_usb() -> list[str]:
def compteur_restant() -> dict: def compteur_restant() -> dict:
"""Retourne l'etat du compteur.""" """Retourne l'etat du compteur, basé sur les consommables restants."""
config = charger_config() config = charger_config()
compteur = config.get("compteur", {}) compteur = config.get("compteur", {})
limite = compteur.get("limite", 400) conso = config.get("consommables", {})
prises = compteur.get("photos_prises", 0) prises = compteur.get("photos_prises", 0)
papier_rest = max(0, conso.get("papier_capacite", 400) - conso.get("papier_utilise", 0))
ruban_rest = max(0, round(conso.get("ruban_capacite", 400) - conso.get("ruban_utilise", 0), 1))
restantes = int(min(papier_rest, ruban_rest))
limite_event = compteur.get("limite", 0)
if compteur.get("actif") and limite_event > 0:
restantes = min(restantes, max(0, limite_event - prises))
papier_cap = conso.get("papier_capacite", 400)
ruban_cap = conso.get("ruban_capacite", 400)
capacite_conso = int(min(papier_cap, ruban_cap))
return { return {
"actif": compteur.get("actif", False), "actif": compteur.get("actif", False),
"limite": limite, "limite": limite_event,
"photos_prises": prises, "photos_prises": prises,
"restantes": max(0, limite - prises), "restantes": restantes,
"capacite": capacite_conso,
"papier_restant": papier_rest,
"ruban_restant": int(ruban_rest),
} }

View File

@@ -28,7 +28,7 @@ SEUIL_SOMBRE = 45
SEUIL_CORRECT = 65 SEUIL_CORRECT = 65
_dernier_analyse: float = 0 _dernier_analyse: float = 0
_INTERVALLE = 3.0 _INTERVALLE = 1.0
_dernier_resultat: dict | None = None _dernier_resultat: dict | None = None

View File

@@ -2,6 +2,7 @@ import json
import logging import logging
import shutil import shutil
import uuid import uuid
from datetime import date, datetime
from pathlib import Path from pathlib import Path
from backend.config import RACINE, DOSSIER_CADRES, FORMATS_CADRES, charger_config, mettre_a_jour_config from backend.config import RACINE, DOSSIER_CADRES, FORMATS_CADRES, charger_config, mettre_a_jour_config
@@ -46,6 +47,8 @@ def creer_evenement(nom: str, **kwargs) -> dict:
"couleur_secondaire": kwargs.get("couleur_secondaire", "#ffffff"), "couleur_secondaire": kwargs.get("couleur_secondaire", "#ffffff"),
"media_accueil": kwargs.get("media_accueil"), "media_accueil": kwargs.get("media_accueil"),
"formats_actifs": kwargs.get("formats_actifs", ["10x15", "15x20", "strip"]), "formats_actifs": kwargs.get("formats_actifs", ["10x15", "15x20", "strip"]),
"date_fin": kwargs.get("date_fin"),
"termine": False,
"cadres": {}, "cadres": {},
} }
_chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8") _chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8")
@@ -86,6 +89,38 @@ def supprimer_evenement(event_id: str) -> bool:
return True return True
def terminer_evenement(event_id: str) -> dict | None:
event = obtenir_evenement(event_id)
if not event:
return None
event["termine"] = True
_chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8")
log.info(f"Evenement termine : {event['nom']} ({event_id})")
return event
def evenement_est_termine() -> bool:
"""Verifie si l'evenement actif est termine (manuellement ou date_fin depassee)."""
config = charger_config()
event_id = config.get("evenement", {}).get("event_id")
if not event_id:
return False
event = obtenir_evenement(event_id)
if not event:
return False
if event.get("termine"):
return True
date_fin = event.get("date_fin")
if date_fin:
try:
fin = datetime.strptime(date_fin, "%Y-%m-%d").date()
if date.today() > fin:
return True
except ValueError:
pass
return False
def activer_evenement(event_id: str) -> dict | None: def activer_evenement(event_id: str) -> dict | None:
event = obtenir_evenement(event_id) event = obtenir_evenement(event_id)
if not event: if not event:

File diff suppressed because it is too large Load Diff

View File

@@ -249,7 +249,7 @@ def imprimer(
conf_imp = config.get("impression", {}) conf_imp = config.get("impression", {})
if imprimante is None: if imprimante is None:
imprimante = conf_imp.get("imprimante", "Mitsubishi") imprimante = conf_imp.get("imprimante") or "Mitsubishi"
if copies is None: if copies is None:
copies = conf_imp.get("copies", 1) copies = conf_imp.get("copies", 1)
if format_papier is None: if format_papier is None:

View File

@@ -26,7 +26,8 @@ PID = 0x2007
CANAUX = { CANAUX = {
"projecteur_gauche": {"relay": 1, "mode": "NO"}, "projecteur_gauche": {"relay": 1, "mode": "NO"},
"projecteur_droit": {"relay": 2, "mode": "NO"}, "projecteur_droit": {"relay": 2, "mode": "NO"},
"canon": {"relay": 3, "mode": "NF"}, "canon": {"relay": 3, "mode": "NF"}, # dummy battery
"canon_usb": {"relay": 4, "mode": "NF"}, # VBUS USB
} }
_lib = None _lib = None
@@ -115,12 +116,13 @@ def status():
rbuf = (ctypes.c_ubyte * 64)() rbuf = (ctypes.c_ubyte * 64)()
with _lock: with _lock:
n = _lib.hid_read_timeout(_dev, rbuf, 64, 500) n = _lib.hid_read_timeout(_dev, rbuf, 64, 500)
if n < 4: if n < 5:
return None return None
return { return {
"relay_1": rbuf[1] == 1, "relay_1": rbuf[1] == 1,
"relay_2": rbuf[2] == 1, "relay_2": rbuf[2] == 1,
"relay_3": rbuf[3] == 1, "relay_3": rbuf[3] == 1,
"relay_4": rbuf[4] == 1,
} }
@@ -144,12 +146,30 @@ def projecteurs(on: bool):
log.info(f"Projecteurs {'allumés' if on else 'éteints'}") log.info(f"Projecteurs {'allumés' if on else 'éteints'}")
def power_cycle_canon(duree: float = 3.0): def power_cycle_canon(duree: float = 5.0):
log.info(f"Power-cycle Canon ({duree}s)") """Cycle complet R3+R4 : coupe dummy battery + VBUS, reboot Canon.
_relay_set(3, True) # NF : ON = coupé Résistance de décharge 470Ω sur le 8V → condensateurs vidés en ~3s."""
time.sleep(duree) log.info(f"Power-cycle Canon R3+R4 ({duree}s)")
_relay_set(3, False) # NF : OFF = alimenté _relay_set(3, True) # R3 ON = coupe dummy battery (NF)
log.info("Canon ré-alimenté") _relay_set(4, True) # R4 ON = coupe VBUS USB (NF)
time.sleep(duree) # résistance vide les caps en ~3s
_relay_set(3, False) # R3 OFF = dummy battery → Canon boot
time.sleep(15) # attente boot Canon
_relay_set(4, False) # R4 OFF = VBUS → USB enumeration
log.info("Canon ré-alimenté — attente USB enumeration")
def hard_reset_canon(duree: float = 10.0):
"""Reset prolongé pour freeze PTP sévère.
Résistance de décharge 470Ω → 5s suffisent, mais on garde une marge."""
log.warning(f"Hard reset Canon — coupure R3+R4 ({duree}s)")
_relay_set(3, True) # R3 ON = coupe dummy battery
_relay_set(4, True) # R4 ON = coupe VBUS
time.sleep(duree) # vidange complète avec résistance
_relay_set(3, False) # R3 OFF = dummy battery → Canon boot
time.sleep(15) # attente boot Canon
_relay_set(4, False) # R4 OFF = VBUS → USB enumeration
log.info("Hard reset Canon terminé — attente démarrage")
def etat_complet(): def etat_complet():

View File

@@ -2246,6 +2246,38 @@ h3 {
cursor: pointer; cursor: pointer;
} }
/* === Diagnostic Panel === */
.diag-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:.8rem}
.diag-card{background:rgba(255,255,255,.06);border-radius:12px;padding:1rem;text-align:center;border:2px solid transparent;transition:border-color .3s}
.diag-card.ok{border-color:#4caf50}
.diag-card.warn{border-color:#ff9800}
.diag-card.err{border-color:#f44336}
.diag-card.off{border-color:#666}
.diag-icon{font-size:2rem;margin-bottom:.3rem}
.diag-label{font-size:.85rem;color:var(--texte-secondaire)}
.diag-status{font-size:.75rem;margin-top:.3rem;font-weight:600}
.diag-card.ok .diag-status{color:#4caf50}
.diag-card.warn .diag-status{color:#ff9800}
.diag-card.err .diag-status{color:#f44336}
.diag-card.off .diag-status{color:#888}
.diag-actions{display:flex;flex-wrap:wrap;gap:.6rem}
.diag-actions button{flex:1;min-width:140px;font-size:.85rem}
.diag-logs{font-family:monospace;font-size:.65rem;background:#0a0a0a;color:#0f0;padding:.8rem;border-radius:8px;white-space:pre-wrap;max-height:350px;overflow-y:auto;word-break:break-all;line-height:1.4}
.diag-usb{font-family:monospace;font-size:.75rem;background:#0a0a0a;color:#ccc;padding:.6rem;border-radius:6px;white-space:pre-wrap}
/* === Camera/Printer error overlay (user-facing, full-screen) === */
.erreur-overlay{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.92);display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:2rem;gap:1.5rem}
.erreur-overlay .erreur-icon{font-size:5rem}
.erreur-overlay .erreur-titre{font-size:2rem;font-weight:700;color:#fff}
.erreur-overlay .erreur-msg{font-size:1.3rem;color:#ccc;max-width:600px;line-height:1.5}
.erreur-overlay .erreur-steps{text-align:left;font-size:1.1rem;color:#eee;max-width:500px;line-height:2}
.erreur-overlay .erreur-steps li{margin-bottom:.5rem}
.erreur-overlay .erreur-spinner{width:60px;height:60px;border:5px solid #333;border-top-color:var(--primaire);border-radius:50%;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.erreur-overlay .erreur-btn{padding:1rem 2.5rem;font-size:1.3rem;border-radius:1rem;border:none;cursor:pointer;margin-top:1rem}
.erreur-overlay .erreur-btn-primaire{background:var(--primaire);color:#fff}
.erreur-overlay .erreur-btn-secondaire{background:#333;color:#fff}
/* Responsive tactile */ /* Responsive tactile */
@media (max-width: 800px) { @media (max-width: 800px) {
.accueil-contenu h1 { font-size: 2.5rem; } .accueil-contenu h1 { font-size: 2.5rem; }

View File

@@ -7,7 +7,7 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<meta http-equiv="Content-Language" content="fr"> <meta http-equiv="Content-Language" content="fr">
<link rel="stylesheet" href="/css/style.css?v=14"> <link rel="stylesheet" href="/css/style.css?v=15">
<link rel="stylesheet" href="/css/themes.css?v=2"> <link rel="stylesheet" href="/css/themes.css?v=2">
</head> </head>
<body> <body>
@@ -20,12 +20,19 @@
<button onclick="document.getElementById('printer-erreur').classList.add('cache')">&#10005;</button> <button onclick="document.getElementById('printer-erreur').classList.add('cache')">&#10005;</button>
</div> </div>
<!-- Overlay erreur camera --> <!-- Overlay erreur equipement (camera/imprimante) -->
<div id="camera-erreur" class="camera-erreur cache"> <div id="erreur-equipement" class="erreur-overlay cache">
<div class="camera-erreur-icon">&#128247;</div> <div class="erreur-icon" id="erreur-equip-icon">&#128247;</div>
<p>Probleme de communication avec l'appareil photo</p> <div class="erreur-titre" id="erreur-equip-titre">Preparation en cours</div>
<span class="camera-erreur-sub">Reconnexion en cours...</span> <div class="erreur-msg" id="erreur-equip-msg">L'appareil photo se prepare, un instant...</div>
<button onclick="document.getElementById('camera-erreur').classList.add('cache'); afficherEcran('accueil')" style="margin-top:2rem;padding:1rem 2rem;font-size:1.5rem;border-radius:1rem;border:none;background:#e91e63;color:#fff;cursor:pointer;">Retour accueil</button> <div class="erreur-spinner" id="erreur-equip-spinner"></div>
<div class="erreur-steps cache" id="erreur-equip-steps"></div>
<div id="erreur-equip-actions" style="display:flex;gap:1rem;flex-wrap:wrap;justify-content:center">
<button class="erreur-btn erreur-btn-primaire cache" id="erreur-equip-btn-retry" onclick="erreurEquipRetry()">Reessayer</button>
<button class="erreur-btn erreur-btn-secondaire cache" id="erreur-equip-btn-video" onclick="erreurEquipVideo()">Voir l'aide video</button>
<button class="erreur-btn erreur-btn-secondaire cache" id="erreur-equip-btn-restart" onclick="erreurEquipRestart()">Redemarrer la borne</button>
<button class="erreur-btn erreur-btn-secondaire" id="erreur-equip-btn-accueil" onclick="fermerErreurEquipement()">Retour accueil</button>
</div>
</div> </div>
<!-- Ecran d'accueil --> <!-- Ecran d'accueil -->
@@ -385,6 +392,7 @@
<button class="onglet" data-onglet="videos-admin" onclick="chargerVideosAdmin()">Videos</button> <button class="onglet" data-onglet="videos-admin" onclick="chargerVideosAdmin()">Videos</button>
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive();demarrerEclairageLiveExt()">Eclairage</button> <button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive();demarrerEclairageLiveExt()">Eclairage</button>
<button class="onglet" data-onglet="wifi" onclick="chargerWifi()">WiFi</button> <button class="onglet" data-onglet="wifi" onclick="chargerWifi()">WiFi</button>
<button class="onglet" data-onglet="diagnostic" onclick="chargerDiagnostic()">Diagnostic</button>
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button> <button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
</div> </div>
<div class="admin-onglets cache" id="onglets-evenement"> <div class="admin-onglets cache" id="onglets-evenement">
@@ -466,9 +474,10 @@
<span id="admin-compteur-limite-display" class="compteur-nombre petit">400</span> <span id="admin-compteur-limite-display" class="compteur-nombre petit">400</span>
</div> </div>
<span class="compteur-label">photos restantes</span> <span class="compteur-label">photos restantes</span>
<span id="admin-compteur-detail" class="compteur-label" style="font-size:0.7em;opacity:0.7;margin-top:2px"></span>
</div> </div>
<div class="champ"> <div class="champ">
<label>Limite totale</label> <label>Limite evenement (0 = pas de limite)</label>
<input type="number" id="admin-compteur-limite" min="1" max="9999" value="400"> <input type="number" id="admin-compteur-limite" min="1" max="9999" value="400">
</div> </div>
<div class="champ-row"> <div class="champ-row">
@@ -693,7 +702,15 @@
<label>Couleur secondaire</label> <label>Couleur secondaire</label>
<input type="color" id="admin-couleur-secondaire" value="#ffffff"> <input type="color" id="admin-couleur-secondaire" value="#ffffff">
</div> </div>
<div class="champ">
<label>Date de fin (impression bloquee apres)</label>
<input type="date" id="admin-date-fin">
</div>
<div class="champ-row">
<button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button> <button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button>
<button class="btn-danger" id="btn-terminer-event" onclick="terminerEvenement()">Terminer l'evenement</button>
</div>
<div id="badge-event-termine" class="cache" style="background:#f44336;color:#fff;padding:8px 16px;border-radius:8px;text-align:center;font-weight:bold;margin-top:8px">Evenement termine — impression desactivee</div>
<div class="champ" id="lien-galerie-event" style="display:none"> <div class="champ" id="lien-galerie-event" style="display:none">
<label>Galerie en ligne</label> <label>Galerie en ligne</label>
@@ -896,6 +913,7 @@
<button class="btn-action btn-petit" onclick="relaisAction('projecteurs','on')">Projecteurs ON</button> <button class="btn-action btn-petit" onclick="relaisAction('projecteurs','on')">Projecteurs ON</button>
<button class="btn-secondaire btn-petit" onclick="relaisAction('projecteurs','off')">Projecteurs OFF</button> <button class="btn-secondaire btn-petit" onclick="relaisAction('projecteurs','off')">Projecteurs OFF</button>
<button class="btn-danger btn-petit" onclick="relaisAction('canon/power-cycle','on')">Power-cycle Canon</button> <button class="btn-danger btn-petit" onclick="relaisAction('canon/power-cycle','on')">Power-cycle Canon</button>
<button class="btn-danger btn-petit" style="background:#b71c1c" onclick="relaisAction('canon/hard-reset','on')">Hard Reset Canon</button>
</div> </div>
<h4>Eclairage automatique</h4> <h4>Eclairage automatique</h4>
<div class="champ"> <div class="champ">
@@ -916,9 +934,57 @@
<input type="number" id="relais-seuil-off" min="0" max="100" value="65" step="5"> <input type="number" id="relais-seuil-off" min="0" max="100" value="65" step="5">
<span style="font-size:0.75rem;color:#888">Projecteurs s'eteignent</span> <span style="font-size:0.75rem;color:#888">Projecteurs s'eteignent</span>
</div> </div>
<h4 style="margin-top:1.5rem">Veille</h4>
<div class="champ">
<label>Eteindre les projecteurs apres (minutes d'inactivite)</label>
<input type="number" id="relais-veille-delai" min="0" max="60" value="0" step="1">
<span style="font-size:0.75rem;color:#888">0 = jamais. Les projecteurs se rallument quand un utilisateur touche l'ecran</span>
</div>
<button class="btn-action" onclick="sauvegarderRelaisConfig()">Sauvegarder</button> <button class="btn-action" onclick="sauvegarderRelaisConfig()">Sauvegarder</button>
</div> </div>
<!-- Panneau Diagnostic -->
<div class="admin-panneau" id="panneau-diagnostic">
<h3>Sante du systeme</h3>
<div class="diag-cards" id="diag-sante">
<div class="diag-card" id="diag-camera">
<div class="diag-icon">&#128247;</div>
<div class="diag-label">Camera</div>
<div class="diag-status" id="diag-camera-status">--</div>
</div>
<div class="diag-card" id="diag-imprimante">
<div class="diag-icon">&#128424;</div>
<div class="diag-label">Imprimante</div>
<div class="diag-status" id="diag-imp-status">--</div>
</div>
<div class="diag-card" id="diag-relais">
<div class="diag-icon">&#128268;</div>
<div class="diag-label">Relais</div>
<div class="diag-status" id="diag-relais-status">--</div>
</div>
<div class="diag-card" id="diag-systeme">
<div class="diag-icon">&#128187;</div>
<div class="diag-label">Systeme</div>
<div class="diag-status" id="diag-sys-status">--</div>
</div>
</div>
<h3 style="margin-top:1.5rem">Actions</h3>
<div class="diag-actions">
<button class="btn-action" onclick="diagReconnecterCamera()">&#128247; Reconnecter camera</button>
<button class="btn-action" onclick="diagReactiverImprimante()">&#128424; Reactiver imprimante</button>
<button class="btn-secondaire" onclick="diagRedemarrerBackend()">&#8635; Redemarrer backend</button>
<button class="btn-secondaire" onclick="diagRefreshChromium()">&#127760; Recharger navigateur</button>
<button class="btn-danger" onclick="if(confirm('Redemarrer la borne ?'))diagRedemarrerSysteme()">&#9888; Redemarrer la borne</button>
</div>
<h3 style="margin-top:1.5rem">Logs backend <button class="btn-secondaire btn-petit" onclick="chargerLogs()" style="margin-left:0.5rem">&#8635; Actualiser</button></h3>
<div id="diag-logs" class="diag-logs">Appuyer sur Actualiser...</div>
<h3 style="margin-top:1.5rem">Peripheriques USB <button class="btn-secondaire btn-petit" onclick="chargerUsb()" style="margin-left:0.5rem">&#8635;</button></h3>
<div id="diag-usb" class="diag-usb">--</div>
</div>
<div class="admin-panneau" id="panneau-infos"> <div class="admin-panneau" id="panneau-infos">
<div class="infos-systeme-header"> <div class="infos-systeme-header">
<h3>Informations systeme</h3> <h3>Informations systeme</h3>
@@ -1079,11 +1145,11 @@
</style> </style>
<script src="/js/websocket.js?v=16"></script> <script src="/js/websocket.js?v=16"></script>
<script src="/js/app.js?v=19"></script> <script src="/js/app.js?v=24"></script>
<script src="/js/camera.js?v=18"></script> <script src="/js/camera.js?v=20"></script>
<script src="/js/effects.js?v=4"></script> <script src="/js/effects.js?v=4"></script>
<script src="/js/gallery.js?v=4"></script> <script src="/js/gallery.js?v=5"></script>
<script src="/js/share.js?v=8"></script> <script src="/js/share.js?v=9"></script>
<script src="/js/admin.js?v=13"></script> <script src="/js/admin.js?v=17"></script>
</body> </body>
</html> </html>

View File

@@ -162,8 +162,11 @@ async function chargerCompteur() {
const etat = await apiGet('/api/compteur'); const etat = await apiGet('/api/compteur');
document.getElementById('tog-compteur-actif').checked = etat.actif; document.getElementById('tog-compteur-actif').checked = etat.actif;
document.getElementById('admin-compteur-restant').textContent = etat.restantes; document.getElementById('admin-compteur-restant').textContent = etat.restantes;
document.getElementById('admin-compteur-limite-display').textContent = etat.limite; const limiteAff = (etat.actif && etat.limite > 0) ? etat.limite : etat.capacite;
document.getElementById('admin-compteur-limite-display').textContent = limiteAff;
setValue('admin-compteur-limite', etat.limite); setValue('admin-compteur-limite', etat.limite);
const detail = document.getElementById('admin-compteur-detail');
if (detail) detail.textContent = `Papier: ${etat.papier_restant} | Ruban: ${etat.ruban_restant}`;
} }
async function sauvegarderCompteur() { async function sauvegarderCompteur() {
@@ -713,10 +716,14 @@ async function chargerListeEvenements() {
for (const ev of events) { for (const ev of events) {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'event-item' + (ev.id === actifId ? ' event-actif' : ''); div.className = 'event-item' + (ev.id === actifId ? ' event-actif' : '');
const badges = [];
if (ev.id === actifId) badges.push('<span class="badge-actif">Actif</span>');
if (ev.termine) badges.push('<span style="background:#f44336;color:#fff;padding:2px 6px;border-radius:4px;font-size:0.7em">Termine</span>');
div.innerHTML = ` div.innerHTML = `
<span class="event-nom">${ev.nom}</span> <span class="event-nom">${ev.nom}${ev.date_fin ? ' <small style="opacity:0.5">(' + ev.date_fin + ')</small>' : ''}</span>
<span class="event-actions"> <span class="event-actions">
${ev.id !== actifId ? `<button class="btn-secondaire btn-petit" onclick="activerEventAdmin('${ev.id}')">Activer</button>` : '<span class="badge-actif">Actif</span>'} ${ev.id !== actifId ? `<button class="btn-secondaire btn-petit" onclick="activerEventAdmin('${ev.id}')">Activer</button>` : ''}
${badges.join(' ')}
<button class="btn-secondaire btn-petit" onclick="editerEventAdmin('${ev.id}')">Editer</button> <button class="btn-secondaire btn-petit" onclick="editerEventAdmin('${ev.id}')">Editer</button>
<button class="btn-danger btn-petit" onclick="supprimerEventAdmin('${ev.id}','${ev.nom}')">Suppr</button> <button class="btn-danger btn-petit" onclick="supprimerEventAdmin('${ev.id}','${ev.nom}')">Suppr</button>
</span>`; </span>`;
@@ -760,6 +767,7 @@ function afficherDetailEvent(event) {
setValue('admin-nom-event', event.nom); setValue('admin-nom-event', event.nom);
setValue('admin-couleur-primaire', event.couleur_primaire || '#e91e63'); setValue('admin-couleur-primaire', event.couleur_primaire || '#e91e63');
setValue('admin-couleur-secondaire', event.couleur_secondaire || '#ffffff'); setValue('admin-couleur-secondaire', event.couleur_secondaire || '#ffffff');
setValue('admin-date-fin', event.date_fin || '');
rafraichirMediaAccueil().then(() => { rafraichirMediaAccueil().then(() => {
const select = document.getElementById('admin-media-accueil'); const select = document.getElementById('admin-media-accueil');
if (event.media_accueil) select.value = event.media_accueil; if (event.media_accueil) select.value = event.media_accueil;
@@ -770,6 +778,17 @@ function afficherDetailEvent(event) {
document.getElementById('tog-event-fmt-15x20').checked = fmts.includes('15x20'); document.getElementById('tog-event-fmt-15x20').checked = fmts.includes('15x20');
document.getElementById('tog-event-fmt-strip').checked = fmts.includes('strip'); document.getElementById('tog-event-fmt-strip').checked = fmts.includes('strip');
// Statut termine
const btnTerminer = document.getElementById('btn-terminer-event');
const badgeTermine = document.getElementById('badge-event-termine');
if (event.termine) {
btnTerminer.classList.add('cache');
badgeTermine.classList.remove('cache');
} else {
btnTerminer.classList.remove('cache');
badgeTermine.classList.add('cache');
}
// Lien galerie en ligne // Lien galerie en ligne
const booth = config.booth || {}; const booth = config.booth || {};
const lienBloc = document.getElementById('lien-galerie-event'); const lienBloc = document.getElementById('lien-galerie-event');
@@ -787,6 +806,17 @@ function afficherDetailEvent(event) {
chargerCadresEvent(); chargerCadresEvent();
} }
async function terminerEvenement() {
if (!eventEditId) return;
if (!confirm('Terminer cet evenement ? L\'impression sera desactivee.')) return;
await apiPost(`/api/evenements/${eventEditId}/terminer`);
config = await apiGet('/api/config');
const event = await apiGet(`/api/evenements/${eventEditId}`);
afficherDetailEvent(event);
await chargerListeEvenements();
afficherStatut('Evenement termine — impression desactivee', 'succes');
}
async function supprimerEventAdmin(id, nom) { async function supprimerEventAdmin(id, nom) {
if (!confirm(`Supprimer l'evenement "${nom}" et ses cadres ?`)) return; if (!confirm(`Supprimer l'evenement "${nom}" et ses cadres ?`)) return;
await fetch(`/api/evenements/${id}`, { method: 'DELETE' }); await fetch(`/api/evenements/${id}`, { method: 'DELETE' });
@@ -856,6 +886,7 @@ async function sauvegarderEvenement() {
couleur_primaire: getValue('admin-couleur-primaire'), couleur_primaire: getValue('admin-couleur-primaire'),
couleur_secondaire: getValue('admin-couleur-secondaire'), couleur_secondaire: getValue('admin-couleur-secondaire'),
media_accueil: getValue('admin-media-accueil') || null, media_accueil: getValue('admin-media-accueil') || null,
date_fin: getValue('admin-date-fin') || null,
formats_actifs, formats_actifs,
}; };
@@ -1743,6 +1774,41 @@ function fermerWizard() {
_wizardSituation = null; _wizardSituation = null;
_wizardEtapes = []; _wizardEtapes = [];
_wizardIndex = 0; _wizardIndex = 0;
_arreterAutoDetect();
}
let _wizardAutoDetectTimer = null;
let _wizardAutoDetectType = null;
function lancerWizardSituation(situationId, autoDetectType) {
_wizardAutoDetectType = autoDetectType || null;
lancerWizard(situationId);
if (_wizardAutoDetectType) {
_demarrerAutoDetect();
}
}
function _demarrerAutoDetect() {
if (_wizardAutoDetectTimer) clearInterval(_wizardAutoDetectTimer);
_wizardAutoDetectTimer = setInterval(async () => {
try {
const s = await apiGet('/api/systeme/sante');
let resolu = false;
if (_wizardAutoDetectType === 'camera' && s.camera.status === 'ok') resolu = true;
if (_wizardAutoDetectType === 'imprimante' && s.imprimante.status === 'prete') resolu = true;
if (resolu) {
_arreterAutoDetect();
fermerWizard();
afficherStatut('Probleme resolu !', 'succes');
if (typeof fermerErreurEquipement === 'function') fermerErreurEquipement();
}
} catch (e) {}
}, 5000);
}
function _arreterAutoDetect() {
if (_wizardAutoDetectTimer) { clearInterval(_wizardAutoDetectTimer); _wizardAutoDetectTimer = null; }
_wizardAutoDetectType = null;
} }
function lancerWizardDepuisAdmin() { function lancerWizardDepuisAdmin() {
@@ -1857,6 +1923,8 @@ async function chargerRelaisConfig() {
if (s2) s2.value = data.seuil_proj2 ?? 30; if (s2) s2.value = data.seuil_proj2 ?? 30;
const soff = document.getElementById('relais-seuil-off'); const soff = document.getElementById('relais-seuil-off');
if (soff) soff.value = data.seuil_off ?? 65; if (soff) soff.value = data.seuil_off ?? 65;
const vd = document.getElementById('relais-veille-delai');
if (vd) vd.value = data.veille_delai ?? 0;
} catch (e) { } catch (e) {
console.warn('Erreur relais config:', e); console.warn('Erreur relais config:', e);
} }
@@ -1867,13 +1935,20 @@ async function sauvegarderRelaisConfig() {
const seuil_proj1 = parseInt(document.getElementById('relais-seuil-proj1')?.value) || 45; const seuil_proj1 = parseInt(document.getElementById('relais-seuil-proj1')?.value) || 45;
const seuil_proj2 = parseInt(document.getElementById('relais-seuil-proj2')?.value) || 30; const seuil_proj2 = parseInt(document.getElementById('relais-seuil-proj2')?.value) || 30;
const seuil_off = parseInt(document.getElementById('relais-seuil-off')?.value) || 65; const seuil_off = parseInt(document.getElementById('relais-seuil-off')?.value) || 65;
await apiPost('/api/relais/config', { eclairage_auto, seuil_proj1, seuil_proj2, seuil_off }); const veille_delai = parseInt(document.getElementById('relais-veille-delai')?.value) || 0;
await apiPost('/api/relais/config', { eclairage_auto, seuil_proj1, seuil_proj2, seuil_off, veille_delai });
afficherStatut('Config relais sauvegardee', 'succes'); afficherStatut('Config relais sauvegardee', 'succes');
} }
async function relaisAction(nom, action) { async function relaisAction(nom, action) {
try { try {
if (nom === 'canon/power-cycle') { if (nom === 'canon/hard-reset') {
await fetch('/api/relais/canon/hard-reset', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({duree: 5})
});
afficherStatut('Hard reset Canon lance (trappe + alim)', 'succes');
} else if (nom === 'canon/power-cycle') {
await fetch('/api/relais/canon/power-cycle', { await fetch('/api/relais/canon/power-cycle', {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({duree: 3}) body: JSON.stringify({duree: 3})
@@ -1893,3 +1968,147 @@ function demarrerEclairageLiveExt() {
chargerRelaisStatus(); chargerRelaisStatus();
chargerRelaisConfig(); chargerRelaisConfig();
} }
// === DIAGNOSTIC ===
let _diagRefreshTimer = null;
async function chargerDiagnostic() {
chargerSante();
chargerLogs();
chargerUsb();
if (_diagRefreshTimer) clearInterval(_diagRefreshTimer);
_diagRefreshTimer = setInterval(chargerSante, 10000);
}
async function chargerSante() {
try {
const s = await apiGet('/api/systeme/sante');
// Camera
const camCard = document.getElementById('diag-camera');
const camSt = document.getElementById('diag-camera-status');
if (s.camera.status === 'ok') {
camCard.className = 'diag-card ok';
camSt.textContent = 'Connectee — ' + s.camera.mode;
} else if (s.camera.usb) {
camCard.className = 'diag-card warn';
camSt.textContent = 'USB detecte mais non connectee';
} else {
camCard.className = 'diag-card err';
camSt.textContent = s.camera.status === 'erreur' ? 'Non disponible' : s.camera.mode;
}
// Imprimante
const impCard = document.getElementById('diag-imprimante');
const impSt = document.getElementById('diag-imp-status');
if (s.imprimante.status === 'prete') {
impCard.className = 'diag-card ok';
impSt.textContent = s.imprimante.nom + ' — Prete';
} else if (s.imprimante.status === 'impression') {
impCard.className = 'diag-card ok';
impSt.textContent = 'En cours d\'impression';
} else if (s.imprimante.status === 'arretee') {
impCard.className = 'diag-card err';
impSt.textContent = s.imprimante.nom + ' — Arretee' + (s.imprimante.usb ? ' (USB ok)' : ' (USB absente)');
} else {
impCard.className = 'diag-card ' + (s.imprimante.usb ? 'warn' : 'off');
impSt.textContent = s.imprimante.usb ? 'USB detectee — CUPS inconnu' : 'Non detectee';
}
if (s.imprimante.jobs > 0) impSt.textContent += ' — ' + s.imprimante.jobs + ' job(s)';
// Relais
const relCard = document.getElementById('diag-relais');
const relSt = document.getElementById('diag-relais-status');
if (s.relais.connecte) {
relCard.className = 'diag-card ok';
relSt.textContent = 'Connecte';
} else {
relCard.className = 'diag-card off';
relSt.textContent = 'Non detecte';
}
// Systeme
const sysCard = document.getElementById('diag-systeme');
const sysSt = document.getElementById('diag-sys-status');
const ramWarn = s.systeme.ram_pct > 85;
const diskWarn = s.systeme.disque_pct > 90;
const tempWarn = s.systeme.temp && s.systeme.temp > 75;
if (ramWarn || diskWarn || tempWarn) {
sysCard.className = 'diag-card warn';
} else {
sysCard.className = 'diag-card ok';
}
let sysInfo = 'RAM ' + s.systeme.ram_pct + '% — Disque ' + s.systeme.disque_pct + '%';
if (s.systeme.temp) sysInfo += ' — ' + s.systeme.temp + '°C';
if (s.systeme.uptime) sysInfo += ' — Up ' + s.systeme.uptime;
sysSt.textContent = sysInfo;
} catch (e) {
console.error('Diagnostic sante:', e);
}
}
async function chargerLogs() {
try {
const r = await apiGet('/api/systeme/logs?n=80');
const el = document.getElementById('diag-logs');
el.textContent = r.lignes.join('\n');
el.scrollTop = el.scrollHeight;
} catch (e) {
document.getElementById('diag-logs').textContent = 'Erreur chargement logs';
}
}
async function chargerUsb() {
try {
const r = await apiGet('/api/systeme/usb');
const el = document.getElementById('diag-usb');
el.textContent = r.peripheriques.join('\n');
} catch (e) {
document.getElementById('diag-usb').textContent = 'Erreur';
}
}
async function diagReconnecterCamera() {
afficherStatut('Reconnexion camera...', 'succes');
try {
await apiPost('/api/camera/reconnecter', {});
await chargerSante();
afficherStatut('Reconnexion terminee', 'succes');
} catch (e) {
afficherStatut('Echec reconnexion', 'erreur');
}
}
async function diagReactiverImprimante() {
afficherStatut('Reactivation imprimante...', 'succes');
try {
const r = await apiPost('/api/imprimante/reactiver', {});
afficherStatut(r.message || 'OK', r.succes ? 'succes' : 'erreur');
await chargerSante();
} catch (e) {
afficherStatut('Echec reactivation', 'erreur');
}
}
async function diagRedemarrerBackend() {
afficherStatut('Redemarrage backend...', 'succes');
try {
await fetch('/api/systeme/redemarrer-backend', { method: 'POST' });
afficherStatut('Backend redemarre — rechargement dans 5s...', 'succes');
setTimeout(() => location.reload(), 5000);
} catch (e) {
afficherStatut('Erreur', 'erreur');
}
}
async function diagRefreshChromium() {
location.reload();
}
async function diagRedemarrerSysteme() {
afficherStatut('Redemarrage borne...', 'succes');
try {
await fetch('/api/systeme/redemarrer', { method: 'POST' });
} catch (e) {}
}

View File

@@ -5,6 +5,11 @@ let modeActuel = 'simple';
let photosSession = []; // Photos de la session en cours let photosSession = []; // Photos de la session en cours
let photoFinale = null; // Photo finale (avec effets) let photoFinale = null; // Photo finale (avec effets)
let ecranActuel = 'accueil'; let ecranActuel = 'accueil';
let _veilleTimer = null;
let _enVeille = false;
let _spotsAllumes = false;
const _shutterSound = new Audio('/sounds/shutter.wav');
_shutterSound.volume = 0.8;
// --- Initialisation --- // --- Initialisation ---
@@ -113,7 +118,13 @@ function allerA(ecran) {
photoFinale = null; photoFinale = null;
arreterPreview(); arreterPreview();
majCompteurAccueil(); majCompteurAccueil();
} else if (ecran === 'capture') { eteindreSpots();
lancerTimerVeille();
} else {
arreterTimerVeille();
reveillerEclairage();
}
if (ecran === 'capture') {
lancerCapture(); lancerCapture();
} else if (ecran === 'partage') { } else if (ecran === 'partage') {
majBoutonImprimerCompteur(); majBoutonImprimerCompteur();
@@ -145,6 +156,7 @@ function setupEcranAccueil() {
accueil.addEventListener('click', (e) => { accueil.addEventListener('click', (e) => {
if (e.target.closest('.btn-admin')) return; if (e.target.closest('.btn-admin')) return;
if (e.target.closest('.btn-photostation')) return; if (e.target.closest('.btn-photostation')) return;
if (_enVeille) reveillerEclairage();
allerA('mode'); allerA('mode');
}); });
} }
@@ -388,24 +400,143 @@ function afficherStatut(message, type = 'succes') {
wsOnMessage('config_maj', (msg) => { wsOnMessage('config_maj', (msg) => {
config = msg.config; config = msg.config;
appliquerConfig(); appliquerConfig();
if (ecranActuel === 'accueil') lancerTimerVeille();
}); });
// Erreur camera : overlay visible sur tous les ecrans, reconnexion automatique cote backend // === Erreur equipement : overlay intelligent avec escalade ===
let _erreurCameraTimer = null; let _erreurCameraTimer = null;
wsOnMessage('camera_erreur', () => { let _erreurCameraCount = 0;
let _erreurCameraPhase = 0; // 0=auto-fix, 1=patience, 2=operator, 3=restart
function _showErreurEquipement(icon, titre, msg, phase) {
const el = document.getElementById('erreur-equipement');
document.getElementById('erreur-equip-icon').textContent = icon;
document.getElementById('erreur-equip-titre').textContent = titre;
document.getElementById('erreur-equip-msg').textContent = msg;
const spinner = document.getElementById('erreur-equip-spinner');
const steps = document.getElementById('erreur-equip-steps');
const btnRetry = document.getElementById('erreur-equip-btn-retry');
const btnVideo = document.getElementById('erreur-equip-btn-video');
const btnRestart = document.getElementById('erreur-equip-btn-restart');
spinner.classList.toggle('cache', phase >= 2);
steps.classList.toggle('cache', phase < 2);
btnRetry.classList.toggle('cache', phase < 1);
btnVideo.classList.toggle('cache', phase < 2);
btnRestart.classList.toggle('cache', phase < 2);
if (phase >= 2) {
steps.innerHTML = '<ol>' +
'<li>Verifiez que l\'appareil photo est allume (bouton ON)</li>' +
'<li>Verifiez le cable USB entre l\'appareil et la borne</li>' +
'<li>Eteignez et rallumez l\'appareil photo</li>' +
'<li>Si le probleme persiste, redemarrez la borne</li>' +
'</ol>';
}
el.classList.remove('cache');
}
function fermerErreurEquipement() {
document.getElementById('erreur-equipement').classList.add('cache');
_erreurCameraCount = 0;
_erreurCameraPhase = 0;
allerA('accueil');
}
function erreurEquipRetry() {
document.getElementById('erreur-equip-titre').textContent = 'Verification en cours...';
document.getElementById('erreur-equip-msg').textContent = 'Un instant...';
document.getElementById('erreur-equip-spinner').classList.remove('cache');
document.getElementById('erreur-equip-steps').classList.add('cache');
document.getElementById('erreur-equip-btn-retry').classList.add('cache');
if (_erreurEquipType === 'imprimante') {
fetch('/api/imprimante/reactiver', { method: 'POST' }).catch(() => {});
} else {
fetch('/api/camera/reconnecter', { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' }).catch(() => {});
}
}
let _erreurEquipType = 'camera';
function erreurEquipVideo() {
document.getElementById('erreur-equipement').classList.add('cache');
const situationMap = { camera: 'camera_hs', imprimante: window._imprimanteErreurSituation || 'depannage_imprimante' };
const situation = situationMap[_erreurEquipType] || 'camera_hs';
if (typeof lancerWizardSituation === 'function') {
lancerWizardSituation(situation, _erreurEquipType);
}
}
function erreurEquipRestart() {
_showErreurEquipement('⏳', 'Redemarrage en cours', 'La borne redemarre, patientez 30 secondes...', 0);
document.getElementById('erreur-equip-btn-accueil').classList.add('cache');
fetch('/api/systeme/redemarrer', { method: 'POST' }).catch(() => {});
}
wsOnMessage('camera_erreur', (data) => {
if (window.location.pathname === '/admin') return; if (window.location.pathname === '/admin') return;
document.getElementById('camera-erreur').classList.remove('cache'); _erreurCameraCount++;
// Reload de dernier recours si la camera ne revient pas apres 2 min _erreurEquipType = 'camera';
if (_erreurCameraCount <= 2) {
_erreurCameraPhase = 0;
_showErreurEquipement('📷', 'Preparation de l\'appareil photo', 'Un instant, reconnexion automatique...', 0);
} else if (_erreurCameraCount <= 5) {
_erreurCameraPhase = 1;
_showErreurEquipement('📷', 'L\'appareil photo ne repond pas', 'Le systeme essaie de le reconnecter. Vous pouvez aussi reessayer manuellement.', 1);
} else {
_erreurCameraPhase = 2;
_showErreurEquipement('📷', 'Appareil photo injoignable', 'Suivez ces etapes pour le remettre en marche :', 2);
}
if (_erreurCameraTimer) clearTimeout(_erreurCameraTimer); if (_erreurCameraTimer) clearTimeout(_erreurCameraTimer);
_erreurCameraTimer = setTimeout(() => { location.reload(); }, 120000); _erreurCameraTimer = setTimeout(() => { location.reload(); }, 180000);
});
wsOnMessage('shutter', () => {
_shutterSound.currentTime = 0;
_shutterSound.play().catch(() => {});
}); });
wsOnMessage('camera_ok', () => { wsOnMessage('camera_ok', () => {
document.getElementById('camera-erreur').classList.add('cache'); document.getElementById('erreur-equipement').classList.add('cache');
_erreurCameraCount = 0;
_erreurCameraPhase = 0;
if (_erreurCameraTimer) { clearTimeout(_erreurCameraTimer); _erreurCameraTimer = null; } if (_erreurCameraTimer) { clearTimeout(_erreurCameraTimer); _erreurCameraTimer = null; }
}); });
// Surveillance imprimante — poll toutes les 30s wsOnMessage('imprimante_erreur', (data) => {
if (window.location.pathname === '/admin') return;
_erreurEquipType = 'imprimante';
const titre = data.titre || 'Probleme imprimante';
const msg = data.message || 'L\'imprimante ne fonctionne pas correctement.';
const etapes = data.etapes || [];
const situation = data.situation || 'depannage_imprimante';
_showErreurEquipement('🖨', titre, msg, 2);
const steps = document.getElementById('erreur-equip-steps');
if (etapes.length) {
steps.innerHTML = '<ol>' + etapes.map(e => '<li>' + e + '</li>').join('') + '</ol>';
steps.classList.remove('cache');
}
document.getElementById('erreur-equip-btn-retry').classList.remove('cache');
document.getElementById('erreur-equip-btn-retry').textContent = 'Verifier';
document.getElementById('erreur-equip-btn-retry').onclick = function() {
fetch('/api/imprimante/reactiver', { method: 'POST' }).catch(() => {});
_showErreurEquipement('🖨', 'Verification en cours...', 'Un instant...', 0);
};
document.getElementById('erreur-equip-btn-video').classList.remove('cache');
document.getElementById('erreur-equip-btn-video').onclick = function() {
document.getElementById('erreur-equipement').classList.add('cache');
if (typeof lancerWizardSituation === 'function') lancerWizardSituation(situation, 'imprimante');
};
window._imprimanteErreurSituation = situation;
});
wsOnMessage('imprimante_ok', () => {
if (_erreurEquipType === 'imprimante') {
document.getElementById('erreur-equipement').classList.add('cache');
}
document.getElementById('printer-erreur').classList.add('cache');
});
function _afficherErreurImprimante(msg) { function _afficherErreurImprimante(msg) {
const el = document.getElementById('printer-erreur'); const el = document.getElementById('printer-erreur');
document.getElementById('printer-erreur-msg').textContent = msg; document.getElementById('printer-erreur-msg').textContent = msg;
@@ -416,21 +547,11 @@ async function _pollStatutImprimante() {
try { try {
const r = await apiGet('/api/imprimante/statut-detail'); const r = await apiGet('/api/imprimante/statut-detail');
const el = document.getElementById('printer-erreur'); const el = document.getElementById('printer-erreur');
if (r.derniere_erreur) { if (r.statut && !r.statut.includes('stopped') && !r.statut.includes('disabled')) {
let msg = '';
const e = r.derniere_erreur.toLowerCase();
if (e.includes('media') && e.includes('match'))
msg = '⚠ Imprimante : format papier incorrect — vérifiez la cassette';
else if (e.includes('jam'))
msg = '⚠ Imprimante : bourrage papier — retirez le papier bloqué';
else if (e.includes('cancel'))
msg = '⚠ Imprimante : job annulé — ' + r.derniere_erreur.split(']').pop().trim();
if (msg) { _afficherErreurImprimante(msg); return; }
}
if (r.statut && (r.statut.includes('stopped') || r.statut.includes('disabled'))) {
_afficherErreurImprimante('⚠ Imprimante arrêtée — utilisez le bouton Évacuer dans l\'admin');
} else {
el.classList.add('cache'); el.classList.add('cache');
if (_erreurEquipType === 'imprimante') {
document.getElementById('erreur-equipement').classList.add('cache');
}
} }
} catch(e) {} } catch(e) {}
} }
@@ -553,6 +674,36 @@ async function wizardTerminer() {
allerA('accueil'); allerA('accueil');
} }
// --- Veille eclairage ---
function lancerTimerVeille() {
arreterTimerVeille();
const delai = (config.relais?.veille_delai || 0) * 60000;
if (delai <= 0) return;
_veilleTimer = setTimeout(() => {
_enVeille = true;
_spotsAllumes = false;
fetch('/api/relais/veille', { method: 'POST' }).catch(() => {});
}, delai);
}
function arreterTimerVeille() {
if (_veilleTimer) { clearTimeout(_veilleTimer); _veilleTimer = null; }
}
function reveillerEclairage() {
if (!_enVeille && _spotsAllumes) return;
_enVeille = false;
_spotsAllumes = true;
arreterTimerVeille();
fetch('/api/relais/reveil', { method: 'POST' }).catch(() => {});
}
function eteindreSpots() {
_spotsAllumes = false;
fetch('/api/relais/veille', { method: 'POST' }).catch(() => {});
}
// Demarrage // Demarrage
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
init().then(() => { init().then(() => {

View File

@@ -272,6 +272,7 @@ async function lancerCapture() {
} }
lancerPreview(); // Miroir live pendant le compte à rebours lancerPreview(); // Miroir live pendant le compte à rebours
wsEnvoyer({type: 'prepare_capture'});
await compteARebours(); await compteARebours();
// Surprise : afficher media juste avant capture // Surprise : afficher media juste avant capture
@@ -489,20 +490,17 @@ function afficherPreviewPhoto(chemin) {
async function majCompteurAccueil() { async function majCompteurAccueil() {
const etat = await apiGet('/api/compteur'); const etat = await apiGet('/api/compteur');
const el = document.getElementById('compteur-accueil'); const el = document.getElementById('compteur-accueil');
if (etat.actif) {
el.classList.remove('cache'); el.classList.remove('cache');
document.getElementById('compteur-restant').textContent = etat.restantes; document.getElementById('compteur-restant').textContent = etat.restantes;
document.getElementById('compteur-limite').textContent = etat.limite; document.getElementById('compteur-limite').textContent =
} else { (etat.actif && etat.limite > 0) ? etat.limite : etat.capacite;
el.classList.add('cache');
}
} }
async function majBoutonImprimerCompteur() { async function majBoutonImprimerCompteur() {
const etat = await apiGet('/api/compteur'); const etat = await apiGet('/api/compteur');
const btn = document.getElementById('btn-imprimer'); const btn = document.getElementById('btn-imprimer');
if (!btn) return; if (!btn) return;
if (etat.actif && etat.restantes <= 0) { if (etat.restantes <= 0) {
btn.classList.add('cache'); btn.classList.add('cache');
} }
} }

View File

@@ -105,12 +105,18 @@ async function adminGalerieImprimer() {
if (noms.length === 0) return; if (noms.length === 0) return;
if (!confirm('Imprimer ' + noms.length + ' photo' + (noms.length > 1 ? 's' : '') + ' ?')) return; if (!confirm('Imprimer ' + noms.length + ' photo' + (noms.length > 1 ? 's' : '') + ' ?')) return;
let ok = 0, fail = 0; let ok = 0, fail = 0, termine = false;
for (const nom of noms) { for (const nom of noms) {
try { try {
const r = await apiPost('/api/imprimer', { photo: nom }); const r = await apiPost('/api/imprimer', { photo: nom });
if (r && r.succes) ok++; else fail++; if (r && r.succes) ok++;
else if (r && r.erreur === 'evenement_termine') { termine = true; break; }
else fail++;
} catch { fail++; } } catch { fail++; }
} }
if (termine) {
alert('Evenement termine — impression desactivee. Les photos ne seront pas imprimees.');
} else {
alert('Impression : ' + ok + ' OK' + (fail > 0 ? ', ' + fail + ' echec' : '')); alert('Impression : ' + ok + ' OK' + (fail > 0 ? ', ' + fail + ' echec' : ''));
} }
}

View File

@@ -1,10 +1,10 @@
/* Module partage - Impression, email, QR code */ /* Module partage - Impression, email, QR code */
let nbExemplaires = 1; let nbExemplaires = 1;
let copiesMax = 5; let copiesMax = 2;
async function ouvrirImpression() { async function ouvrirImpression() {
copiesMax = config.impression?.copies_max || 5; copiesMax = config.impression?.copies_max || 2;
nbExemplaires = 1; nbExemplaires = 1;
document.getElementById('nb-exemplaires').textContent = nbExemplaires; document.getElementById('nb-exemplaires').textContent = nbExemplaires;
cadreChoisi = null; cadreChoisi = null;
@@ -110,8 +110,14 @@ async function lancerImpression() {
cadre: cadreChoisi || undefined, cadre: cadreChoisi || undefined,
format_papier: formatImpression || undefined, format_papier: formatImpression || undefined,
}); });
if (resultat.succes) { if (resultat.attente) {
afficherStatutEco('⏳', resultat.message || 'Votre photo sortira avec la suivante !', 'attente-eco');
} else if (resultat.jumeau && resultat.succes) {
afficherStatutEco('🎁', 'Vous recevez 2 tirages identiques — gardez-en un, offrez l\'autre !', 'jumeau-eco');
} else if (resultat.succes) {
afficherStatut(`${nbExemplaires} exemplaire(s) envoye(s) a l'imprimante !`, 'succes'); afficherStatut(`${nbExemplaires} exemplaire(s) envoye(s) a l'imprimante !`, 'succes');
} else if (resultat.erreur === 'evenement_termine') {
afficherStatut('Evenement termine — impression indisponible', 'erreur');
} else { } else {
afficherStatut('Erreur d\'impression', 'erreur'); afficherStatut('Erreur d\'impression', 'erreur');
} }

View File

@@ -54,8 +54,8 @@ function _afficherPause() {
function _masquerPause() { function _masquerPause() {
const el = document.getElementById('ecran-pause'); const el = document.getElementById('ecran-pause');
if (el) { el.style.display = 'none'; } if (el) { el.style.display = 'none'; }
// Relancer le preview si on était sur l'accueil // Relancer le preview seulement si on est sur un ecran de prise de vue (pas l'accueil)
if (typeof lancerPreview === 'function' && document.getElementById('ecran-accueil')?.classList.contains('actif')) { if (typeof lancerPreview === 'function' && !document.getElementById('ecran-accueil')?.classList.contains('actif')) {
lancerPreview(); lancerPreview();
} }
} }

View File

@@ -244,14 +244,50 @@ Backup local des fichiers modifiés : `lxc111_backup/` dans ce dossier projet.
- Compteur papier, ruban et photos avec diagnostic ratio - Compteur papier, ruban et photos avec diagnostic ratio
- Compteur poses perdues + ratio corrigé 0.5 (10 poses par feuille, 20 feuilles par rouleau) - Compteur poses perdues + ratio corrigé 0.5 (10 poses par feuille, 20 feuilles par rouleau)
### Module relais HID + éclairage auto (1-2 août)
- Module LCUS 4 canaux (5131:2007) piloté via hidapi/ctypes (`backend/relais.py`)
- Assignation : R1=Proj gauche (NO), R2=Proj droit (NO), R3=Canon alim (NF), R4=libre
- udev rule `/etc/udev/rules.d/99-usbrelay.rules` pour accès sans sudo
- 5 endpoints API relais + admin éclairage avec status relais + boutons ON/OFF
- Éclairage auto : analyse luminosité visage → allume projecteurs selon 3 seuils configurables
- Power cycle Canon : relais NF = OFF=alimenté, ON=coupé ; intégré en niveau 10 de l'escalade recovery DSLR
- Bug fix : byte 4 du status LCUS = bruit (0x4F), seuls bytes 1-3 valides
### Vidéos didactiques multi-étapes (1 août)
- Système complet : situations avec N étapes vidéo, chaque étape boucle jusqu'à validation opérateur
- 6 situations par défaut (montage, démontage, bourrage, rouleau, wifi, calibration)
- Wizard overlay plein écran avec bouton "OK c'est bon" + navigation étapes
- Storage `data/videos/{situation}/etape_XX.mp4`, config.json pour toggles/labels
- 8 endpoints API vidéos + migration ancien format plat
### Panneau diagnostic + auto-recovery + overlays erreur (3 août)
- Onglet "Diagnostic" dans admin : 4 cartes santé (camera/imprimante/relais/système), actions (reconnecter/réactiver/redémarrer), logs live, USB
- Endpoints : `/api/systeme/sante`, `/api/systeme/logs`, `/api/systeme/redemarrer-backend`, `/api/imprimante/reactiver`, `/api/systeme/usb`
- Auto-recovery imprimante : `_surveiller_imprimante` toutes les 30s, cupsenable si disabled
- Overlay erreur utilisateur avec escalade : auto-fix → patience → opérateur → aide vidéo → redémarrage
- Wizard vidéo connecté aux erreurs avec auto-détection résolution (poll sante)
- Situation `camera_hs` ajoutée aux vidéos didactiques par défaut
### Fix impression galerie + robustesse (2 août)
- Bug: `imprimante: null` dans config.json → TypeError: subprocess recevait None en argument
- Cause: `.get("imprimante", "Mitsubishi")` retourne None si la clé existe avec valeur null
- Fix: pattern `or "Mitsubishi"` dans 5 endroits (printer.py + main.py)
- statut-detail endpoint: ajout try/except pour ne plus retourner 500
- api_imprimer: ajout try/except global pour erreurs inattendues
- Config Surface corrigée : `imprimante` passé de null à "Mitsubishi"
### Canon DSLR restart — problème ouvert (2 août)
- Canon ne redémarre pas après coupure alim secteur (bouton power doit être pressé)
- Interrupteur doit être laissé sur ON pour que le power cycle fonctionne
- Si ça ne suffit pas : bypass physique du bouton power (soudure pont ou optocoupler sur canal 4)
- Camera fixée au plancher, trappe batterie inaccessible
--- ---
## Commits non pushés (au 12 juillet 2026) ## Commits non pushés (au 2 août 2026)
3 commits en avance sur origin/main : Incluent relais HID, vidéos didactiques, consommables, admin backoffice.
- `5e482a8` Consommables : poses perdues + ratio 0.5 Push Gitea bloqué (pas de credentials SSH/HTTPS configurés sur Surface).
- `3b6f376` Suivi consommables : compteur papier/ruban/photos + diagnostic
- `44a9992` Admin backoffice : rubriques + vidéos + surprise + rembobinage
--- ---