From d02cc90feef2d7179e03b740c1af975112ae54ab Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 10 Jun 2026 12:11:33 +0200 Subject: [PATCH] =?UTF-8?q?Bullet-proof=20cam=C3=A9ra=20:=20reconnexion=20?= =?UTF-8?q?auto=20+=20fix=20crash=20capture=20+=20AF=20LiveView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backend: - surveiller_dslr : fix condition qui court-circuitait le check santé quand mode==gphoto2 — watchdog surveille maintenant via preview_dslr_ok et reconnecte automatiquement après 15s d'échec (3x5s) - _pusher_preview : ne pas envoyer camera_erreur pendant capture_en_cours (race condition HTTP/WS : le POST /capturer peut arriver avant preview_stop) - api_capturer : force _preview_actif=False dès le début de la capture - _capture_gphoto2 : déclenche autofocusdrive avant la prise en LiveView frontend: - websocket.js : support multi-callbacks par type (wsOnMessage ne remplace plus le callback précédent, tous s'exécutent) - app.js : camera_erreur affiche bannière + reload dernier recours à 2min (plus 10s), camera_ok cache la bannière automatiquement - camera.js : camera_erreur abort capture + arreterPreview(); camera_ok reset captureAbortee Co-Authored-By: Claude Sonnet 4.6 --- backend/camera.py | 12 ++++++++ backend/main.py | 64 ++++++++++++++++++++++------------------ frontend/js/app.js | 8 +++-- frontend/js/camera.js | 6 ++-- frontend/js/websocket.js | 7 +++-- 5 files changed, 61 insertions(+), 36 deletions(-) diff --git a/backend/camera.py b/backend/camera.py index b4d9b49..7b9f158 100644 --- a/backend/camera.py +++ b/backend/camera.py @@ -146,12 +146,24 @@ class Camera: else: return self._capture_simulation(chemin_dest) + def _declencher_af_liveview(self): + """Déclenche l'autofocus LiveView Canon avant la capture (best-effort).""" + try: + cfg = self.camera.get_config() + af = cfg.get_child_by_name("autofocusdrive") + af.set_value(1) + self.camera.set_config(cfg) + time.sleep(0.4) # Attendre que l'AF se verrouille + except Exception: + pass # Non supporté sur ce modèle ou en mode MF + def _capture_gphoto2(self, chemin_dest: Path) -> Path | None: from PIL import Image as PILImage for attempt in range(3): try: with self._gp_lock: + self._declencher_af_liveview() chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE) fichier_camera = gp.CameraFile() self.camera.file_get( diff --git a/backend/main.py b/backend/main.py index cbcbb05..b98c6a3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -86,7 +86,8 @@ async def _pusher_preview(): if donnees is None: _preview_none_count += 1 # Grace period : attendre plusieurs frames avant de signaler l'erreur - if camera.mode == "gphoto2" and _preview_none_count > _PREVIEW_GRACE_FRAMES: + # Ne pas signaler pendant une capture (race condition HTTP/WS) + if camera.mode == "gphoto2" and _preview_none_count > _PREVIEW_GRACE_FRAMES and not capture_en_cours: await diffuser_ws({"type": "camera_erreur", "message": "DSLR ne repond pas au preview"}) _preview_actif = False _preview_none_count = 0 @@ -97,40 +98,46 @@ async def _pusher_preview(): await diffuser_ws({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"}) async def surveiller_dslr(): - """Verifie periodiquement si un DSLR est branche et bascule dessus.""" + """Surveille le DSLR en continu : detecte les deconnexions et reconnecte automatiquement.""" global _dslr_erreurs while True: await asyncio.sleep(5) - # Ne pas interférer pendant que le thread preview utilise la camera - if capture_en_cours or (camera.mode == "gphoto2" and camera.connectee): + if capture_en_cours: continue try: - if GPHOTO2_DISPONIBLE: - dslrs = gp.Camera.autodetect() - if camera.mode == "gphoto2": - # Verifier que le DSLR repond encore - try: - camera.camera.get_summary() + if not GPHOTO2_DISPONIBLE: + continue + if camera.mode == "gphoto2" and camera.connectee: + # Le thread preview capture en permanence — preview_dslr_ok reflète la santé réelle + if camera.preview_dslr_ok: + _dslr_erreurs = 0 + else: + _dslr_erreurs += 1 + if _dslr_erreurs >= 3: # 3 x 5s = 15s sans preview valide + log.warning("DSLR ne repond plus (preview KO depuis 15s), reconnexion forcee...") _dslr_erreurs = 0 - except Exception: - _dslr_erreurs += 1 - if _dslr_erreurs >= 2: - log.warning("DSLR ne repond plus, reconnexion...") - _dslr_erreurs = 0 - camera.deconnecter() - camera.connectee = False - camera.mode = "erreur" - await diffuser_ws({"type": "camera_erreur", "message": "Probleme de communication avec l'appareil photo"}) - # Tenter de reconnecter en boucle - if len(dslrs) > 0: - camera.connecter(source="gphoto2") - if camera.connectee: - await diffuser_ws({"type": "camera_ok"}) - elif len(dslrs) > 0: - log.info(f"DSLR detecte : {dslrs[0][0]}, bascule automatique") + camera.deconnecter() + await diffuser_ws({"type": "camera_erreur", "message": "Appareil photo deconnecte"}) + await asyncio.sleep(2) # Laisser le bus USB se reinitialiser + camera.connecter(source="gphoto2") + if camera.connectee: + log.info("DSLR reconnecte avec succes") + await diffuser_ws({"type": "camera_ok"}) + else: + log.warning("Echec reconnexion DSLR — nouvelle tentative dans 5s") + else: + # Non connecte : chercher et connecter un DSLR disponible + dslrs = gp.Camera.autodetect() + if len(dslrs) > 0: + log.info(f"DSLR detecte : {dslrs[0][0]}, connexion automatique...") camera.connecter(source="gphoto2") - except Exception: - pass + if camera.connectee: + log.info("DSLR connecte avec succes") + await diffuser_ws({"type": "camera_ok"}) + else: + log.warning("Echec connexion DSLR detecte") + except Exception as e: + log.debug(f"surveiller_dslr: {e}") @asynccontextmanager @@ -259,6 +266,7 @@ async def api_capturer(): return JSONResponse({"erreur": "Limite de photos atteinte"}, status_code=403) capture_en_cours = True + _preview_actif = False # Eviter faux camera_erreur si race condition HTTP avant preview_stop try: loop = asyncio.get_event_loop() chemin = await loop.run_in_executor(None, camera.capturer) diff --git a/frontend/js/app.js b/frontend/js/app.js index b4c29bc..eb77b2b 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -360,17 +360,19 @@ wsOnMessage('config_maj', (msg) => { appliquerConfig(); }); -// Erreur camera : afficher overlay + rechargement auto apres 10s (kiosk seulement) +// Erreur camera : overlay visible sur tous les ecrans, reconnexion automatique cote backend let _erreurCameraTimer = null; -wsOnMessage('camera_erreur', (msg) => { +wsOnMessage('camera_erreur', () => { if (window.location.pathname === '/admin') return; document.getElementById('camera-erreur').classList.remove('cache'); + // Reload de dernier recours si la camera ne revient pas apres 2 min if (_erreurCameraTimer) clearTimeout(_erreurCameraTimer); - _erreurCameraTimer = setTimeout(() => { location.reload(); }, 10000); + _erreurCameraTimer = setTimeout(() => { location.reload(); }, 120000); }); wsOnMessage('camera_ok', () => { document.getElementById('camera-erreur').classList.add('cache'); + if (_erreurCameraTimer) { clearTimeout(_erreurCameraTimer); _erreurCameraTimer = null; } }); // Surveillance imprimante — poll toutes les 30s diff --git a/frontend/js/camera.js b/frontend/js/camera.js index 0d9272a..19f53ae 100644 --- a/frontend/js/camera.js +++ b/frontend/js/camera.js @@ -72,11 +72,13 @@ wsOnMessage('preview', (msg) => { resetPreviewTimeout(); }); -// Erreur camera : abort capture en cours + retour accueil automatique +// Erreur camera : abort capture + retour accueil si on est en train de capturer wsOnMessage('camera_erreur', () => { captureAbortee = true; + arreterPreview(); if (ecranActuel === 'capture') { - afficherErreurCapture('Appareil photo déconnecté', 'Retour dans 4 secondes...'); + // Retour accueil automatique en 4s (afficherErreurCapture le fait) + afficherErreurCapture('Appareil photo déconnecté', 'Reconnexion en cours...'); } }); diff --git a/frontend/js/websocket.js b/frontend/js/websocket.js index 965f56c..32f2aee 100644 --- a/frontend/js/websocket.js +++ b/frontend/js/websocket.js @@ -18,8 +18,8 @@ function wsConnecter() { ws.onmessage = (event) => { const msg = JSON.parse(event.data); - const cb = wsCallbacks[msg.type]; - if (cb) cb(msg); + const cbs = wsCallbacks[msg.type]; + if (cbs) cbs.forEach(cb => cb(msg)); }; ws.onclose = () => { @@ -39,7 +39,8 @@ function wsEnvoyer(msg) { } function wsOnMessage(type, callback) { - wsCallbacks[type] = callback; + if (!wsCallbacks[type]) wsCallbacks[type] = []; + wsCallbacks[type].push(callback); } // Connexion au demarrage