#!/usr/bin/env python3 """ bridge.py — serial <-> UDP relay between the Arduino Uno and Godot (Main.gd) Godot never touches the serial port; it only speaks UDP: * it LISTENS on 127.0.0.1:4242 for telemetry ("x,y,btn") * it SENDS to 127.0.0.1:4243 for LED commands ("L1" / "L0") This bridge sits in the middle: Arduino --serial line "x,y,btn\\n"--> bridge --UDP--> Godot :4242 Godot :4243 --UDP "L1"/"L0"--> bridge --serial--> Arduino Double-click friendly: with no arguments it auto-detects the Arduino port and keeps the window open on exit so you can read any error. Override if needed: python bridge.py --port COM3 python bridge.py --port COM3 --baud 115200 # baud must match the .ino Requires pyserial: pip install pyserial """ import argparse import socket import sys import time try: import serial # pyserial from serial.tools import list_ports except ImportError: print("pyserial not installed. Run: pip install pyserial") input("\nPress Enter to close...") sys.exit(1) TELEMETRY_ADDR = ("127.0.0.1", 4242) # Godot listens here COMMAND_PORT = 4243 # Godot sends LED commands here # Substrings that identify a typical Uno / clone in the port description. PORT_HINTS = ("arduino", "ch340", "usb-serial", "usb serial", "wchusb", "cp210") def pick_port() -> str: """Return a serial port: the obvious Arduino one, else the only one, else ask.""" ports = list(list_ports.comports()) if not ports: raise SystemExit("No serial ports found. Is the Uno plugged in?") for p in ports: blob = f"{p.description} {p.manufacturer or ''}".lower() if any(h in blob for h in PORT_HINTS): print(f"auto-detected {p.device} ({p.description})") return p.device if len(ports) == 1: print(f"using only port {ports[0].device} ({ports[0].description})") return ports[0].device print("Multiple serial ports found:") for i, p in enumerate(ports): print(f" [{i}] {p.device} {p.description}") choice = input("Pick a number: ").strip() return ports[int(choice)].device def run(port: str, baud: int) -> None: # UDP socket: sends telemetry to Godot AND receives LED commands from it. # Bind to all interfaces ("") not just 127.0.0.1 — on some Windows setups a # loopback-only bind won't see packets whose source the sender picked freely. udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) udp.bind(("", COMMAND_PORT)) udp.setblocking(False) # Open the serial port. A short timeout keeps readline() from blocking forever. ser = serial.Serial(port, baud, timeout=0.05) time.sleep(2.0) # Uno auto-resets when the port opens; wait for the sketch to boot ser.reset_input_buffer() print(f"bridge up: {port}@{baud} -> UDP {TELEMETRY_ADDR}, <- UDP :{COMMAND_PORT}") print("leave this window open. Ctrl+C to quit.") try: while True: # 1) Arduino -> Godot: forward each complete telemetry line. line = ser.readline() # b"x,y,btn\r\n" or b"" on timeout if line: text = line.strip() if text: udp.sendto(text, TELEMETRY_ADDR) # 2) Godot -> Arduino: drain any pending LED commands, newest applied last. while True: try: data, src = udp.recvfrom(64) except BlockingIOError: break except ConnectionResetError: # Windows: a prior sendto hit a closed port; ICMP resets recv. Ignore. break cmd = data.strip() if cmd in (b"L1", b"L0"): ser.write(cmd + b"\n") ser.flush() print(f"cmd {cmd.decode()} from {src} -> serial") else: print(f"ignored UDP {data!r} from {src}") except KeyboardInterrupt: pass finally: ser.close() udp.close() print("\nbridge closed.") def main() -> None: ap = argparse.ArgumentParser(description="Arduino <-> Godot serial/UDP bridge") ap.add_argument("--port", help="serial port, e.g. COM3 (auto-detected if omitted)") ap.add_argument("--baud", type=int, default=115200, help="must match JoystickBridge.ino") args = ap.parse_args() try: port = args.port or pick_port() run(port, args.baud) except Exception as e: # Keep the window open so double-click users can actually read the error. print(f"\nERROR: {e}") input("\nPress Enter to close...") if __name__ == "__main__": main()