Godot + Arduino series: companion project code

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.
This commit is contained in:
tyler parker
2026-08-09 21:14:55 +00:00
commit bb1d70c620
21 changed files with 1010 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# Godot editor cache (regenerated locally)
.godot/
*.import
export_presets.cfg
# Python
__pycache__/
*.pyc
.venv/
venv/
# OS / editor
.DS_Store
Thumbs.db
*.swp
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Codebycandle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+33
View File
@@ -0,0 +1,33 @@
# Godot + Arduino — blog project code
Companion code for the *"Godot Isn't Just for Games"* series on the
[Codebycandle blog](https://codebycandle.com/blog) — using the Godot 4 game
engine as a hardware control panel for an Arduino Uno.
Each folder is a self-contained project for one article: the Arduino sketch, the
Python serial↔UDP bridge, and the Godot project.
| Part | Article | Folder | What it does |
|------|---------|--------|--------------|
| 1 | [Your First Arduino Round-Trip](https://codebycandle.com/blog/godot-arduino-led-first-light) | [`godot-arduino-1-led-first-light/`](./godot-arduino-1-led-first-light) | A Godot button lights a real LED on D13, over a Python-to-UDP bridge. |
| 2 | A Live Joystick Dashboard *(coming soon)* | [`godot-arduino-2-joystick-dashboard/`](./godot-arduino-2-joystick-dashboard) | A KY-023 joystick streams `x,y,btn` telemetry into a glowing Godot XY pad. |
## The shape of it
```
Arduino <--USB serial--> bridge.py <--UDP 127.0.0.1--> Godot
```
Godot has no serial support, so a ~40-line Python bridge owns the COM port and
relays each line over UDP on localhost, in both directions. See each part's
README for wiring and run steps.
## Requirements
- Arduino Uno (or compatible) + the Arduino IDE
- Python 3 with `pyserial` (`pip install pyserial`)
- [Godot 4](https://godotengine.org/) (standard build — no GDExtension needed)
## License
Released under the [MIT License](./LICENSE) — use it freely.
+30
View File
@@ -0,0 +1,30 @@
# Part 1 — Your First Arduino Round-Trip
Code for [*"Godot Isn't Just for Games: Your First Arduino Round-Trip"*](https://codebycandle.com/blog/godot-arduino-led-first-light).
A button in a Godot 4 scene reaches down a USB cable — through a Python-to-UDP
bridge — and lights a real LED on an Arduino Uno.
## Contents
```
arduino/
LedBasics/LedBasics.ino # reads L1/L0 off serial, drives the LED on D13
bridge/bridge.py # serial <-> UDP relay (run this, leave it running)
bridge/ledtest.py # quick standalone LED blink test (no Godot)
godot/
ArduinoGodot/ # Godot 4 project: one styled toggle button
```
## Wiring
D13 → 220 Ω resistor → LED long leg (anode); LED short leg (cathode) → GND.
No breadboard? The Uno's onboard LED is already on D13.
## Run it
1. **Flash** `arduino/LedBasics/LedBasics.ino` to the Uno (Arduino IDE, 115200 baud).
2. **Bridge:** `pip install pyserial`, then `python arduino/bridge/bridge.py`. Leave it running.
3. **Godot:** open `godot/ArduinoGodot/` in Godot 4, press Play, click the button.
The bridge auto-detects the board and reconnects if the cable pops out.
@@ -0,0 +1,44 @@
/*
* LedBasics.ino — Part 1: the minimal round trip.
*
* Godot sends "L1" / "L0" over USB serial (via the Python bridge) and this
* sketch toggles an LED. No sensors yet — Part 2 (JoystickBridge.ino) adds the
* joystick telemetry going the other way.
*
* PROTOCOL
* IN (host -> Uno): "L1" = LED on, "L0" = LED off (newline-terminated)
*
* WIRING
* D13 -> 220 ohm resistor -> LED anode (+, long leg)
* LED cathode (-, short leg) -> GND
* (D13 is also the Uno's onboard LED, so it flashes during every upload —
* a free "is this pin alive" check before you've wired anything.)
*/
const uint8_t PIN_LED = 13; // LED via 220 ohm resistor (also the onboard LED)
char cmdBuf[8];
uint8_t cmdLen = 0;
void setup() {
Serial.begin(115200);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW);
}
void loop() {
// Commands in from Godot: "L1" / "L0", one per line.
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (cmdLen >= 2 && cmdBuf[0] == 'L') {
digitalWrite(PIN_LED, cmdBuf[1] == '1' ? HIGH : LOW);
}
cmdLen = 0;
} else if (cmdLen < sizeof(cmdBuf) - 1) {
cmdBuf[cmdLen++] = c;
} else {
cmdLen = 0; // overflow guard: drop the runt line
}
}
}
@@ -0,0 +1,129 @@
#!/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()
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""
ledtest.py — isolate the bridge's receive path from Godot.
Sends a single LED command straight to the bridge's command port, exactly like
Godot does. If the running bridge prints "cmd L1 ... -> serial" (and the LED
lights), the bridge's receive side is fine and any failure is on Godot's end.
If the bridge stays silent even for this, the problem is the bridge/binding.
Usage (with bridge.py already running):
python ledtest.py L1 # turn LED on
python ledtest.py L0 # turn LED off
"""
import socket
import sys
msg = (sys.argv[1] if len(sys.argv) > 1 else "L1").encode()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(msg, ("127.0.0.1", 4243))
print(f"sent {msg!r} to 127.0.0.1:4243")
@@ -0,0 +1,24 @@
extends Control
# Part 1 — the minimal round trip: a Godot button that toggles an LED on the
# Arduino. Only the OUTBOUND (command) half of the link is used here; Part 2
# adds inbound telemetry from the joystick.
const COMMAND_ADDR := "127.0.0.1"
const COMMAND_PORT := 4243
@onready var led_button: Button = $Panel/LedButton
var _udp_out := PacketPeerUDP.new()
var _led_on := false
func _ready() -> void:
# To SEND with PacketPeerUDP in Godot 4, use set_dest_address — NOT
# connect_to_host, which is for receiving and silently drops outbound packets.
_udp_out.set_dest_address(COMMAND_ADDR, COMMAND_PORT)
led_button.pressed.connect(_on_led_pressed)
func _on_led_pressed() -> void:
_led_on = not _led_on
_udp_out.put_packet(("L1" if _led_on else "L0").to_utf8_buffer())
led_button.text = "LED: ON" if _led_on else "LED: OFF"
@@ -0,0 +1 @@
uid://ctgyyk0it7dpd
@@ -0,0 +1,91 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://LedBasics.gd" id="1_led"]
[sub_resource type="StyleBoxFlat" id="bg"]
bg_color = Color(0.043, 0.055, 0.078, 1)
[sub_resource type="StyleBoxFlat" id="btn_normal"]
bg_color = Color(0.11, 0.145, 0.204, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.2, 0.6, 1, 0.5)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="btn_hover"]
bg_color = Color(0.145, 0.196, 0.278, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.3, 0.7, 1, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="btn_pressed"]
bg_color = Color(0.2, 0.6, 1, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="Main" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_led")
[node name="Panel" type="Panel" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_styles/panel = SubResource("bg")
[node name="Title" type="Label" parent="Panel"]
layout_mode = 0
offset_left = 40.0
offset_top = 96.0
offset_right = 480.0
offset_bottom = 130.0
theme_override_colors/font_color = Color(0.85, 0.93, 1, 1)
theme_override_font_sizes/font_size = 24
text = "GODOT + ARDUINO: LED"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Subtitle" type="Label" parent="Panel"]
layout_mode = 0
offset_left = 40.0
offset_top = 132.0
offset_right = 480.0
offset_bottom = 152.0
theme_override_colors/font_color = Color(0.4, 0.55, 0.7, 1)
theme_override_font_sizes/font_size = 12
text = "· click to reach down the wire ·"
horizontal_alignment = 1
vertical_alignment = 1
[node name="LedButton" type="Button" parent="Panel"]
layout_mode = 0
offset_left = 150.0
offset_top = 220.0
offset_right = 370.0
offset_bottom = 284.0
theme_override_colors/font_color = Color(0.85, 0.93, 1, 1)
theme_override_colors/font_hover_color = Color(1, 1, 1, 1)
theme_override_colors/font_pressed_color = Color(0.043, 0.055, 0.078, 1)
theme_override_font_sizes/font_size = 18
theme_override_styles/normal = SubResource("btn_normal")
theme_override_styles/hover = SubResource("btn_hover")
theme_override_styles/pressed = SubResource("btn_pressed")
theme_override_styles/focus = SubResource("btn_normal")
text = "LED: OFF"
@@ -0,0 +1,24 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="ArduinoGodot"
run/main_scene="res://LedBasics.tscn"
config/features=PackedStringArray("4.7")
[display]
window/size/viewport_width=520
window/size/viewport_height=610
[dotnet]
project/assembly_name="ArduinoGodot"
@@ -0,0 +1,28 @@
# Part 2 — A Live Joystick Dashboard
Code for *"Godot Isn't Just for Games: A Live Joystick Dashboard"* **(article coming soon).**
Builds on [Part 1](../godot-arduino-1-led-first-light): a 2-axis KY-023 joystick streams
its position up the same bridge into a Godot console — a glowing dot tracks the
stick across an XY pad in real time, with the LED button still working.
## Contents
```
arduino/
JoystickBridge/JoystickBridge.ino # streams "x,y,btn" + still drives the LED
bridge/bridge.py # same serial <-> UDP relay as Part 1
godot/
ArduinoGodot/ # Godot 4 project: XY pad + dot + LED button
```
## Wiring (adds to Part 1)
KY-023 joystick: `+5V`→5V, `GND`→GND, `VRx`→A0, `VRy`→A1, `SW`→D2. Keep the
LED from Part 1 on D13.
## Run it
1. **Flash** `arduino/JoystickBridge/JoystickBridge.ino` to the Uno.
2. **Bridge:** `python arduino/bridge/bridge.py` (leave running).
3. **Godot:** open `godot/ArduinoGodot/` in Godot 4 and press Play — move the stick.
@@ -0,0 +1,79 @@
/*
* JoystickBridge.ino — Arduino Uno <-> Godot (ArduinoGodot/Main.gd)
*
* Talks plain serial lines to the UDP<->serial bridge process, which forwards
* telemetry to Godot on UDP 4242 and relays LED commands from UDP 4243.
*
* PROTOCOL (must match Main.gd)
* OUT (Uno -> host): "x,y,btn\n" raw 10-bit ADC, e.g. "512,498,1"
* btn = 1 when the stick's push-button is pressed.
* IN (host -> Uno): "L1" = LED on, "L0" = LED off (newline optional)
*
* WIRING (KY-023 2-axis analog joystick + external LED)
* Joystick +5V / VCC -> 5V
* Joystick GND -> GND
* Joystick VRx -> A0
* Joystick VRy -> A1
* Joystick SW -> D2 (uses internal pull-up; pressed = LOW)
*
* LED: D13 -> 220 ohm resistor -> LED anode (+, long leg)
* LED cathode (-, short leg) -> GND
* (D13 is also the Uno's onboard LED, so it flashes during every upload —
* a free "is this pin alive" check. 220-330 ohm keeps it near ~15 mA.)
*/
const uint8_t PIN_VRX = A0; // joystick X axis
const uint8_t PIN_VRY = A1; // joystick Y axis
const uint8_t PIN_SW = 2; // joystick push-button (active LOW)
const uint8_t PIN_LED = 13; // external LED through a 220 ohm resistor (also onboard LED)
const unsigned long BAUD = 115200; // must match the bridge process
const unsigned long SEND_MS = 16; // ~60 Hz telemetry
unsigned long lastSend = 0;
char cmdBuf[8];
uint8_t cmdLen = 0;
void setup() {
Serial.begin(BAUD);
pinMode(PIN_SW, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW);
}
void loop() {
handleCommands();
unsigned long now = millis();
if (now - lastSend >= SEND_MS) {
lastSend = now;
int x = analogRead(PIN_VRX); // 0..1023
int y = analogRead(PIN_VRY); // 0..1023
int btn = (digitalRead(PIN_SW) == LOW) // pull-up: LOW == pressed
? 1 : 0; // Godot wants 1 = pressed
// "x,y,btn\n"
Serial.print(x);
Serial.print(',');
Serial.print(y);
Serial.print(',');
Serial.println(btn);
}
}
// Parse newline-terminated commands. Recognizes "L1" / "L0".
void handleCommands() {
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (cmdLen >= 2 && cmdBuf[0] == 'L') {
digitalWrite(PIN_LED, cmdBuf[1] == '1' ? HIGH : LOW);
}
cmdLen = 0;
} else if (cmdLen < sizeof(cmdBuf) - 1) {
cmdBuf[cmdLen++] = c;
} else {
cmdLen = 0; // overflow guard: drop the runt line
}
}
}
@@ -0,0 +1,129 @@
#!/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()
@@ -0,0 +1,24 @@
extends Control
# Part 1 — the minimal round trip: a Godot button that toggles an LED on the
# Arduino. Only the OUTBOUND (command) half of the link is used here; Part 2
# adds inbound telemetry from the joystick.
const COMMAND_ADDR := "127.0.0.1"
const COMMAND_PORT := 4243
@onready var led_button: Button = $Panel/LedButton
var _udp_out := PacketPeerUDP.new()
var _led_on := false
func _ready() -> void:
# To SEND with PacketPeerUDP in Godot 4, use set_dest_address — NOT
# connect_to_host, which is for receiving and silently drops outbound packets.
_udp_out.set_dest_address(COMMAND_ADDR, COMMAND_PORT)
led_button.pressed.connect(_on_led_pressed)
func _on_led_pressed() -> void:
_led_on = not _led_on
_udp_out.put_packet(("L1" if _led_on else "L0").to_utf8_buffer())
led_button.text = "LED: ON" if _led_on else "LED: OFF"
@@ -0,0 +1 @@
uid://ctgyyk0it7dpd
@@ -0,0 +1,91 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://LedBasics.gd" id="1_led"]
[sub_resource type="StyleBoxFlat" id="bg"]
bg_color = Color(0.043, 0.055, 0.078, 1)
[sub_resource type="StyleBoxFlat" id="btn_normal"]
bg_color = Color(0.11, 0.145, 0.204, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.2, 0.6, 1, 0.5)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="btn_hover"]
bg_color = Color(0.145, 0.196, 0.278, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.3, 0.7, 1, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="btn_pressed"]
bg_color = Color(0.2, 0.6, 1, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="Main" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_led")
[node name="Panel" type="Panel" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_styles/panel = SubResource("bg")
[node name="Title" type="Label" parent="Panel"]
layout_mode = 0
offset_left = 40.0
offset_top = 96.0
offset_right = 480.0
offset_bottom = 130.0
theme_override_colors/font_color = Color(0.85, 0.93, 1, 1)
theme_override_font_sizes/font_size = 24
text = "GODOT + ARDUINO: LED"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Subtitle" type="Label" parent="Panel"]
layout_mode = 0
offset_left = 40.0
offset_top = 132.0
offset_right = 480.0
offset_bottom = 152.0
theme_override_colors/font_color = Color(0.4, 0.55, 0.7, 1)
theme_override_font_sizes/font_size = 12
text = "· click to reach down the wire ·"
horizontal_alignment = 1
vertical_alignment = 1
[node name="LedButton" type="Button" parent="Panel"]
layout_mode = 0
offset_left = 150.0
offset_top = 220.0
offset_right = 370.0
offset_bottom = 284.0
theme_override_colors/font_color = Color(0.85, 0.93, 1, 1)
theme_override_colors/font_hover_color = Color(1, 1, 1, 1)
theme_override_colors/font_pressed_color = Color(0.043, 0.055, 0.078, 1)
theme_override_font_sizes/font_size = 18
theme_override_styles/normal = SubResource("btn_normal")
theme_override_styles/hover = SubResource("btn_hover")
theme_override_styles/pressed = SubResource("btn_pressed")
theme_override_styles/focus = SubResource("btn_normal")
text = "LED: OFF"
@@ -0,0 +1,63 @@
extends Control
const TELEMETRY_PORT := 4242
const COMMAND_ADDR := "127.0.0.1"
const COMMAND_PORT := 4243
const ADC_MAX := 1023.0
const SMOOTH_SPEED := 12.0 # higher = snappier, lower = floatier
@onready var led_button: Button = $Panel/LedButton
@onready var pad: Panel = $Panel/Pad
@onready var dot: ColorRect = $Panel/Pad/Dot
var _udp_in := PacketPeerUDP.new()
var _udp_out := PacketPeerUDP.new()
var _target_x := 512.0 # rest near center so the dot starts in the middle
var _target_y := 512.0
var _smoothed_x := 512.0
var _smoothed_y := 512.0
var _btn_pressed := false
var _led_on := false
const COLOR_DOT_IDLE := Color(0.3, 0.7, 1.0) # cyan
const COLOR_DOT_PRESSED := Color(0.2, 1.0, 0.4) # green
func _ready() -> void:
_udp_in.bind(TELEMETRY_PORT, "127.0.0.1")
# set_dest_address is the reliable way to *send* with PacketPeerUDP in Godot 4;
# connect_to_host is aimed at receiving and can silently drop outbound packets.
_udp_out.set_dest_address(COMMAND_ADDR, COMMAND_PORT)
led_button.pressed.connect(_on_led_pressed)
func _process(delta: float) -> void:
# Drain the queue; the newest packet wins. Format: "x,y,btn" e.g. "512,498,0"
while _udp_in.get_available_packet_count() > 0:
var parts := _udp_in.get_packet().get_string_from_utf8().split(",")
if parts.size() >= 3 and parts[0].is_valid_int() and parts[1].is_valid_int():
_target_x = float(parts[0])
_target_y = float(parts[1])
_btn_pressed = parts[2].strip_edges() == "1"
# Frame-rate-independent smoothing so the dot glides instead of twitching.
var t := 1.0 - exp(-SMOOTH_SPEED * delta)
_smoothed_x = lerpf(_smoothed_x, _target_x, t)
_smoothed_y = lerpf(_smoothed_y, _target_y, t)
# Normalize raw ADC (0..1023) to 0..1.
var nx := clampf(_smoothed_x / ADC_MAX, 0.0, 1.0)
var ny := clampf(_smoothed_y / ADC_MAX, 0.0, 1.0)
# Move the dot within the pad. Invert Y so pushing the stick UP moves the dot UP.
# (If it feels backwards, change "1.0 - ny" to "ny", or swap for X below.)
var travel := pad.size - dot.size
dot.position = Vector2(nx * travel.x, (1.0 - ny) * travel.y)
# Reflect the stick's push-button by recoloring the dot.
dot.color = COLOR_DOT_PRESSED if _btn_pressed else COLOR_DOT_IDLE
func _on_led_pressed() -> void:
_led_on = not _led_on
_udp_out.put_packet(("L1" if _led_on else "L0").to_utf8_buffer())
led_button.text = "LED: ON" if _led_on else "LED: OFF"
@@ -0,0 +1 @@
uid://d103i6ghi3ydl
@@ -0,0 +1,137 @@
[gd_scene format=3 uid="uid://b81ornfxnvxxo"]
[ext_resource type="Script" uid="uid://d103i6ghi3ydl" path="res://Main.gd" id="1_main"]
[sub_resource type="StyleBoxFlat" id="bg"]
bg_color = Color(0.043, 0.055, 0.078, 1)
[sub_resource type="StyleBoxFlat" id="pad"]
bg_color = Color(0.071, 0.09, 0.125, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.2, 0.6, 1, 0.55)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
shadow_color = Color(0.2, 0.6, 1, 0.15)
shadow_size = 12
[sub_resource type="StyleBoxFlat" id="btn_normal"]
bg_color = Color(0.11, 0.145, 0.204, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.2, 0.6, 1, 0.5)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="btn_pressed"]
bg_color = Color(0.2, 0.6, 1, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="btn_hover"]
bg_color = Color(0.145, 0.196, 0.278, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.3, 0.7, 1, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="Main" type="Control" unique_id=2088197079]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_main")
[node name="Panel" type="Panel" parent="." unique_id=2059422118]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("bg")
[node name="Title" type="Label" parent="Panel" unique_id=1798584764]
layout_mode = 0
offset_left = 40.0
offset_top = 28.0
offset_right = 480.0
offset_bottom = 58.0
theme_override_colors/font_color = Color(0.85, 0.93, 1, 1)
theme_override_font_sizes/font_size = 22
text = "GODOT + ARDUINO DEMO"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Subtitle" type="Label" parent="Panel" unique_id=462472763]
layout_mode = 0
offset_left = 40.0
offset_top = 60.0
offset_right = 480.0
offset_bottom = 78.0
theme_override_colors/font_color = Color(0.4, 0.55, 0.7, 1)
theme_override_font_sizes/font_size = 12
text = "· real-time serial link ·"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Pad" type="Panel" parent="Panel" unique_id=1034082861]
layout_mode = 0
offset_left = 70.0
offset_top = 104.0
offset_right = 450.0
offset_bottom = 484.0
theme_override_styles/panel = SubResource("pad")
[node name="CrosshairV" type="ColorRect" parent="Panel/Pad" unique_id=1237114789]
layout_mode = 0
offset_left = 189.0
offset_right = 191.0
offset_bottom = 380.0
color = Color(0.2, 0.6, 1, 0.12)
[node name="CrosshairH" type="ColorRect" parent="Panel/Pad" unique_id=1265685762]
layout_mode = 0
offset_top = 189.0
offset_right = 380.0
offset_bottom = 191.0
color = Color(0.2, 0.6, 1, 0.12)
[node name="Dot" type="ColorRect" parent="Panel/Pad" unique_id=2003621157]
layout_mode = 0
offset_right = 28.0
offset_bottom = 28.0
color = Color(0.3, 0.7, 1, 1)
[node name="LedButton" type="Button" parent="Panel" unique_id=1325564653]
layout_mode = 0
offset_left = 160.0
offset_top = 516.0
offset_right = 360.0
offset_bottom = 566.0
theme_override_colors/font_color = Color(0.85, 0.93, 1, 1)
theme_override_colors/font_pressed_color = Color(0.043, 0.055, 0.078, 1)
theme_override_colors/font_hover_color = Color(1, 1, 1, 1)
theme_override_font_sizes/font_size = 18
theme_override_styles/normal = SubResource("btn_normal")
theme_override_styles/pressed = SubResource("btn_pressed")
theme_override_styles/hover = SubResource("btn_hover")
theme_override_styles/focus = SubResource("btn_normal")
text = "LED: OFF"
@@ -0,0 +1,24 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="ArduinoGodot"
run/main_scene="res://Main.tscn"
config/features=PackedStringArray("4.7")
[display]
window/size/viewport_width=520
window/size/viewport_height=610
[dotnet]
project/assembly_name="ArduinoGodot"