From 1364b62297256ace72a71a10383e66de3fd1fb9e Mon Sep 17 00:00:00 2001 From: Jules Date: Fri, 5 Jun 2026 23:01:25 +0200 Subject: [PATCH] Fix preview + miroir : thread continu, grace period, lock gphoto2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _thread_preview tourne TOUJOURS quand DSLR connecté (plus de condition _preview_actif) → capture_preview() appelé en continu → miroir reste levé - Grace period 15 frames (~500ms) avant de signaler camera_erreur : évite la race condition qui tuait le preview avant la 1ère frame - _preview_gphoto2 protégé par _gp_lock (accès concurrent safe) - surveiller_dslr ne tourne plus quand le DSLR est en mode gphoto2 connecté Co-Authored-By: Claude Sonnet 4.6 --- backend/camera.py | 5 +++-- backend/main.py | 26 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/backend/camera.py b/backend/camera.py index da9e409..115aacc 100644 --- a/backend/camera.py +++ b/backend/camera.py @@ -216,8 +216,9 @@ class Camera: def _preview_gphoto2(self) -> bytes | None: try: - fichier = self.camera.capture_preview() - donnees = bytes(fichier.get_data_and_size()) + with self._gp_lock: + fichier = self.camera.capture_preview() + donnees = bytes(fichier.get_data_and_size()) img = cv2.imdecode(np.frombuffer(donnees, dtype=np.uint8), cv2.IMREAD_COLOR) if img is not None: h, w = img.shape[:2] diff --git a/backend/main.py b/backend/main.py index 7e393b9..0f3f6a4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -48,11 +48,16 @@ _preview_lock = threading.Lock() _loop_principal: asyncio.AbstractEventLoop | None = None # loop asyncio principal +_preview_none_count = 0 +_PREVIEW_GRACE_FRAMES = 15 # ~500ms avant de declarer une erreur + + def _thread_preview(): - """Thread de fond : capture les frames DSLR en continu et les stocke.""" - global _derniere_frame_preview, _preview_actif + """Thread de fond : capture les frames DSLR en continu. + Tourne TOUJOURS quand le DSLR est connecte pour maintenir le miroir leve.""" + global _derniere_frame_preview while True: - if not _preview_actif or capture_en_cours: + if capture_en_cours or camera.mode != "gphoto2" or not camera.connectee: time.sleep(0.05) continue try: @@ -67,20 +72,26 @@ def _thread_preview(): async def _pusher_preview(): """Tache asyncio : pousse la derniere frame a tous les clients WS abonnes.""" - global _preview_actif + global _preview_actif, _preview_none_count while True: await asyncio.sleep(0.05) # 20 fps max envoye aux clients + if not _preview_actif: + _preview_none_count = 0 + continue if not clients_ws: _preview_actif = False continue with _preview_lock: donnees = _derniere_frame_preview if donnees is None: - # Signaler l'erreur si le DSLR est sense etre connecte - if camera.mode == "gphoto2" and _preview_actif: + _preview_none_count += 1 + # Grace period : attendre plusieurs frames avant de signaler l'erreur + if camera.mode == "gphoto2" and _preview_none_count > _PREVIEW_GRACE_FRAMES: await diffuser_ws({"type": "camera_erreur", "message": "DSLR ne repond pas au preview"}) _preview_actif = False + _preview_none_count = 0 continue + _preview_none_count = 0 loop = asyncio.get_event_loop() b64 = await loop.run_in_executor(None, lambda d=donnees: base64.b64encode(d).decode("ascii")) await diffuser_ws({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"}) @@ -90,7 +101,8 @@ async def surveiller_dslr(): global _dslr_erreurs while True: await asyncio.sleep(5) - if capture_en_cours or _preview_actif: + # Ne pas interférer pendant que le thread preview utilise la camera + if capture_en_cours or (camera.mode == "gphoto2" and camera.connectee): continue try: if GPHOTO2_DISPONIBLE: