TIL: Turning an HTTP request into a relay click (Garage Door Project, Part 3/5)
Series: Opening my garage door with my phone — Part 3 of 5 Learned: July 2026
With the Arduino clicking on command (Part 1) and the Raspberry Pi finally reachable (Part 2), it was time to connect them:
Laptop/phone → Wi-Fi → extender → Pi (Flask API) → USB serial → Arduino → relay
1. Find the Arduino on the Pi
Plug the Arduino into the Pi with a USB data cable, then:
ls /dev/ttyUSB* /dev/ttyACM* # clones often show up as ttyUSB0
dmesg | tail -n 20 # look for "ch341-uart ... attached to ttyUSB0"
lsusb # is it on the bus at all?
Let your user access serial ports without sudo (log out/in or reboot afterwards):
sudo usermod -a -G dialout $USER
2. Test the serial link by hand
Before writing any Python, talk to the Arduino directly:
sudo apt install screen
screen /dev/ttyUSB0 9600
# type o → relay clicks
# exit with Ctrl+A, then \
3. The bridge: Flask + pyserial
import time
import serial
from flask import Flask, jsonify
SERIAL_PORT = "/dev/ttyUSB0"
ser = serial.Serial(SERIAL_PORT, 9600, timeout=1)
time.sleep(2) # the Arduino resets when the port opens
app = Flask(__name__)
@app.route("/api/toggle", methods=["POST"])
def toggle():
ser.write(b"o")
return jsonify({"status": "triggered"})
@app.route("/api/ping", methods=["GET"])
def ping():
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
The /api/ping endpoint turned out to be very handy later for health checks in Home Assistant.
4. Make it a service
Running the script by hand doesn’t survive a reboot, so it became a systemd unit at /etc/systemd/system/garage.service:
[Unit]
Description=Garage Door API
After=network.target
[Service]
ExecStart=/usr/bin/python3 /home/<user>/garage/garage_server.py
WorkingDirectory=/home/<user>/garage
Restart=always
User=<user>
[Install]
WantedBy=multi-user.target
My first systemctl start failed with Unit garage.service not found.
What I learned: systemd doesn’t watch that folder. After creating or editing a unit file you must run:
sudo systemctl daemon-reload
sudo systemctl enable --now garage.service
sudo systemctl status garage.service
sudo journalctl -u garage.service -n 50 --no-pager # when something's off
5. The serial port can only have one owner
The service started, but couldn’t talk to the Arduino. The culprit was my forgotten screen session from step 2, still holding /dev/ttyUSB0.
screen -ls # list sessions
screen -X -S <session_id> quit # kill one
sudo fuser /dev/ttyUSB0 # who is using the port?
sudo fuser -k /dev/ttyUSB0 # make them stop
sudo systemctl restart garage.service
TIL: a serial port is exclusive. Serial Monitor,
screen, a stray Node script: any of them will silently block your service.
6. The first real success
From my laptop:
curl -X POST http://<pi-ip>:5000/api/toggle
Click. The whole chain worked end to end.
7. A word on security
At this point anyone on my Wi-Fi who knows the Pi’s IP can “press the button”. Easy first improvements:
- Require a secret header (e.g.
X-API-KEY) and return401without it. - Don’t open router ports for remote access; use a private tunnel instead (that’s Part 5).
Takeaways
- Test each link of the chain on its own: Serial Monitor →
screen→curl. daemon-reloadafter every unit file change.- Only one process can own a serial port.
- Add a cheap
/pingendpoint from day one.
Next up: I needed a proper home for Home Assistant, which meant reviving a very old laptop.