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:
@@ -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:
|
||||||
try:
|
def _exit():
|
||||||
self.camera.exit()
|
try:
|
||||||
except gp.GPhoto2Error:
|
self.camera.exit()
|
||||||
pass
|
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:
|
if self.webcam:
|
||||||
self.webcam.release()
|
self.webcam.release()
|
||||||
self.camera = None
|
self.camera = None
|
||||||
|
|||||||
@@ -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,44 +136,57 @@ 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
|
||||||
camera.connecter(source="gphoto2")
|
log.info(f"DSLR detecte : {dslrs[0][0]}, connexion automatique... (tentative {_echecs_connexion})")
|
||||||
if camera.connectee:
|
await _reconnecter_dslr_avec_reset(_echecs_connexion)
|
||||||
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})")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"surveiller_dslr: {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():
|
def _appliquer_config_camera():
|
||||||
"""Applique les paramètres caméra persistants (flash, etc.) après connexion."""
|
"""Applique les paramètres caméra persistants (flash, etc.) après connexion."""
|
||||||
cfg = charger_config()
|
cfg = charger_config()
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user