Self-contained project code for the "Godot Isn't Just for Games" series: Arduino sketches, the Python serial<->UDP bridge, and the Godot 4 projects, one folder per article.
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 (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()
|