An LLM-powered agent that reads live sensor data from an ESP32 and controls a physical relay via natural language commands using Claude API tool-calling.
Built at AgileTech Studio, Lahore — September 2026
Team: Yawar Abbas (bridge server + agent) · Ahtesham (React frontend)
"An AI agent you can talk to — 'turn on the fan, it's getting warm' — that reasons over live sensor data and actually flips a physical relay switch. Not a simulation."
| Typical IoT project | This project |
|---|---|
| Hardcoded if/else rules | LLM agent reasons before acting |
| App-based manual toggle | Natural language chat interface |
| Simulated data | Real physical hardware feedback loop |
| One-way data reading | Agent reads sensors AND controls devices |
| Single data point decisions | Rolling sensor memory + trend analysis |
| No safety controls | 30-second relay cooldown guardrail |
User: "It's getting warm, do something about it."
│
▼
┌─────────────────────────┐
│ Claude AI Agent │
│ reasons over context │
│ decides which tools │
│ to call │
└───────────┬─────────────┘
│ tool: get_sensor_reading()
▼
┌─────────────────────────┐ HTTP GET /sensor
│ FastAPI Bridge Server │ ──────────────────▶ ┌─────────────────┐
│ - exposes tools │ │ ESP32-WROOM-32 │
│ - talks to ESP32 │ ◀────────────────── │ DHT22 on GPIO4 │
│ - logs to SQLite │ {temp, humidity} │ Relay on GPIO5 │
└───────────┬─────────────┘ └─────────────────┘
│ agent reasons: "27°C, rising trend — turning on fan"
│ tool: set_relay("on")
▼
┌─────────────────────────┐ HTTP POST /relay
│ FastAPI Bridge Server │ ──────────────────▶ Fan turns ON physically
└───────────┬─────────────┘
▼
Agent replies: "It's 27°C and rising — I've turned the fan on."
│
▼
┌─────────────────────────┐
│ React Frontend │
│ Chat UI + Live Sensor │
│ Panel + Trend Chart │
└─────────────────────────┘
| Layer | Technology |
|---|---|
| Firmware | MicroPython on ESP32-WROOM-32 |
| Bridge Server | FastAPI (Python) |
| AI Agent | Claude API — tool-calling (claude-sonnet-4-6) |
| Frontend | React 18 + Vite + Tailwind CSS + Recharts |
| Logging | SQLite (aiosqlite) |
| Hardware | ESP32, DHT22, Songle Relay Module |
- ESP32-WROOM-32 (DevKitC)
- DHT22 temperature and humidity sensor
- Songle SRD-05VDC-SL-C relay module
- Breadboard + jumper wires
- Micro-USB cable (data capable — not charge-only)
| Component | ESP32 Pin | Notes |
|---|---|---|
| DHT22 VCC | 3V3 | Right side, pin 1 |
| DHT22 GND | GND | Right side, pin 2 |
| DHT22 DATA | GPIO4 | Right side, 5th pin from bottom |
| Relay VCC | 3V3 | Shared 3.3V rail |
| Relay GND | GND | Shared ground rail |
| Relay IN | GPIO5 | Right side, 8th pin from bottom |
Important: Relay is active LOW.
relay.value(0) = ON · relay.value(1) = OFF
Download and install from:
https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers
Plug in the ESP32 via Micro-USB. Confirm it appears as a COM port in Device Manager (e.g. COM3).
pip install esptoolDownload the ESP32 generic .bin from:
https://micropython.org/download/ESP32_GENERIC/
Use the latest stable release.
esptool --chip esp32 --port COM3 erase_flashReplace COM3 with your actual port number.
esptool --chip esp32 --port COM3 --baud 460800 write_flash -z 0x1000 firmware.binReplace firmware.bin with the full path to your downloaded .bin file.
Flash offset must be 0x1000 for ESP32-WROOM-32 — using 0x0 will fail.
Open any serial monitor (Thonny, PuTTY, or Arduino IDE) at 115200 baud.
Press the EN (reset) button on the ESP32.
You should see the MicroPython REPL prompt >>>.
Download from: https://thonny.org
- Open Thonny
- Go to Tools → Options → Interpreter
- Select MicroPython (ESP32)
- Select your COM port
- Click OK
- Open
firmware/main.pyin Thonny - Edit the WiFi credentials at the top:
SSID = 'your_wifi_name'
PASSWORD = 'your_wifi_password'Note: ESP32-WROOM-32 supports 2.4GHz only — will not connect to 5GHz networks.
3. Go to File → Save As
4. Select MicroPython device
5. Save as main.py
Press the EN (reset) button on the ESP32.
Open Thonny's serial monitor — you should see:
Connecting to WiFi....
Connected! IP: 192.168.x.x
HTTP server listening on 192.168.x.x port 80
# Read sensor
curl http://192.168.x.x/sensor
# Turn relay on
curl -X POST http://192.168.x.x/relay `
-H "Content-Type: application/json" `
-d '{"state": "on"}'
# Turn relay off
curl -X POST http://192.168.x.x/relay `
-H "Content-Type: application/json" `
-d '{"state": "off"}'- Python 3.11+
- Node.js 18+
- Git
git clone https://github.com/yawar2518/agentic-iot-controller.git
cd agentic-iot-controllerCopy-Item .env.example bridge\.envOpen bridge/.env and fill in:
ESP32_IP=192.168.x.x # IP shown in ESP32 serial monitor
ESP32_PORT=80
ANTHROPIC_API_KEY=your_key_here
CLAUDE_MODEL=claude-sonnet-4-6
BRIDGE_HOST=0.0.0.0
BRIDGE_PORT=8000cd bridge
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r requirements.txtuvicorn main:app --host 0.0.0.0 --port 8000 --reloadServer starts at: http://localhost:8000
cd ..\frontend
npm install
npm run devFrontend starts at: http://localhost:5173
Base URL: http://localhost:8000
| Method | Endpoint | Body | Response |
|---|---|---|---|
| POST | /chat | {"message": "string"} |
{"reply": "string", "actions": []} |
| GET | /sensor | none | {"temperature": 27.4, "humidity": 63.4} |
| GET | /logs | none | [{"timestamp": "...", "temp": 27.4, "relay": "on"}] |
| GET | /relay/status | none | {"relay": "on" or "off"} |
- Relay rate limiting — minimum 30 second cooldown between toggles. Agent explains the wait time in natural language if blocked.
- Sensor memory — rolling window of last 10 readings. Agent reasons over trend (rising/falling/stable), not just a single snapshot.
- Context injection — every request includes current relay state, cooldown status, and sensor trend so Claude never acts blind.
- Always explains — agent always states why it acted, referencing actual sensor values.
To control a real fan or lamp:
- Work with the power cord unplugged from the wall
- Cut only the live wire (brown wire)
- Strip both cut ends ~1cm
- Connect one end to relay COM terminal
- Connect other end to relay NO (Normally Open) terminal
- Plug the device into the socket end, cord into the wall
When relay is OFF → COM-NO circuit open → device has no power
When relay is ON → COM-NO circuit closes → device gets 220V
Always unplug before touching any wiring.
agentic-iot-controller/
├── firmware/
│ └── main.py # MicroPython HTTP server — /sensor + /relay
├── bridge/
│ ├── config.py # Settings from .env
│ ├── tools.py # get_sensor_reading(), set_relay() + rate limiting
│ ├── agent.py # Claude tool-calling loop + context injection
│ ├── logger.py # SQLite async logging
│ ├── memory.py # Rolling sensor window + trend analysis
│ ├── main.py # FastAPI server — four endpoints
│ └── requirements.txt
├── frontend/
│ └── src/
│ ├── components/
│ │ ├── ChatWindow.jsx
│ │ ├── SensorPanel.jsx
│ │ ├── RelayStatus.jsx
│ │ └── TrendChart.jsx
│ └── App.jsx
├── docs/
│ └── ahtesham-workflow.md
├── .env.example
├── .gitignore
└── README.md
- Phase 1 — Hardware bringup (DHT22 + relay wired and confirmed)
- Phase 2 — ESP32 HTTP API (/sensor + /relay live)
- Phase 3 — FastAPI bridge server + Claude agent tool-calling
- Phase 4 — Guardrails + rolling sensor memory
- Phase 5 — React frontend (in progress)
- Phase 6 — Integration + demo video
| Name | Role |
|---|---|
| Yawar Abbas | Bridge server, Claude agent, SQLite logging, firmware |
| Ahtesham | React frontend, live sensor panel, trend charts |
PM: Muhammad Arslan — AgileTech Studio, Lahore