From bb1d70c620263446882ea4a9af789b728cc5cf9b Mon Sep 17 00:00:00 2001 From: tyler parker Date: Sun, 9 Aug 2026 20:51:08 +0000 Subject: [PATCH] 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. --- .gitignore | 15 ++ LICENSE | 21 +++ README.md | 33 +++++ godot-arduino-1-led-first-light/README.md | 30 ++++ .../arduino/LedBasics/LedBasics.ino | 44 ++++++ .../arduino/bridge/bridge.py | 129 +++++++++++++++++ .../arduino/bridge/ledtest.py | 21 +++ .../godot/ArduinoGodot/LedBasics.gd | 24 +++ .../godot/ArduinoGodot/LedBasics.gd.uid | 1 + .../godot/ArduinoGodot/LedBasics.tscn | 91 ++++++++++++ .../godot/ArduinoGodot/project.godot | 24 +++ godot-arduino-2-joystick-dashboard/README.md | 28 ++++ .../arduino/JoystickBridge/JoystickBridge.ino | 79 ++++++++++ .../arduino/bridge/bridge.py | 129 +++++++++++++++++ .../godot/ArduinoGodot/LedBasics.gd | 24 +++ .../godot/ArduinoGodot/LedBasics.gd.uid | 1 + .../godot/ArduinoGodot/LedBasics.tscn | 91 ++++++++++++ .../godot/ArduinoGodot/Main.gd | 63 ++++++++ .../godot/ArduinoGodot/Main.gd.uid | 1 + .../godot/ArduinoGodot/Main.tscn | 137 ++++++++++++++++++ .../godot/ArduinoGodot/project.godot | 24 +++ 21 files changed, 1010 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 godot-arduino-1-led-first-light/README.md create mode 100644 godot-arduino-1-led-first-light/arduino/LedBasics/LedBasics.ino create mode 100644 godot-arduino-1-led-first-light/arduino/bridge/bridge.py create mode 100644 godot-arduino-1-led-first-light/arduino/bridge/ledtest.py create mode 100644 godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd create mode 100644 godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd.uid create mode 100644 godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.tscn create mode 100644 godot-arduino-1-led-first-light/godot/ArduinoGodot/project.godot create mode 100644 godot-arduino-2-joystick-dashboard/README.md create mode 100644 godot-arduino-2-joystick-dashboard/arduino/JoystickBridge/JoystickBridge.ino create mode 100644 godot-arduino-2-joystick-dashboard/arduino/bridge/bridge.py create mode 100644 godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd create mode 100644 godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd.uid create mode 100644 godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.tscn create mode 100644 godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd create mode 100644 godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd.uid create mode 100644 godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.tscn create mode 100644 godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/project.godot diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd51a3a --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..89a7022 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9cb937 --- /dev/null +++ b/README.md @@ -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. diff --git a/godot-arduino-1-led-first-light/README.md b/godot-arduino-1-led-first-light/README.md new file mode 100644 index 0000000..f0e5903 --- /dev/null +++ b/godot-arduino-1-led-first-light/README.md @@ -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. diff --git a/godot-arduino-1-led-first-light/arduino/LedBasics/LedBasics.ino b/godot-arduino-1-led-first-light/arduino/LedBasics/LedBasics.ino new file mode 100644 index 0000000..849b042 --- /dev/null +++ b/godot-arduino-1-led-first-light/arduino/LedBasics/LedBasics.ino @@ -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 + } + } +} diff --git a/godot-arduino-1-led-first-light/arduino/bridge/bridge.py b/godot-arduino-1-led-first-light/arduino/bridge/bridge.py new file mode 100644 index 0000000..c031948 --- /dev/null +++ b/godot-arduino-1-led-first-light/arduino/bridge/bridge.py @@ -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() diff --git a/godot-arduino-1-led-first-light/arduino/bridge/ledtest.py b/godot-arduino-1-led-first-light/arduino/bridge/ledtest.py new file mode 100644 index 0000000..9c17886 --- /dev/null +++ b/godot-arduino-1-led-first-light/arduino/bridge/ledtest.py @@ -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") diff --git a/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd b/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd new file mode 100644 index 0000000..5cdbcd6 --- /dev/null +++ b/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd @@ -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" diff --git a/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd.uid b/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd.uid new file mode 100644 index 0000000..bbb23a4 --- /dev/null +++ b/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.gd.uid @@ -0,0 +1 @@ +uid://ctgyyk0it7dpd diff --git a/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.tscn b/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.tscn new file mode 100644 index 0000000..828e6de --- /dev/null +++ b/godot-arduino-1-led-first-light/godot/ArduinoGodot/LedBasics.tscn @@ -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" diff --git a/godot-arduino-1-led-first-light/godot/ArduinoGodot/project.godot b/godot-arduino-1-led-first-light/godot/ArduinoGodot/project.godot new file mode 100644 index 0000000..f6ac270 --- /dev/null +++ b/godot-arduino-1-led-first-light/godot/ArduinoGodot/project.godot @@ -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" diff --git a/godot-arduino-2-joystick-dashboard/README.md b/godot-arduino-2-joystick-dashboard/README.md new file mode 100644 index 0000000..62f45d3 --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/README.md @@ -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. diff --git a/godot-arduino-2-joystick-dashboard/arduino/JoystickBridge/JoystickBridge.ino b/godot-arduino-2-joystick-dashboard/arduino/JoystickBridge/JoystickBridge.ino new file mode 100644 index 0000000..24572b9 --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/arduino/JoystickBridge/JoystickBridge.ino @@ -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 + } + } +} diff --git a/godot-arduino-2-joystick-dashboard/arduino/bridge/bridge.py b/godot-arduino-2-joystick-dashboard/arduino/bridge/bridge.py new file mode 100644 index 0000000..c031948 --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/arduino/bridge/bridge.py @@ -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() diff --git a/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd new file mode 100644 index 0000000..5cdbcd6 --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd @@ -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" diff --git a/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd.uid b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd.uid new file mode 100644 index 0000000..bbb23a4 --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.gd.uid @@ -0,0 +1 @@ +uid://ctgyyk0it7dpd diff --git a/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.tscn b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.tscn new file mode 100644 index 0000000..828e6de --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/LedBasics.tscn @@ -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" diff --git a/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd new file mode 100644 index 0000000..0f5798f --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd @@ -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" diff --git a/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd.uid b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd.uid new file mode 100644 index 0000000..a23d1ca --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.gd.uid @@ -0,0 +1 @@ +uid://d103i6ghi3ydl diff --git a/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.tscn b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.tscn new file mode 100644 index 0000000..e998a29 --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/Main.tscn @@ -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" diff --git a/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/project.godot b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/project.godot new file mode 100644 index 0000000..c253ab9 --- /dev/null +++ b/godot-arduino-2-joystick-dashboard/godot/ArduinoGodot/project.godot @@ -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"