DEV Community

Cover image for The Agent Left the Terminal. I Wired It to an ESP32.
v. Splicer
v. Splicer

Posted on • Originally published at Medium

The Agent Left the Terminal. I Wired It to an ESP32.

My AI could write perfect firmware. It had just never touched a wire.

For the last 6 months, my agents have lived in the same place: a black terminal window.

They were brilliant in there. They could refactor a codebase at 2am, spin up 5 sub-agents to audit my repo, write tests I was too lazy to write. But they were also completely, physically useless.

I would ask: "Is it cold in the office?"
And it would curl wttr.in.

It had no body. No hands. No way to affect the real world. So I built it one.

The Cage

If you're using OpenClaw / Claude Code properly, you know the feeling. The agent is no longer a chatbot. It's persistent. It has memory, it has cron jobs, it has tools.

The problem is we keep treating it like a software engineer when it could be a physical operator.

I wanted an agent that doesn't just tell me the plant is dry. I wanted one that waters it. One that doesn't just log that my workshop door opened, but turns on the light, buzzes me, and locks it after.

That meant breaking it out of the terminal.

The Idea: Give It GPIO

The architecture is stupid simple when you draw it out:

OpenClaw Agent -> Custom WebSocket Daemon (on my laptop / Pi) -> WiFi -> ESP32 -> Real World

The agent never talks directly to the ESP32. It talks to a daemon I wrote that exposes physical tools as functions. To the agent, turn_on_relay looks just like read_file. It has no idea it's doing hardware.

This is the core of what I'm calling physical agents. I ended up documenting the entire bridge architecture, protocol, and handshake logic in detail in my guide PHYSICAL AGENTS: Bridging OpenClaw with ESP32 Microcontrollers via Custom WebSocket Daemonsnumbpilled.gumroad.com/l/physical-openclaw-agents — because the devil is in the reconnection and state sync.

The Wire: The Daemon

OpenClaw is powerful because you can give it custom tools. So I wrote a tiny Python daemon that runs 24/7 and registers tools like esp32.gpio_write, esp32.read_sensor, esp32.pulse.

# daemon.py - the bridge
from openclaw_daemon import tool
import asyncio
import websockets

ESP32_CLIENTS = set()

@tool("esp32.gpio_write")
async def gpio_write(pin: int, state: int, device_id: str = "workbench_01"):
    """Write to a GPIO pin on the ESP32. The agent uses this like any other tool."""
    payload = {"action": "gpio_write", "pin": pin, "state": state}
    for ws in ESP32_CLIENTS:
        await ws.send_json(payload)
    return {"status": "sent", "pin": pin, "state": state}

async def handler(websocket):
    ESP32_CLIENTS.add(websocket)
    try:
        async for msg in websocket:
            # sensor data coming back from ESP32 -> feed to agent memory
            await memory.ingest(msg)
    finally:
        ESP32_CLIENTS.remove(websocket)
Enter fullscreen mode Exit fullscreen mode

The agent now thinks GPIO is just another API. It doesn't care if it's an ESP32, a Raspberry Pi, or a toaster.

For the persistence layer — keeping the daemon and the agent alive across reboots, crashes, laptop sleeps — I didn't reinvent it. I used the exact pattern from the Paperclip Method: Replace Your Dev Team With Persistent Claude Agentsnumbpilled.gumroad.com/l/paperclip-claude-method. The heartbeat + memory compaction loop is what makes the physical part reliable. Without it, your plant dies when your Mac sleeps.

The Body: The ESP32

The ESP32 side is 80 lines of C++. It just connects to WiFi, connects to the daemon, and listens.

#include <WiFi.h>
#include <WebSocketsClient.h>

WebSocketsClient webSocket;

void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
  if(type == WStype_TEXT){
    // {"action":"gpio_write","pin":2,"state":1}
    DynamicJsonDocument doc(256);
    deserializeJson(doc, payload);
    if(doc["action"] == "gpio_write"){
      digitalWrite(doc["pin"], doc["state"]);
    }
  }
}

void setup(){
  pinMode(2, OUTPUT);
  WiFi.begin(SSID, PASS);
  webSocket.begin("192.168.1.10", 8080, "/");
  webSocket.onEvent(webSocketEvent);
}
Enter fullscreen mode Exit fullscreen mode

That's it. Now when I say in OpenClaw: "Turn on the bench light and tell me the temperature in 10 minutes," it does:

> openclaw tools call esp32.gpio_write pin=2 state=1
> openclaw tools call esp32.read_sensor type="dht22" pin=4

If you're new to the CLI, you'll be living in it. I had The Complete OpenClaw CLI Reference (June 2026) – 120+ Commandsnumbpilled.gumroad.com/l/openclaw-cli-reference-guide — open on my second monitor the entire first weekend. You need openclaw daemon register and openclaw memory more than you think.

The First Blink

The first time it worked, it was anticlimactic and profound.

I typed: the soil sensor says 22% - handle it

My agent checked the log, saw the threshold was 30%, called esp32.gpio_write pin=5 state=1, waited 4 seconds, called pin=5 state=0.

In my office, 10 feet away, a little 5V pump whirred and watered my monstera. The LED on the ESP32 blinked.

The agent left the terminal. For the first time, it did something that wasn't text.

What This Unlocks

Once you have this, you stop thinking about agents as coders.

My agent now:

  • Monitors my 3D printer with a vibration sensor and pauses OctoPrint if it detects a failure pattern
  • Turns on a red light outside my office when I'm in deep work mode (it reads my calendar via OpenClaw)
  • Waters plants, opens my garage, and resets my router when ping fails

It's not home automation. Home automation is if-this-then-that. This is an agent that reasons about physical state. "It's 11pm, the workshop is 58F, and there's motion. That's probably the cat. Don't turn on the alarm, just log it."

We spent 2 years making agents that can use a computer. The next 2 years will be about agents that can touch the world.

If you want to build one, start with a $7 ESP32 DevKit, one LED, and the daemon above. Don't try to build the whole smart home. Give it one hand first.

The terminal was just the crib. It's time to let it walk.


Build notes, full firmware, and the reconnection protocol with queueing are all in the guides linked above. Code for the simple blink example is free on my GitHub.

Top comments (0)