#!/usr/bin/env python3 """Toggle the USB relay via serial: ./relay 1|0|id [serial_port]. Without an explicit port the relay board is found automatically: every USB serial port is probed with the ID command until one answers. """ import sys import serial import serial.tools.list_ports def find_relay_port() -> str | None: """Find the relay board by probing CH340 ports with the ID command.""" candidates = [ p.device for p in serial.tools.list_ports.comports() if "1A86:7523" in (p.hwid or "") ] for dev in candidates: try: s = serial.Serial() s.port = dev s.baudrate = 115200 s.timeout = 1 s.dtr = False s.rts = False s.open() try: s.readline() # gooi boot-banner weg s.reset_input_buffer() s.write(b"?") s.flush() if s.readline().decode(errors="replace").startswith("ID "): return dev finally: s.close() except serial.SerialException: continue return None def main() -> int: if len(sys.argv) not in (2, 3) or sys.argv[1] not in ("0", "1", "id"): print(f"Usage: {sys.argv[0]} 0|1|id [serial_port]", file=sys.stderr) return 1 command = sys.argv[1] serial_cmd = "?" if command == "id" else command if len(sys.argv) == 3: port = sys.argv[2] else: port = find_relay_port() if port is None: print("ERROR: no relay module found on any USB serial port", file=sys.stderr) return 1 print(f"Using {port}", file=sys.stderr) try: # dtr/rts expliciet uit vóór open: voorkomt (of beperkt) de CH340 # auto-reset (DTR->RST) bij het openen van de poort. pyserial 3.5 # accepteert geen dtr=/rts= kwargs in de constructor, maar de # states worden wél geëerd door open(). s = serial.Serial() s.port = port s.baudrate = 115200 s.timeout = 2 s.dtr = False s.rts = False s.open() except serial.SerialException as exc: print(f"ERROR: cannot open {port}: {exc}", file=sys.stderr) return 1 try: # Als het board toch reset bij open: wacht op de boot-banner en # gooi die weg, zodat de reply niet vervuild raakt. Geen banner # binnen 2 s (timeout, readline geeft b"") = geen reset: door. s.readline() s.reset_input_buffer() s.write(serial_cmd.encode()) s.flush() reply = s.readline().decode(errors="replace").strip() finally: s.close() if reply in ("ON", "OFF") or (command == "id" and reply.startswith("ID ")): print(reply) return 0 print(f"ERROR: no valid reply from board (got {reply!r})", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())