Fiabilite Canon : veille/reveil relais, sequence boot, stats diagnostic, fin evenement, compteur consommables
- Preview thread : poll uniquement quand ecran capture actif, keepalive 30s sinon - Sequence boot Canon : power cycle relais si absent USB au demarrage - Veille auto 30min : Canon coupe par relais apres inactivite, reveil au preview_start - Stats diagnostic /api/canon/stats : keepalive, preview, captures, connexions - Escalade reconnexion adoucie : pas d'USB reset les 2 premieres tentatives - Hard reset relais 10s (resistance decharge 990ohm sur dummy battery 8V) - Gestion fin evenement : date_fin + bouton terminer + guard 403 impression - Compteur base sur min(papier, ruban) avec cap evenement optionnel - Pre-eclairage countdown : projecteurs ON 3s avant capture pour mesure expo - Canon _configurer_init() : ISO 800, drivemode Single, viewfinder, EXIF preserve - Admin : detail evenement, compteur consommables, galerie impression Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyN9xFp84P9VaWT3Qe2DXu
This commit is contained in:
@@ -88,11 +88,12 @@ class Camera:
|
||||
pass
|
||||
cam.init()
|
||||
self.camera = cam
|
||||
self._desactiver_veille_init()
|
||||
self._activer_viewfinder_init()
|
||||
time.sleep(1)
|
||||
self._configurer_init()
|
||||
self.connectee = True
|
||||
self.mode = "gphoto2"
|
||||
self.preview_dslr_ok = True
|
||||
self._log_reglages()
|
||||
log.info("Camera DSLR connectee (LiveView actif)")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -190,23 +191,36 @@ class Camera:
|
||||
vf = cfg.get_child_by_name("viewfinder")
|
||||
vf.set_value(0)
|
||||
self.camera.set_config(cfg)
|
||||
time.sleep(0.8)
|
||||
except Exception:
|
||||
pass
|
||||
log.info(f"Déclenchement capture DSLR (tentative {attempt+1}/3)")
|
||||
t0 = time.time()
|
||||
chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE)
|
||||
t1 = time.time()
|
||||
fichier_camera = gp.CameraFile()
|
||||
self.camera.file_get(
|
||||
chemin_camera.folder, chemin_camera.name, gp.GP_FILE_TYPE_NORMAL, fichier_camera
|
||||
)
|
||||
t2 = time.time()
|
||||
tmp_path = str(chemin_dest) + ".tmp"
|
||||
fichier_camera.save(tmp_path)
|
||||
log.info(f"Capture: shutter={t1-t0:.1f}s download={t2-t1:.1f}s")
|
||||
threading.Thread(target=self._post_capture_warmup, daemon=True).start()
|
||||
from PIL import ImageOps as PILImageOps
|
||||
img = PILImageOps.exif_transpose(PILImage.open(tmp_path))
|
||||
t3 = time.time()
|
||||
pil_img = PILImage.open(tmp_path)
|
||||
exif_data = pil_img.info.get("exif")
|
||||
img = PILImageOps.exif_transpose(pil_img)
|
||||
if img.width > 4000:
|
||||
ratio = 4000 / img.width
|
||||
img = img.resize((4000, int(img.height * ratio)), PILImage.LANCZOS)
|
||||
img.save(str(chemin_dest), "JPEG", quality=92)
|
||||
img = img.resize((4000, int(img.height * ratio)), PILImage.BILINEAR)
|
||||
save_kwargs = {"quality": 92}
|
||||
if exif_data:
|
||||
save_kwargs["exif"] = exif_data
|
||||
img.save(str(chemin_dest), "JPEG", **save_kwargs)
|
||||
t4 = time.time()
|
||||
log.info(f"Post-process: {t4-t3:.1f}s")
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
log.info(f"Photo capturee (DSLR) : {chemin_dest} ({img.width}x{img.height})")
|
||||
return chemin_dest
|
||||
@@ -292,35 +306,48 @@ class Camera:
|
||||
except Exception as e:
|
||||
log.debug(f"configurer_flash : {e}")
|
||||
|
||||
def _desactiver_veille_init(self):
|
||||
"""Desactive la mise en veille auto du boitier (Canon : autopoweroff en minutes, 0=jamais).
|
||||
Sans ca, le DSLR s'eteint seul apres quelques minutes d'inactivite et apparait deconnecte."""
|
||||
candidats = ["autopoweroff", "auto_power_off", "/main/settings/autopoweroff"]
|
||||
try:
|
||||
cfg = self.camera.get_config()
|
||||
for nom in candidats:
|
||||
def _configurer_init(self):
|
||||
"""Configure le Canon : veille OFF, ISO, drivemode, viewfinder (appels séparés)."""
|
||||
reglages = [
|
||||
("autopoweroff", 0),
|
||||
("viewfinder", 1),
|
||||
("iso", "800"),
|
||||
("drivemode", "Single"),
|
||||
]
|
||||
for nom, valeur in reglages:
|
||||
for tentative in range(3):
|
||||
try:
|
||||
widget = cfg.get_child_by_name(nom)
|
||||
widget.set_value(0)
|
||||
cfg = self.camera.get_config()
|
||||
w = cfg.get_child_by_name(nom)
|
||||
w.set_value(valeur)
|
||||
self.camera.set_config(cfg)
|
||||
log.info(f"Mise en veille DSLR désactivée ({nom})")
|
||||
return
|
||||
log.info(f"Canon init: {nom} = {valeur}")
|
||||
break
|
||||
except gp.GPhoto2Error as e:
|
||||
if tentative < 2:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
log.warning(f"Canon init {nom}: {e}")
|
||||
except Exception:
|
||||
continue
|
||||
log.debug("Widget autopoweroff non trouvé sur ce modèle")
|
||||
except Exception as e:
|
||||
log.debug(f"_desactiver_veille_init : {e}")
|
||||
break
|
||||
|
||||
def _activer_viewfinder_init(self):
|
||||
"""Active le LiveView pendant l'init (pas de lock, appelé avant que le thread démarre)."""
|
||||
def _log_reglages(self):
|
||||
"""Log les réglages Canon actuels pour diagnostic."""
|
||||
try:
|
||||
cfg = self.camera.get_config()
|
||||
vf = cfg.get_child_by_name("viewfinder")
|
||||
vf.set_value(1)
|
||||
self.camera.set_config(cfg)
|
||||
log.info("Viewfinder activé (init)")
|
||||
except Exception as e:
|
||||
log.warning(f"Viewfinder non supporté : {e}")
|
||||
vals = {}
|
||||
for nom in ["autoexposuremode", "iso", "shutterspeed", "aperture", "meteringmode"]:
|
||||
try:
|
||||
w = cfg.get_child_by_name(nom)
|
||||
vals[nom] = w.get_value()
|
||||
log.info(f"Canon actuel: {nom} = {vals[nom]}")
|
||||
except Exception:
|
||||
pass
|
||||
mode = vals.get("autoexposuremode", "")
|
||||
if mode in ("Flash Off", "Auto", "Night Portrait", "Landscape", "Portrait", "Sports"):
|
||||
log.warning(f"Canon en mode scène '{mode}' — ISO/vitesse non modifiables. Tourner le dial sur M ou Av.")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def activer_viewfinder(self):
|
||||
"""Active le LiveView (miroir levé) depuis le thread — thread-safe."""
|
||||
|
||||
@@ -203,16 +203,28 @@ def detecter_usb() -> list[str]:
|
||||
|
||||
|
||||
def compteur_restant() -> dict:
|
||||
"""Retourne l'etat du compteur."""
|
||||
"""Retourne l'etat du compteur, basé sur les consommables restants."""
|
||||
config = charger_config()
|
||||
compteur = config.get("compteur", {})
|
||||
limite = compteur.get("limite", 400)
|
||||
conso = config.get("consommables", {})
|
||||
prises = compteur.get("photos_prises", 0)
|
||||
papier_rest = max(0, conso.get("papier_capacite", 400) - conso.get("papier_utilise", 0))
|
||||
ruban_rest = max(0, round(conso.get("ruban_capacite", 400) - conso.get("ruban_utilise", 0), 1))
|
||||
restantes = int(min(papier_rest, ruban_rest))
|
||||
limite_event = compteur.get("limite", 0)
|
||||
if compteur.get("actif") and limite_event > 0:
|
||||
restantes = min(restantes, max(0, limite_event - prises))
|
||||
papier_cap = conso.get("papier_capacite", 400)
|
||||
ruban_cap = conso.get("ruban_capacite", 400)
|
||||
capacite_conso = int(min(papier_cap, ruban_cap))
|
||||
return {
|
||||
"actif": compteur.get("actif", False),
|
||||
"limite": limite,
|
||||
"limite": limite_event,
|
||||
"photos_prises": prises,
|
||||
"restantes": max(0, limite - prises),
|
||||
"restantes": restantes,
|
||||
"capacite": capacite_conso,
|
||||
"papier_restant": papier_rest,
|
||||
"ruban_restant": int(ruban_rest),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ SEUIL_SOMBRE = 45
|
||||
SEUIL_CORRECT = 65
|
||||
|
||||
_dernier_analyse: float = 0
|
||||
_INTERVALLE = 3.0
|
||||
_INTERVALLE = 1.0
|
||||
_dernier_resultat: dict | None = None
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
import logging
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from backend.config import RACINE, DOSSIER_CADRES, FORMATS_CADRES, charger_config, mettre_a_jour_config
|
||||
@@ -46,6 +47,8 @@ def creer_evenement(nom: str, **kwargs) -> dict:
|
||||
"couleur_secondaire": kwargs.get("couleur_secondaire", "#ffffff"),
|
||||
"media_accueil": kwargs.get("media_accueil"),
|
||||
"formats_actifs": kwargs.get("formats_actifs", ["10x15", "15x20", "strip"]),
|
||||
"date_fin": kwargs.get("date_fin"),
|
||||
"termine": False,
|
||||
"cadres": {},
|
||||
}
|
||||
_chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8")
|
||||
@@ -86,6 +89,38 @@ def supprimer_evenement(event_id: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def terminer_evenement(event_id: str) -> dict | None:
|
||||
event = obtenir_evenement(event_id)
|
||||
if not event:
|
||||
return None
|
||||
event["termine"] = True
|
||||
_chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8")
|
||||
log.info(f"Evenement termine : {event['nom']} ({event_id})")
|
||||
return event
|
||||
|
||||
|
||||
def evenement_est_termine() -> bool:
|
||||
"""Verifie si l'evenement actif est termine (manuellement ou date_fin depassee)."""
|
||||
config = charger_config()
|
||||
event_id = config.get("evenement", {}).get("event_id")
|
||||
if not event_id:
|
||||
return False
|
||||
event = obtenir_evenement(event_id)
|
||||
if not event:
|
||||
return False
|
||||
if event.get("termine"):
|
||||
return True
|
||||
date_fin = event.get("date_fin")
|
||||
if date_fin:
|
||||
try:
|
||||
fin = datetime.strptime(date_fin, "%Y-%m-%d").date()
|
||||
if date.today() > fin:
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def activer_evenement(event_id: str) -> dict | None:
|
||||
event = obtenir_evenement(event_id)
|
||||
if not event:
|
||||
|
||||
770
backend/main.py
770
backend/main.py
File diff suppressed because it is too large
Load Diff
@@ -249,7 +249,7 @@ def imprimer(
|
||||
conf_imp = config.get("impression", {})
|
||||
|
||||
if imprimante is None:
|
||||
imprimante = conf_imp.get("imprimante", "Mitsubishi")
|
||||
imprimante = conf_imp.get("imprimante") or "Mitsubishi"
|
||||
if copies is None:
|
||||
copies = conf_imp.get("copies", 1)
|
||||
if format_papier is None:
|
||||
|
||||
@@ -26,7 +26,8 @@ PID = 0x2007
|
||||
CANAUX = {
|
||||
"projecteur_gauche": {"relay": 1, "mode": "NO"},
|
||||
"projecteur_droit": {"relay": 2, "mode": "NO"},
|
||||
"canon": {"relay": 3, "mode": "NF"},
|
||||
"canon": {"relay": 3, "mode": "NF"}, # dummy battery
|
||||
"canon_usb": {"relay": 4, "mode": "NF"}, # VBUS USB
|
||||
}
|
||||
|
||||
_lib = None
|
||||
@@ -115,12 +116,13 @@ def status():
|
||||
rbuf = (ctypes.c_ubyte * 64)()
|
||||
with _lock:
|
||||
n = _lib.hid_read_timeout(_dev, rbuf, 64, 500)
|
||||
if n < 4:
|
||||
if n < 5:
|
||||
return None
|
||||
return {
|
||||
"relay_1": rbuf[1] == 1,
|
||||
"relay_2": rbuf[2] == 1,
|
||||
"relay_3": rbuf[3] == 1,
|
||||
"relay_4": rbuf[4] == 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -144,12 +146,30 @@ def projecteurs(on: bool):
|
||||
log.info(f"Projecteurs {'allumés' if on else 'éteints'}")
|
||||
|
||||
|
||||
def power_cycle_canon(duree: float = 3.0):
|
||||
log.info(f"Power-cycle Canon ({duree}s)")
|
||||
_relay_set(3, True) # NF : ON = coupé
|
||||
time.sleep(duree)
|
||||
_relay_set(3, False) # NF : OFF = alimenté
|
||||
log.info("Canon ré-alimenté")
|
||||
def power_cycle_canon(duree: float = 5.0):
|
||||
"""Cycle complet R3+R4 : coupe dummy battery + VBUS, reboot Canon.
|
||||
Résistance de décharge 470Ω sur le 8V → condensateurs vidés en ~3s."""
|
||||
log.info(f"Power-cycle Canon R3+R4 ({duree}s)")
|
||||
_relay_set(3, True) # R3 ON = coupe dummy battery (NF)
|
||||
_relay_set(4, True) # R4 ON = coupe VBUS USB (NF)
|
||||
time.sleep(duree) # résistance vide les caps en ~3s
|
||||
_relay_set(3, False) # R3 OFF = dummy battery → Canon boot
|
||||
time.sleep(15) # attente boot Canon
|
||||
_relay_set(4, False) # R4 OFF = VBUS → USB enumeration
|
||||
log.info("Canon ré-alimenté — attente USB enumeration")
|
||||
|
||||
|
||||
def hard_reset_canon(duree: float = 10.0):
|
||||
"""Reset prolongé pour freeze PTP sévère.
|
||||
Résistance de décharge 470Ω → 5s suffisent, mais on garde une marge."""
|
||||
log.warning(f"Hard reset Canon — coupure R3+R4 ({duree}s)")
|
||||
_relay_set(3, True) # R3 ON = coupe dummy battery
|
||||
_relay_set(4, True) # R4 ON = coupe VBUS
|
||||
time.sleep(duree) # vidange complète avec résistance
|
||||
_relay_set(3, False) # R3 OFF = dummy battery → Canon boot
|
||||
time.sleep(15) # attente boot Canon
|
||||
_relay_set(4, False) # R4 OFF = VBUS → USB enumeration
|
||||
log.info("Hard reset Canon terminé — attente démarrage")
|
||||
|
||||
|
||||
def etat_complet():
|
||||
|
||||
Reference in New Issue
Block a user