From 97e3f6508dccb82561fb09fe0821f31b4b46bd8e Mon Sep 17 00:00:00 2001 From: Jules Date: Tue, 23 Jun 2026 08:30:15 +0200 Subject: [PATCH] =?UTF-8?q?DSLR=20auto-recovery=20USB=20reset=20+=20cadre?= =?UTF-8?q?=20overlay=20non-mirror=C3=A9=20+=20lien=20galerie=20=C3=A9v?= =?UTF-8?q?=C3=A9nement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - surveiller_dslr: reset USB ioctl après 2 échecs, double reset après 3 - deconnecter(): timeout threading 3s pour éviter blocage PTP - CSS: .preview-live .cadre-overlay sans scaleX(-1) - Admin événement: lien direct vers galerie en ligne Co-Authored-By: Claude Opus 4.6 --- backend/camera.py | 18 ++++++++---- backend/main.py | 62 ++++++++++++++++++++++++++---------------- frontend/css/style.css | 4 +++ frontend/index.html | 9 ++++-- frontend/js/admin.js | 14 ++++++++++ 5 files changed, 75 insertions(+), 32 deletions(-) diff --git a/backend/camera.py b/backend/camera.py index 063740c..3b8a786 100644 --- a/backend/camera.py +++ b/backend/camera.py @@ -114,13 +114,19 @@ class Camera: log.error(f"Impossible d'ouvrir /dev/video{index}") return False - def deconnecter(self): - """Deconnecte l'appareil photo.""" + def deconnecter(self, timeout: float = 3.0): + """Deconnecte l'appareil photo avec timeout pour eviter le blocage PTP.""" if self.camera and GPHOTO2_DISPONIBLE: - try: - self.camera.exit() - except gp.GPhoto2Error: - pass + def _exit(): + try: + self.camera.exit() + except Exception: + pass + t = threading.Thread(target=_exit, daemon=True) + t.start() + t.join(timeout=timeout) + if t.is_alive(): + log.warning("camera.exit() bloque — abandon (le FD sera libere au GC)") if self.webcam: self.webcam.release() self.camera = None diff --git a/backend/main.py b/backend/main.py index ff8aef5..6569c18 100644 --- a/backend/main.py +++ b/backend/main.py @@ -124,7 +124,8 @@ def _usb_reset_canon(): async def surveiller_dslr(): - """Surveille le DSLR en continu : detecte les deconnexions et reconnecte automatiquement.""" + """Surveille le DSLR en continu : detecte les deconnexions et reconnecte automatiquement. + Apres plusieurs echecs, fait un reset USB ioctl pour debloquer le port PTP.""" global _dslr_erreurs _echecs_connexion = 0 while True: @@ -135,44 +136,57 @@ async def surveiller_dslr(): 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 _echecs_connexion = 0 else: _dslr_erreurs += 1 - if _dslr_erreurs >= 3 and not _preview_actif: # Ne pas couper pendant le countdown + if _dslr_erreurs >= 3 and not _preview_actif: log.warning(f"camera_erreur : DSLR preview KO depuis {_dslr_erreurs * 5}s, reconnexion forcee...") _dslr_erreurs = 0 - 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") - _echecs_connexion = 0 - _appliquer_config_camera() - await diffuser_ws({"type": "camera_ok"}) - else: - log.warning("Echec reconnexion DSLR — nouvelle tentative dans 5s") + _echecs_connexion += 1 + await _reconnecter_dslr_avec_reset(_echecs_connexion) 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") - if camera.connectee: - log.info("DSLR connecte avec succes") - _echecs_connexion = 0 - _appliquer_config_camera() - await diffuser_ws({"type": "camera_ok"}) - else: - _echecs_connexion += 1 - log.warning(f"Echec connexion DSLR ({_echecs_connexion})") + _echecs_connexion += 1 + log.info(f"DSLR detecte : {dslrs[0][0]}, connexion automatique... (tentative {_echecs_connexion})") + await _reconnecter_dslr_avec_reset(_echecs_connexion) except Exception as e: log.debug(f"surveiller_dslr: {e}") +async def _reconnecter_dslr_avec_reset(echecs: int): + """Reconnexion DSLR avec reset USB progressif.""" + camera.deconnecter() + await diffuser_ws({"type": "camera_erreur", "message": "Appareil photo deconnecte"}) + + if echecs >= 2: + log.info(f"Reset USB Canon (echec #{echecs})...") + _usb_reset_canon() + await asyncio.sleep(4) + else: + await asyncio.sleep(2) + + camera.connecter(source="gphoto2") + if camera.connectee: + log.info("DSLR reconnecte avec succes") + _appliquer_config_camera() + await diffuser_ws({"type": "camera_ok"}) + else: + log.warning(f"Echec reconnexion DSLR (tentative {echecs})") + if echecs >= 3: + log.info("Reset USB force supplementaire...") + _usb_reset_canon() + await asyncio.sleep(5) + camera.connecter(source="gphoto2") + if camera.connectee: + log.info("DSLR reconnecte apres double reset") + _appliquer_config_camera() + await diffuser_ws({"type": "camera_ok"}) + + def _appliquer_config_camera(): """Applique les paramètres caméra persistants (flash, etc.) après connexion.""" cfg = charger_config() diff --git a/frontend/css/style.css b/frontend/css/style.css index 4510896..749643d 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -321,6 +321,10 @@ html, body { transform: scaleX(-1); /* miroir selfie : ce que voit l'utilisateur en se posant */ } +.preview-live .cadre-overlay { + transform: translate(-50%, -50%); +} + .preview-live img.preview-chargement { filter: blur(8px) brightness(0.6); } diff --git a/frontend/index.html b/frontend/index.html index 0d8d609..8744dca 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ - + @@ -626,6 +626,11 @@ + +

Formats d'impression

Formats papier disponibles pour cet evenement.

@@ -773,6 +778,6 @@ - + diff --git a/frontend/js/admin.js b/frontend/js/admin.js index 2acc3e9..81ca67b 100644 --- a/frontend/js/admin.js +++ b/frontend/js/admin.js @@ -768,6 +768,20 @@ function afficherDetailEvent(event) { document.getElementById('tog-event-fmt-15x20').checked = fmts.includes('15x20'); document.getElementById('tog-event-fmt-strip').checked = fmts.includes('strip'); + // Lien galerie en ligne + const booth = config.booth || {}; + const lienBloc = document.getElementById('lien-galerie-event'); + const lienA = document.getElementById('url-galerie-event'); + if (booth.actif && booth.url) { + const evtId = booth.event_id || eventEditId || 'default'; + const url = booth.url.replace(/\/$/, '') + '/' + evtId; + lienA.href = url; + lienA.textContent = url; + lienBloc.style.display = ''; + } else { + lienBloc.style.display = 'none'; + } + chargerCadresEvent(); }