Compare commits

..

3 Commits

Author SHA1 Message Date
cf41f66367 Gestion erreurs capture : messages clairs a chaque etape
- Verification etat camera AVANT le lancement (pas de DSLR = erreur immediate)
- Detection erreur camera PENDANT le compte a rebours (pas de capture aveugle)
- Capture : erreur reseau catchee, retry x3, message explicite si echec
- Limite de photos : message + detail au lieu de retour accueil silencieux
- Strip/collage : verification resultat, message si generation echoue
- traiterCapture : try/catch global, plus de retour accueil silencieux
- Overlay erreur : titre + detail lisible, 4s avant retour accueil

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 20:47:58 +02:00
f9fd4c4f22 Fix erreur DSLR: detection immediate + message clair a l'ecran
- Backend: detecte quand le preview DSLR echoue (fallback silencieux)
  et envoie camera_erreur au frontend (debounce 5s)
- Frontend: overlay d'erreur avec message si preview DSLR indisponible
- Frontend: si capture echoue, affiche le motif 3s avant retour accueil
  au lieu de retourner silencieusement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 20:46:46 +02:00
ef6679dc97 Preview: overlay erreur si DSLR deconnecte pendant la capture
- Overlay semi-transparent sur le preview quand camera_erreur recue
- Timeout 3s : si aucune frame recue, affiche l'erreur automatiquement
- Disparait des que les frames reprennent (camera_ok ou nouvelle frame)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 20:42:05 +02:00
5 changed files with 171 additions and 24 deletions

View File

@@ -64,6 +64,7 @@ class Camera:
self.webcam = None self.webcam = None
self.webcam_index = -1 self.webcam_index = -1
self._preview_webcam_fallback = None self._preview_webcam_fallback = None
self.preview_dslr_ok = True # True si le dernier preview DSLR a reussi
def connecter(self, source=None) -> bool: def connecter(self, source=None) -> bool:
"""Connecte la camera. source: 'gphoto2', 'webcam:0', 'webcam:2', etc.""" """Connecte la camera. source: 'gphoto2', 'webcam:0', 'webcam:2', etc."""
@@ -202,7 +203,9 @@ class Camera:
elif self.mode == "gphoto2": elif self.mode == "gphoto2":
data = self._preview_gphoto2() data = self._preview_gphoto2()
if data: if data:
self.preview_dslr_ok = True
return data return data
self.preview_dslr_ok = False
# Fallback webcam pour le preview si le DSLR ne supporte pas le live view # Fallback webcam pour le preview si le DSLR ne supporte pas le live view
if not self._preview_webcam_fallback: if not self._preview_webcam_fallback:
self._init_preview_fallback() self._init_preview_fallback()

View File

@@ -37,6 +37,7 @@ clients_ws: list[WebSocket] = []
capture_en_cours = False capture_en_cours = False
_dslr_erreurs = 0 _dslr_erreurs = 0
_dernier_envoi_erreur_preview = 0.0 # timestamp du dernier camera_erreur envoye depuis le preview
async def surveiller_dslr(): async def surveiller_dslr():
"""Verifie periodiquement si un DSLR est branche et bascule dessus.""" """Verifie periodiquement si un DSLR est branche et bascule dessus."""
@@ -529,6 +530,8 @@ async def traiter_message_ws(msg: dict, ws: WebSocket):
if type_msg == "ping": if type_msg == "ping":
await ws.send_json({"type": "pong"}) await ws.send_json({"type": "pong"})
elif type_msg == "preview": elif type_msg == "preview":
import time as _time
global _dernier_envoi_erreur_preview
# Pendant une capture DSLR, utiliser directement la webcam fallback # Pendant une capture DSLR, utiliser directement la webcam fallback
donnees = None donnees = None
if capture_en_cours and camera._preview_webcam_fallback: if capture_en_cours and camera._preview_webcam_fallback:
@@ -541,6 +544,12 @@ async def traiter_message_ws(msg: dict, ws: WebSocket):
if donnees: if donnees:
b64 = base64.b64encode(donnees).decode("ascii") b64 = base64.b64encode(donnees).decode("ascii")
await ws.send_json({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"}) await ws.send_json({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"})
# Si le DSLR est cense etre connecte mais le preview echoue, signaler l'erreur
if camera.mode == "gphoto2" and not camera.preview_dslr_ok:
now = _time.monotonic()
if now - _dernier_envoi_erreur_preview > 5.0:
_dernier_envoi_erreur_preview = now
await ws.send_json({"type": "camera_erreur", "message": "Le DSLR ne repond pas au preview"})
async def diffuser_ws(message: dict): async def diffuser_ws(message: dict):

View File

