diff --git a/backend/camera.py b/backend/camera.py index ba729c8..41f61b9 100644 --- a/backend/camera.py +++ b/backend/camera.py @@ -212,6 +212,33 @@ class Camera: else: return None # Pas de simulation ni webcam en dehors du mode dedie + def configurer_flash(self, actif: bool): + """Active ou désactive le flash intégré Canon. + Essaie plusieurs noms de widgets selon le modèle.""" + if not self.camera or self.mode != "gphoto2": + return + # strobofiring : "0"=inhibé(off), "2"=auto — Canon EOS 60D (widget radio, valeurs string) + # flashmode : "On"/"Off" — autres Canon + candidats = [ + ("strobofiring", "2", "0"), # (widget, valeur_on, valeur_off) + ("flashmode", "On", "Off"), + ] + try: + with self._gp_lock: + cfg = self.camera.get_config() + for nom, val_on, val_off in candidats: + try: + widget = cfg.get_child_by_name(nom) + widget.set_value(val_on if actif else val_off) + self.camera.set_config(cfg) + log.info(f"Flash intégré ({nom}) : {'On' if actif else 'Off'}") + return + except Exception: + pass + log.debug("Aucun widget flash trouvé sur ce modèle Canon") + except Exception as e: + log.debug(f"configurer_flash : {e}") + def _activer_viewfinder_init(self): """Active le LiveView pendant l'init (pas de lock, appelé avant que le thread démarre).""" try: diff --git a/backend/main.py b/backend/main.py index 6b6fdef..5934878 100644 --- a/backend/main.py +++ b/backend/main.py @@ -49,7 +49,7 @@ _loop_principal: asyncio.AbstractEventLoop | None = None # loop asyncio princip _preview_none_count = 0 -_PREVIEW_GRACE_FRAMES = 15 # ~500ms avant de declarer une erreur +_PREVIEW_GRACE_FRAMES = 40 # 40 x 50ms = 2s avant de declarer une erreur (Canon LiveView peut être lent à démarrer) def _thread_preview(): @@ -88,6 +88,7 @@ async def _pusher_preview(): # Grace period : attendre plusieurs frames avant de signaler l'erreur # Ne pas signaler pendant une capture (race condition HTTP/WS) if camera.mode == "gphoto2" and _preview_none_count > _PREVIEW_GRACE_FRAMES and not capture_en_cours: + log.warning(f"camera_erreur envoyé : preview None depuis {_preview_none_count * 50}ms") await diffuser_ws({"type": "camera_erreur", "message": "DSLR ne repond pas au preview"}) _preview_actif = False _preview_none_count = 0 @@ -134,8 +135,8 @@ async def surveiller_dslr(): _echecs_connexion = 0 else: _dslr_erreurs += 1 - if _dslr_erreurs >= 3: # 3 x 5s = 15s sans preview valide - log.warning("DSLR ne repond plus (preview KO depuis 15s), reconnexion forcee...") + if _dslr_erreurs >= 3 and not _preview_actif: # Ne pas couper pendant le countdown + 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"}) @@ -144,6 +145,7 @@ async def surveiller_dslr(): 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") @@ -156,6 +158,7 @@ async def surveiller_dslr(): 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 @@ -164,6 +167,13 @@ async def surveiller_dslr(): log.debug(f"surveiller_dslr: {e}") +def _appliquer_config_camera(): + """Applique les paramètres caméra persistants (flash, etc.) après connexion.""" + cfg = charger_config() + flash_integre = cfg.get("camera", {}).get("flash_integre", True) + camera.configurer_flash(flash_integre) + + @asynccontextmanager async def lifespan(app: FastAPI): """Demarrage et arret de l'application.""" @@ -171,6 +181,7 @@ async def lifespan(app: FastAPI): log.info("Demarrage du photobooth") _loop_principal = asyncio.get_event_loop() camera.connecter() + _appliquer_config_camera() # Thread de capture preview (bloquant, tourne en parallele) t = threading.Thread(target=_thread_preview, daemon=True) t.start() @@ -276,6 +287,9 @@ async def api_config(): async def api_config_update(modifications: dict): config = mettre_a_jour_config(modifications) await diffuser_ws({"type": "config_maj", "config": config}) + # Appliquer immédiatement les paramètres caméra si modifiés + if "camera" in modifications and camera.connectee: + _appliquer_config_camera() return config @@ -430,6 +444,13 @@ async def api_systeme_info(): } +@app.post("/api/log-js") +async def api_log_js(request: Request): + data = await request.json() + log.warning(f"[JS] {data.get('msg', '?')}") + return {"ok": True} + + @app.get("/api/camera/statut") async def api_camera_statut(): return { @@ -476,6 +497,8 @@ async def api_camera_reconnecter(body: dict = {}): camera.deconnecter() source = body.get("source") ok = camera.connecter(source=source) + if ok: + _appliquer_config_camera() return {"connectee": ok, "mode": camera.mode} @@ -1073,8 +1096,10 @@ async def traiter_message_ws(msg: dict, ws: WebSocket): elif type_msg == "preview_start": global _preview_actif _preview_actif = True + log.info("WS: preview_start recu") elif type_msg == "preview_stop": _preview_actif = False + log.info("WS: preview_stop recu") async def diffuser_ws(message: dict): diff --git a/frontend/css/style.css b/frontend/css/style.css index d5ec3ab..253f171 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -1342,6 +1342,64 @@ h3 { color: var(--texte-secondaire); } +.cadre-choix-item.cadre-selectionne { + border-color: var(--primaire); +} + +/* Popup sélection cadre au moment de l'impression */ +.popup-box-cadre { + max-width: 92vw; + width: 680px; + max-height: 90vh; + padding: 1.2rem; + overflow: hidden; +} + +.grille-cadres-popup { + flex: 1; + min-height: 0; + min-width: 0; + flex-wrap: nowrap; + overflow: hidden; + gap: 0.6rem; + padding: 0.3rem; +} + +.grille-cadres-popup .cadre-choix-item { + flex: 0 1 78px; + min-width: 0; + width: auto; +} + +.grille-cadres-popup .cadre-choix-item .cadre-thumb-wrap { + width: 100%; + aspect-ratio: 2 / 3; + height: auto; +} + +.grille-cadres-popup .cadre-choix-item .cadre-thumb-wrap img { + width: 250%; + height: 250%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.grille-cadres-popup .cadre-choix-aucun .cadre-choix-vide { + width: 100%; + aspect-ratio: 2 / 3; + height: auto; + font-size: 1.6rem; +} + +.grille-cadres-popup .cadre-choix-item span { + font-size: 0.68rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + .cadre-choix-aucun .cadre-choix-vide { width: 140px; height: 210px; diff --git a/frontend/index.html b/frontend/index.html index 8548f07..2947f73 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ - + @@ -144,7 +144,6 @@ QR Code -
@@ -379,6 +378,9 @@
+
+ +

Imprimante

@@ -672,6 +674,18 @@
+ + +