The shared bridge shipped in both posts named Main.gd and
JoystickBridge.ino in its docstring and --baud help — so the part-1
(LED basics) download referenced files absent from its own project.
Neutralize the wording ("Godot", "the .ino sketch") so the same bridge
reads correctly in both downloads.
130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
bridge.py — serial <-> UDP relay between the Arduino Uno and Godot
|
|
|
|
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 the .ino sketch")
|
|
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()
|