"""Pilotage module relais USB HID (LCUS 5131:2007). 4 canaux, protocole LCUS : ON : 0x00 0xA0 0x01 0x00 0x00 0x00 0x00 OFF : 0x00 0xA0 0x00 0x00 0x00 0x00 0x00 Status : 0x00 0xD1 ... Assignation : 1 = Projecteur gauche (NO) 2 = Projecteur droit (NO) 3 = Canon alim (NF — OFF=alimenté, ON=coupé) 4 = Libre """ import ctypes import ctypes.util import logging import threading import time log = logging.getLogger("photobooth.relais") VID = 0x5131 PID = 0x2007 CANAUX = { "projecteur_gauche": {"relay": 1, "mode": "NO"}, "projecteur_droit": {"relay": 2, "mode": "NO"}, "canon": {"relay": 3, "mode": "NF"}, # dummy battery "canon_usb": {"relay": 4, "mode": "NF"}, # VBUS USB } _lib = None _dev = None _lock = threading.Lock() def _charger_hidapi(): global _lib if _lib is not None: return _lib for name in ("hidapi-hidraw", "hidapi-libusb", "hidapi"): path = ctypes.util.find_library(name) if path: _lib = ctypes.CDLL(path) _lib.hid_init() _lib.hid_open.restype = ctypes.c_void_p _lib.hid_read_timeout.restype = ctypes.c_int log.info(f"hidapi chargé : {path}") return _lib try: _lib = ctypes.CDLL("libhidapi-hidraw.so.0") _lib.hid_init() _lib.hid_open.restype = ctypes.c_void_p _lib.hid_read_timeout.restype = ctypes.c_int return _lib except OSError: log.warning("hidapi introuvable — relais désactivés") return None def connecter(): global _dev lib = _charger_hidapi() if not lib: return False with _lock: if _dev: return True _dev = lib.hid_open(VID, PID, None) if not _dev: log.warning("Module relais non trouvé (5131:2007)") _dev = None return False log.info("Module relais connecté") return True def deconnecter(): global _dev with _lock: if _dev and _lib: _lib.hid_close(_dev) _dev = None def est_connecte(): return _dev is not None def _ecrire(data: list[int]): if not _dev or not _lib: return False buf = (ctypes.c_ubyte * 9)(*data) with _lock: res = _lib.hid_write(_dev, buf, 9) return res > 0 def _relay_set(num: int, on: bool): state = 0x01 if on else 0x00 checksum = (0xA0 + num + state) & 0xFF ok = _ecrire([0x00, 0xA0, num, state, checksum, 0x00, 0x00, 0x00, 0x00]) if ok: log.info(f"Relais {num} → {'ON' if on else 'OFF'}") else: log.error(f"Échec écriture relais {num}") return ok def status(): if not _dev or not _lib: return None _ecrire([0x00, 0xD1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) time.sleep(0.1) rbuf = (ctypes.c_ubyte * 64)() with _lock: n = _lib.hid_read_timeout(_dev, rbuf, 64, 500) 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, } def activer(nom: str): cfg = CANAUX.get(nom) if not cfg: return False return _relay_set(cfg["relay"], True) def desactiver(nom: str): cfg = CANAUX.get(nom) if not cfg: return False return _relay_set(cfg["relay"], False) def projecteurs(on: bool): _relay_set(1, on) _relay_set(2, on) log.info(f"Projecteurs {'allumés' if on else 'éteints'}") 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(): raw = status() if raw is None: return {"connecte": False} result = {"connecte": True} for nom, cfg in CANAUX.items(): relay_on = raw.get(f"relay_{cfg['relay']}", False) if cfg["mode"] == "NF": result[nom] = not relay_on else: result[nom] = relay_on return result