Fix preview + miroir : thread continu, grace period, lock gphoto2

- _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 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:01:25 +02:00
parent 0a918666b3
commit 1364b62297
2 changed files with 22 additions and 9 deletions

View File

@@ -216,8 +216,9 @@ class Camera:
def _preview_gphoto2(self) -> bytes | None: def _preview_gphoto2(self) -> bytes | None:
try: try:
fichier = self.camera.capture_preview() with self._gp_lock:
donnees = bytes(fichier.get_data_and_size()) fichier = self.camera.capture_preview()
donnees = bytes(fichier.get_data_and_size())
img = cv2.imdecode(np.frombuffer(donnees, dtype=np.uint8), cv2.IMREAD_COLOR) img = cv2.imdecode(np.frombuffer(donnees, dtype=np.uint8), cv2.IMREAD_COLOR)
if img is not None: if img is not None:
h, w = img.shape[:2] h, w = img.shape[:2]

View File

@@ -48,11 +48,16 @@ _preview_lock = threading.Lock()
_loop_principal: asyncio.AbstractEventLoop | None = None # loop asyncio principal _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(): def _thread_preview():
"""Thread de fond : capture les frames DSLR en continu et les stocke.""" """Thread de fond : capture les frames DSLR en continu.
global _derniere_frame_preview, _preview_actif Tourne TOUJOURS quand le DSLR est connecte pour maintenir le miroir leve."""
global _derniere_frame_preview
while True: 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) time.sleep(0.05)
continue continue
try: try:
@@ -67,20 +72,26 @@ def _thread_preview():
async def _pusher_preview(): async def _pusher_preview():
"""Tache asyncio : pousse la derniere frame a tous les clients WS abonnes.""" """Tache asyncio : pousse la derniere frame a tous les clients WS abonnes."""
global _preview_actif global _preview_actif, _preview_none_count
while True: while True:
await asyncio.sleep(0.05) # 20 fps max envoye aux clients await asyncio.sleep(0.05) # 20 fps max envoye aux clients
if not _preview_actif:
_preview_none_count = 0
continue
if not clients_ws: if not clients_ws:
_preview_actif = False _preview_actif = False
continue continue
with _preview_lock: with _preview_lock:
donnees = _derniere_frame_preview donnees = _derniere_frame_preview
if donnees is None: if donnees is None:
# Signaler l'erreur si le DSLR est sense etre connecte _preview_none_count += 1
if camera.mode == "gphoto2" and _preview_actif: # 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"}) await diffuser_ws({"type": "camera_erreur", "message": "DSLR ne repond pas au preview"})
_preview_actif = False _preview_actif = False
_preview_none_count = 0
continue continue
_preview_none_count = 0
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
b64 = await loop.run_in_executor(None, lambda d=donnees: base64.b64encode(d).decode("ascii")) 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}"}) await diffuser_ws({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"})
@@ -90,7 +101,8 @@ async def surveiller_dslr():
global _dslr_erreurs global _dslr_erreurs
while True: while True:
await asyncio.sleep(5) 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 continue
try: try:
if GPHOTO2_DISPONIBLE: if GPHOTO2_DISPONIBLE: