Escalade DSLR recovery : uhubctl power cycle + backoff 60s après 10 échecs

Niveaux : ioctl reset (2) -> unbind/rebind (3-5) -> uhubctl 5V cut (6+) -> backoff 60s (10+)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 08:44:12 +02:00
parent e2f65b1ba7
commit 6dafe61417

View File

@@ -151,12 +151,49 @@ 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:
if _echecs_connexion >= 10:
await asyncio.sleep(60)
else:
await asyncio.sleep(5)
if capture_en_cours:
continue
@@ -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: