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}")
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

View File

@@ -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()