From 6dafe6141733c1cfb11c36a884251333e85a93ba Mon Sep 17 00:00:00 2001 From: Jules Date: Tue, 23 Jun 2026 08:44:12 +0200 Subject: [PATCH] =?UTF-8?q?Escalade=20DSLR=20recovery=20:=20uhubctl=20powe?= =?UTF-8?q?r=20cycle=20+=20backoff=2060s=20apr=C3=A8s=2010=20=C3=A9checs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Niveaux : ioctl reset (2) -> unbind/rebind (3-5) -> uhubctl 5V cut (6+) -> backoff 60s (10+) Co-Authored-By: Claude Opus 4.6 --- backend/main.py | 54 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/backend/main.py b/backend/main.py index 965a9eb..53062bd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -151,13 +151,50 @@ def _usb_rebind_canon(): return False +def _usb_power_cycle_canon(): + """Coupe et retablit l'alimentation 5V du port USB Canon via uhubctl. + Necessite un hub USB compatible power switching (Realtek, etc.).""" + import glob, subprocess, time + try: + for f in glob.glob("/sys/bus/usb/devices/*/idVendor"): + if open(f).read().strip() == "04a9": + dev_id = __import__("os.path", fromlist=["basename"]).basename( + __import__("os.path", fromlist=["dirname"]).dirname(f) + ) + if "." in dev_id: + hub = dev_id.rsplit(".", 1)[0] + port = dev_id.rsplit(".", 1)[1] + else: + hub = dev_id.rsplit("-", 1)[0] + port = dev_id.rsplit("-", 1)[1] + log.info(f"uhubctl power OFF: hub={hub} port={port}") + subprocess.run( + ["sudo", "uhubctl", "-l", hub, "-p", port, "-a", "off"], + timeout=10, capture_output=True, + ) + time.sleep(8) + subprocess.run( + ["sudo", "uhubctl", "-l", hub, "-p", port, "-a", "on"], + timeout=10, capture_output=True, + ) + log.info(f"uhubctl power ON: hub={hub} port={port}") + time.sleep(8) + return True + except Exception as e: + log.warning(f"_usb_power_cycle_canon: {e}") + return False + + async def surveiller_dslr(): """Surveille le DSLR en continu : detecte les deconnexions et reconnecte automatiquement. - Apres plusieurs echecs, fait un reset USB ioctl pour debloquer le port PTP.""" + Escalade : ioctl reset -> unbind/rebind -> uhubctl power cycle -> backoff long.""" global _dslr_erreurs _echecs_connexion = 0 while True: - await asyncio.sleep(5) + if _echecs_connexion >= 10: + await asyncio.sleep(60) + else: + await asyncio.sleep(5) if capture_en_cours: continue try: @@ -175,7 +212,6 @@ async def surveiller_dslr(): _echecs_connexion += 1 await _reconnecter_dslr_avec_reset(_echecs_connexion) else: - # Non connecte : chercher et connecter un DSLR disponible dslrs = gp.Camera.autodetect() if len(dslrs) > 0: _echecs_connexion += 1 @@ -187,13 +223,19 @@ async def surveiller_dslr(): async def _reconnecter_dslr_avec_reset(echecs: int): """Reconnexion DSLR avec escalade progressive : - 1-2 : ioctl reset - 3+ : unbind/rebind USB (simule debranchement physique) + 1 : simple reconnexion + 2 : ioctl reset + 3-5 : unbind/rebind USB + 6+ : uhubctl power cycle (coupe 5V du port) + 10+ : backoff 60s entre tentatives """ camera.deconnecter() await diffuser_ws({"type": "camera_erreur", "message": "Appareil photo deconnecte"}) - if echecs >= 3: + if echecs >= 6: + log.info(f"USB power cycle Canon (echec #{echecs})...") + await asyncio.get_event_loop().run_in_executor(None, _usb_power_cycle_canon) + elif echecs >= 3: log.info(f"USB rebind Canon (echec #{echecs})...") ok = await asyncio.get_event_loop().run_in_executor(None, _usb_rebind_canon) if not ok: