DSLR auto-recovery USB reset + cadre overlay non-mirroré + lien galerie événement

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 08:30:15 +02:00
parent 6bbcf85e52
commit 97e3f6508d
5 changed files with 75 additions and 32 deletions

View File

@@ -114,13 +114,19 @@ class Camera:
log.error(f"Impossible d'ouvrir /dev/video{index}") log.error(f"Impossible d'ouvrir /dev/video{index}")
return False return False
def deconnecter(self): def deconnecter(self, timeout: float = 3.0):
"""Deconnecte l'appareil photo.""" """Deconnecte l'appareil photo avec timeout pour eviter le blocage PTP."""
if self.camera and GPHOTO2_DISPONIBLE: if self.camera and GPHOTO2_DISPONIBLE:
def _exit():
try: try:
self.camera.exit() self.camera.exit()
except gp.GPhoto2Error: except Exception:
pass 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: if self.webcam:
self.webcam.release() self.webcam.release()
self.camera = None self.camera = None

View File

@@ -124,7 +124,8 @@ def _usb_reset_canon():
async def surveiller_dslr(): 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 global _dslr_erreurs
_echecs_connexion = 0 _echecs_connexion = 0
while True: while True:
@@ -135,42 +136,55 @@ async def surveiller_dslr():
if not GPHOTO2_DISPONIBLE: if not GPHOTO2_DISPONIBLE:
continue continue
if camera.mode == "gphoto2" and camera.connectee: 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: if camera.preview_dslr_ok:
_dslr_erreurs = 0 _dslr_erreurs = 0
_echecs_connexion = 0 _echecs_connexion = 0
else: else:
_dslr_erreurs += 1 _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...") log.warning(f"camera_erreur : DSLR preview KO depuis {_dslr_erreurs * 5}s, reconnexion forcee...")
_dslr_erreurs = 0 _dslr_erreurs = 0
camera.deconnecter() _echecs_connexion += 1
await diffuser_ws({"type": "camera_erreur", "message": "Appareil photo deconnecte"}) await _reconnecter_dslr_avec_reset(_echecs_connexion)
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")
else: else:
# Non connecte : chercher et connecter un DSLR disponible # Non connecte : chercher et connecter un DSLR disponible
dslrs = gp.Camera.autodetect() dslrs = gp.Camera.autodetect()
if len(dslrs) > 0: if len(dslrs) > 0:
log.info(f"DSLR detecte : {dslrs[0][0]}, connexion automatique...") _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") camera.connecter(source="gphoto2")
if camera.connectee: if camera.connectee:
log.info("DSLR connecte avec succes") log.info("DSLR reconnecte avec succes")
_echecs_connexion = 0
_appliquer_config_camera() _appliquer_config_camera()
await diffuser_ws({"type": "camera_ok"}) await diffuser_ws({"type": "camera_ok"})
else: else:
_echecs_connexion += 1 log.warning(f"Echec reconnexion DSLR (tentative {echecs})")
log.warning(f"Echec connexion DSLR ({_echecs_connexion})") if echecs >= 3:
except Exception as e: log.info("Reset USB force supplementaire...")
log.debug(f"surveiller_dslr: {e}") _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(): def _appliquer_config_camera():

View File

@@ -321,6 +321,10 @@ html, body {
transform: scaleX(-1); /* miroir selfie : ce que voit l'utilisateur en se posant */ 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 { .preview-live img.preview-chargement {
filter: blur(8px) brightness(0.6); filter: blur(8px) brightness(0.6);
} }

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=7"> <link rel="stylesheet" href="/css/style.css?v=8">
<link rel="stylesheet" href="/css/themes.css?v=2"> <link rel="stylesheet" href="/css/themes.css?v=2">
</head> </head>
<body> <body>
@@ -626,6 +626,11 @@
</div> </div>
<button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button> <button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button>
<div class="champ" id="lien-galerie-event" style="display:none">
<label>Galerie en ligne</label>
<a id="url-galerie-event" href="#" target="_blank" class="btn-secondaire btn-petit" style="text-decoration:none;display:inline-block">Ouvrir la galerie</a>
</div>
<h3>Formats d'impression</h3> <h3>Formats d'impression</h3>
<p class="aide">Formats papier disponibles pour cet evenement.</p> <p class="aide">Formats papier disponibles pour cet evenement.</p>
<div class="toggle-groupe" id="event-formats-toggles"> <div class="toggle-groupe" id="event-formats-toggles">
@@ -773,6 +778,6 @@
<script src="/js/effects.js?v=4"></script> <script src="/js/effects.js?v=4"></script>
<script src="/js/gallery.js?v=3"></script> <script src="/js/gallery.js?v=3"></script>
<script src="/js/share.js?v=7"></script> <script src="/js/share.js?v=7"></script>
<script src="/js/admin.js?v=7"></script> <script src="/js/admin.js?v=8"></script>
</body> </body>
</html> </html>

View File

@@ -768,6 +768,20 @@ 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');
// 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(); chargerCadresEvent();
} }