@@ -303,6 +303,41 @@ html, body {
object-fit: contain; object-fit: contain;
} }
.preview-erreur-camera {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
color: #fff;
z-index: 10;
}
.preview-erreur-camera.cache {
display: none;
}
.preview-erreur-icon {
font-size: 5rem;
animation: pulse-texte 1.5s ease-in-out infinite;
}
.preview-erreur-camera p {
font-size: 1.8rem;
font-weight: bold;
margin: 0;
}
.preview-erreur-camera span {
font-size: 1.3rem;
opacity: 0.85;
text-align: center;
max-width: 80%;
}
.compte-a-rebours { .compte-a-rebours {
position: absolute; position: absolute;
top: 0; left: 0; top: 0; left: 0;

View File

@@ -63,6 +63,11 @@
<section id="ecran-capture" class="ecran"> <section id="ecran-capture" class="ecran">
<div id="preview-live" class="preview-live"> <div id="preview-live" class="preview-live">
<img id="img-preview" src="" alt="Preview"> <img id="img-preview" src="" alt="Preview">
<div id="preview-erreur-camera" class="preview-erreur-camera cache">
<div class="preview-erreur-icon">&#128247;</div>
<p>Appareil photo deconnecte</p>
<span>Reconnexion en cours...</span>
</div>
</div> </div>
<div id="compte-a-rebours" class="compte-a-rebours cache"> <div id="compte-a-rebours" class="compte-a-rebours cache">
<span id="chiffre-car">3</span> <span id="chiffre-car">3</span>

View File

@@ -2,18 +2,55 @@
let previewActif = false; let previewActif = false;
let previewInterval = null; let previewInterval = null;
let previewTimeoutId = null;
const PREVIEW_TIMEOUT_MS = 3000;
const EMOJI_CAR = { 3: '🤪', 2: '😱', 1: '🔥' }; const EMOJI_CAR = { 3: '🤪', 2: '😱', 1: '🔥' };
// --- Preview live --- // --- Preview live ---
function afficherErreurCapture(titre, detail = '') {
arreterPreview();
const el = document.getElementById('preview-erreur-camera');
if (!el) { allerA('accueil'); return; }
el.querySelector('p').textContent = titre;
el.querySelector('span').textContent = detail || 'Retour dans 4 secondes...';
el.classList.remove('cache');
setTimeout(() => {
el.classList.add('cache');
allerA('accueil');
}, 4000);
}
function afficherErreurPreview(visible) {
const el = document.getElementById('preview-erreur-camera');
if (!el) return;
if (visible) {
el.querySelector('p').textContent = 'Appareil photo deconnecte';
el.querySelector('span').textContent = 'Reconnexion en cours...';
el.classList.remove('cache');
} else {
el.classList.add('cache');
}
}
function resetPreviewTimeout() {
if (previewTimeoutId) clearTimeout(previewTimeoutId);
previewTimeoutId = setTimeout(() => {
if (previewActif) afficherErreurPreview(true);
}, PREVIEW_TIMEOUT_MS);
}
function lancerPreview() { function lancerPreview() {
if (previewActif) return; if (previewActif) return;
previewActif = true; previewActif = true;
afficherErreurPreview(false);
previewInterval = setInterval(() => { previewInterval = setInterval(() => {
if (previewActif) wsEnvoyer({ type: 'preview' }); if (previewActif) wsEnvoyer({ type: 'preview' });
}, 100); }, 100);
resetPreviewTimeout();
} }
function arreterPreview() { function arreterPreview() {
@@ -22,12 +59,28 @@ function arreterPreview() {
clearInterval(previewInterval); clearInterval(previewInterval);
previewInterval = null; previewInterval = null;
} }
if (previewTimeoutId) {
clearTimeout(previewTimeoutId);
previewTimeoutId = null;
}
afficherErreurPreview(false);
} }
wsOnMessage('preview', (msg) => { wsOnMessage('preview', (msg) => {
if (!previewActif) return; if (!previewActif) return;
const img = document.getElementById('img-preview'); const img = document.getElementById('img-preview');
if (img) img.src = msg.image; if (img) img.src = msg.image;
afficherErreurPreview(false);
resetPreviewTimeout();
});
// Erreur camera pendant la preview
wsOnMessage('camera_erreur', () => {
if (previewActif) afficherErreurPreview(true);
});
wsOnMessage('camera_ok', () => {
afficherErreurPreview(false);
}); });
// --- Capture --- // --- Capture ---
@@ -36,17 +89,27 @@ async function lancerCapture() {
photosSession = []; photosSession = [];
photoImpression = null; photoImpression = null;
// Vider les aperçus precedents
document.getElementById('photo-resultat').src = ''; document.getElementById('photo-resultat').src = '';
document.getElementById('photo-partage').src = ''; document.getElementById('photo-partage').src = '';
majCompteurAccueil(); majCompteurAccueil();
// Verifier l'etat de la camera avant de commencer
try {
const statut = await apiGet('/api/camera/statut');
if (!statut.connectee || statut.mode === 'erreur') {
afficherErreurCapture('Appareil photo non disponible', 'Verifiez la connexion USB du DSLR');
return;
}
} catch (e) {
afficherErreurCapture('Impossible de joindre le serveur', 'Verifiez que le photobooth est bien demarre');
return;
}
const nbPhotos = getNombrePhotos(); const nbPhotos = getNombrePhotos();
const compteurEl = document.getElementById('capture-compteur'); const compteurEl = document.getElementById('capture-compteur');
for (let i = 0; i < nbPhotos; i++) { for (let i = 0; i < nbPhotos; i++) {
// Lancer la preview pour que l'utilisateur se voit
lancerPreview(); lancerPreview();
await pause(500); await pause(500);
@@ -54,29 +117,48 @@ async function lancerCapture() {
compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`; compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`;
} }
// Compte a rebours par dessus la preview
await compteARebours(); await compteARebours();
// Arreter la preview et attendre que le DSLR soit libre // Si la camera a signale une erreur pendant le compte a rebours, on arrete
const erreurVisible = !document.getElementById('preview-erreur-camera').classList.contains('cache');
if (erreurVisible) {
arreterPreview();
afficherErreurCapture('Appareil photo deconnecte', 'Reconnectez le DSLR et reessayez');
return;
}
arreterPreview(); arreterPreview();
await pause(300); await pause(300);
afficherFlash(); afficherFlash();
// Tenter la capture avec retry // Tenter la capture avec retry
let resultat = null; let resultat = null;
let erreurReseau = false;
for (let r = 0; r < 3; r++) { for (let r = 0; r < 3; r++) {
resultat = await apiPost('/api/capturer'); try {
resultat = await apiPost('/api/capturer');
} catch (e) {
erreurReseau = true;
await pause(500);
continue;
}
erreurReseau = false;
if (!resultat.erreur) break; if (!resultat.erreur) break;
if (resultat.erreur === 'Limite de photos atteinte') { if (resultat.erreur === 'Limite de photos atteinte') {
allerA('accueil'); afficherErreurCapture('Limite de photos atteinte', 'La session est terminee');
return; return;
} }
await pause(500); await pause(500);
} }
if (resultat.erreur) { if (erreurReseau) {
allerA('accueil'); afficherErreurCapture('Erreur reseau', 'Le serveur ne repond pas — redemarrez le photobooth');
return;
}
if (!resultat || resultat.erreur) {
const msg = resultat?.erreur || 'Erreur inconnue';
afficherErreurCapture('Echec de la capture', msg.includes('Echec') ? 'Verifiez le DSLR et reessayez' : msg);
return; return;
} }
@@ -194,30 +276,43 @@ let photoImpression = null; // Nom du fichier d'impression (2 bandes sur 10x15)
async function traiterCapture() { async function traiterCapture() {
if (photosSession.length === 0) { if (photosSession.length === 0) {
allerA('accueil'); afficherErreurCapture('Aucune photo capturee', 'Une erreur inattendue s\'est produite');
return; return;
} }
photoImpression = null; photoImpression = null;
if (modeActuel === 'multi') { try {
const mode = config.multi_shot?.mode || 'strip'; if (modeActuel === 'multi') {
let resultat; const mode = config.multi_shot?.mode || 'strip';
if (mode === 'strip') { let resultat;
resultat = await apiPost('/api/strip', { photos: photosSession }); if (mode === 'strip') {
if (resultat.impression) { resultat = await apiPost('/api/strip', { photos: photosSession });
photoImpression = resultat.impression; if (resultat.erreur) {
afficherErreurCapture('Erreur creation de la planche', resultat.erreur);
return;
}
if (resultat.impression) photoImpression = resultat.impression;
} else {
resultat = await apiPost('/api/collage', { photos: photosSession });
if (resultat.erreur) {
afficherErreurCapture('Erreur creation du collage', resultat.erreur);
return;
}
}
if (!resultat.nom) {
afficherErreurCapture('Erreur traitement des photos', 'Le fichier final n\'a pas ete genere');
return;
} }
} else {
resultat = await apiPost('/api/collage', { photos: photosSession });
}
if (resultat.nom) {
photoFinale = resultat.nom; photoFinale = resultat.nom;
afficherPreviewPhoto('/data/exports/' + resultat.nom); afficherPreviewPhoto('/data/exports/' + resultat.nom);
} else {
photoFinale = photosSession[0];
afficherPreviewPhoto('/data/photos/' + photosSession[0]);
} }
} else { } catch (e) {
photoFinale = photosSession[0]; afficherErreurCapture('Erreur reseau', 'Impossible de traiter les photos');
afficherPreviewPhoto('/data/photos/' + photosSession[0]); return;
} }
allerA('preview'); allerA('preview');