Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions arduino/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Python virtual environment
venv/
__pycache__/
*.pyc

# Arduino build artifacts
arduino_claw_uno/build/
210 changes: 210 additions & 0 deletions arduino/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# ESP-Claw β€” Arduino UNO Port

This directory contains the Arduino UNO compatibility layer for the [ESP-Claw](../README.md) AI Agent Framework.

## Architecture: Split-Brain Design

The Arduino UNO (ATmega328P: 2KB SRAM, 32KB Flash, 16MHz) cannot run the full ESP-Claw stack β€” there's simply not enough memory for LLM inference, Lua runtime, event routing, or Wi-Fi networking. Instead, we use a **split-brain architecture**:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Serial (115200 baud) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HOST BRAIN │◄──────────────────────────►│ ARDUINO UNO β”‚
β”‚ β”‚ JSON commands β”‚ (Peripheral Ctrl) β”‚
β”‚ β€’ ESP32 + ESP-Claw β”‚ β”‚ β”‚
β”‚ β€’ PC + Python bridgeβ”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β€’ GPIO read/write β”‚
β”‚ β€’ Raspberry Pi │────►│ {"c":"dw", │──────►│ β€’ Analog read (ADC) β”‚
β”‚ β”‚ β”‚ "p":13, β”‚ β”‚ β€’ PWM output β”‚
β”‚ Handles: β”‚ β”‚ "v":1} β”‚ β”‚ β€’ Servo control β”‚
β”‚ β€’ LLM API calls β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β€’ I2C master β”‚
β”‚ β€’ Event routing β”‚ β”‚ β”‚
β”‚ β€’ Memory / skills β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Status LED blinks β”‚
β”‚ β€’ IM integrations │◄────│ {"ok":true} │◄──────│ to show firmware β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ is running β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

The **host** (ESP32, PC, or Raspberry Pi) runs the AI agent, makes LLM API calls, and sends hardware commands to the UNO over Serial. The **UNO** executes GPIO/ADC/PWM/Servo/I2C operations and returns results.

## Quick Start

### 1. Flash the Arduino UNO

Open `arduino_claw_uno/arduino_claw_uno.ino` in the Arduino IDE (or use arduino-cli):

```bash
# Using arduino-cli
arduino-cli compile --fqbn arduino:avr:uno arduino_claw_uno/
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:uno arduino_claw_uno/
```

### 2. Test with the Python Bridge

```bash
cd host_bridge
pip install -r requirements.txt
python bridge.py --interactive
```

You'll see an interactive prompt:
```
Connected: arduino_uno (ATmega328P)
Capabilities: gpio, adc, pwm, servo, i2c

claw-uno> mode 13 out # Set pin 13 as output
OK
claw-uno> dw 13 1 # Turn on built-in LED
OK
claw-uno> ar 0 # Read analog pin A0
Value: 512 (2.50V)
claw-uno> i2s # Scan I2C bus
Found 2 device(s): 0x3C, 0x68
```

### 3. Use as a Python Library

```python
from bridge import ArduinoClawBridge

uno = ArduinoClawBridge(port="/dev/ttyACM0")
uno.connect()

# GPIO β€” equivalent to ESP-Claw's lua_module_gpio
uno.pin_mode(13, "out")
uno.digital_write(13, 1) # LED on
value = uno.digital_read(7) # Read a button

# Analog β€” equivalent to ESP-Claw's lua_module_adc
reading = uno.analog_read(0) # Read A0 (0-1023)
uno.pwm_write(9, 128) # 50% duty cycle on pin 9

# Servo
uno.servo_attach(9)
uno.servo_write(9, 90) # Move to 90Β°
angle = uno.servo_read(9)

# I2C β€” equivalent to ESP-Claw's lua_module_i2c
devices = uno.i2c_scan()
uno.i2c_write(0x3C, [0x00, 0xAE])
data = uno.i2c_read(0x3C, 2)

uno.disconnect()
```

## Serial Protocol Reference

All commands are newline-delimited JSON. Max message size: 128 bytes.

### Identity & Heartbeat

| Command | Request | Response |
|---|---|---|
| Identify | `{"c":"id"}` | `{"board":"uno","chip":"ATmega328P","sram":2048,"flash":32768,"pins":{"digital":14,"analog":6},"caps":["gpio","adc","pwm","servo","i2c"]}` |
| Ping | `{"c":"pi"}` | `{"ok":true}` |

### GPIO

| Command | Request | Response |
|---|---|---|
| pinMode | `{"c":"pm","p":13,"m":"out"}` | `{"ok":true}` |
| digitalWrite | `{"c":"dw","p":13,"v":1}` | `{"ok":true}` |
| digitalRead | `{"c":"dr","p":7}` | `{"ok":true,"v":1}` |

Modes: `"in"` (INPUT), `"out"` (OUTPUT), `"pu"` (INPUT_PULLUP)

### Analog

| Command | Request | Response |
|---|---|---|
| analogRead | `{"c":"ar","p":0}` | `{"ok":true,"v":512}` |
| analogWrite (PWM) | `{"c":"pw","p":9,"v":128}` | `{"ok":true}` |

- ADC pins: A0–A5 (pass 0–5 or 14–19)
- PWM pins: 3, 5, 6, 9, 10, 11 (8-bit: 0–255)

### Servo

| Command | Request | Response |
|---|---|---|
| Attach | `{"c":"sa","p":9}` | `{"ok":true}` |
| Write angle | `{"c":"sv","p":9,"v":90}` | `{"ok":true}` |
| Read angle | `{"c":"sr","p":9}` | `{"ok":true,"v":90}` |
| Detach | `{"c":"sd","p":9}` | `{"ok":true}` |

Max 4 simultaneous servos (SRAM limitation).

### I2C

| Command | Request | Response |
|---|---|---|
| Write | `{"c":"i2w","a":60,"d":"48656C"}` | `{"ok":true}` |
| Read | `{"c":"i2r","a":60,"n":2}` | `{"ok":true,"d":"4F4B"}` |
| Scan | `{"c":"i2s"}` | `{"ok":true,"devs":[60,104]}` |

- Data is hex-encoded (e.g., `"48656C"` = bytes `[0x48, 0x65, 0x6C]`)
- Max 16 bytes per I2C write, max 16 bytes per I2C read
- I2C uses pins A4 (SDA) and A5 (SCL)

### Error Responses

All errors follow this format:
```json
{"ok":false,"e":"error message"}
```

Common errors: `"bad pin"`, `"pin reserved"`, `"bad mode"`, `"not pwm pin"`, `"not adc pin"`, `"no servo slot"`, `"not attached"`, `"i2c nack"`, `"overflow"`, `"parse fail"`

## Mapping to ESP-Claw Components

| ESP-Claw Component | Arduino UNO Equivalent |
|---|---|
| `lua_module_gpio` | `cmd_gpio.cpp` β€” GPIO via serial |
| `lua_module_adc` | `cmd_analog.cpp` β€” ADC via serial |
| `lua_module_mcpwm` | `cmd_analog.cpp` β€” PWM via serial |
| `lua_module_i2c` | `cmd_i2c.cpp` β€” I2C via serial |
| `board_info.yaml` | `board_defs.h` β€” UNO pin definitions |
| `claw_core` | Stays on host (ESP32 or PC) |
| `claw_event_router` | Stays on host |
| `claw_memory` | Stays on host |
| `cap_im_*` | Stays on host |

## Pin Map

```
Arduino UNO R3 Pin Map
─────────────────────────────────
D0 β€” Serial RX (RESERVED)
D1 β€” Serial TX (RESERVED)
D2 β€” Digital I/O
D3 β€” Digital I/O + PWM ⚑
D4 β€” Digital I/O
D5 β€” Digital I/O + PWM ⚑
D6 β€” Digital I/O + PWM ⚑
D7 β€” Digital I/O
D8 β€” Digital I/O
D9 β€” Digital I/O + PWM ⚑ + Servo
D10 β€” Digital I/O + PWM ⚑ + SPI SS
D11 β€” Digital I/O + PWM ⚑ + SPI MOSI
D12 β€” Digital I/O + SPI MISO
D13 β€” Digital I/O + Built-in LED πŸ’‘
A0 β€” Analog Input
A1 β€” Analog Input
A2 β€” Analog Input
A3 β€” Analog Input
A4 β€” Analog Input + I2C SDA
A5 β€” Analog Input + I2C SCL
─────────────────────────────────
```

## Memory Budget

The firmware is designed to fit within the UNO's tight constraints:

| Resource | Budget | Usage (approx) |
|---|---|---|
| Flash (32 KB) | 32,256 bytes | ~8-10 KB |
| SRAM (2 KB) | 2,048 bytes | ~700-900 bytes |
| EEPROM (1 KB) | 1,024 bytes | 0 (reserved for future) |

## License

Apache-2.0 β€” same as the main ESP-Claw project.
109 changes: 109 additions & 0 deletions arduino/arduino_claw_uno/arduino_claw_uno.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* arduino_claw_uno.ino β€” ESP-Claw Arduino UNO Peripheral Controller
*
* This sketch turns an Arduino UNO into a remote peripheral controller
* for the ESP-Claw AI Agent framework. The UNO exposes its hardware
* capabilities (GPIO, ADC, PWM, Servo, I2C) through a compact JSON
* serial protocol.
*
* Architecture: Split-Brain
* β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Serial/UART β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
* β”‚ HOST BRAIN │◄───────────────►│ ARDUINO UNO β”‚
* β”‚ (ESP32 or β”‚ JSON commands β”‚ (this code) β”‚
* β”‚ PC bridge) β”‚ β”‚ β”‚
* β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
*
* The host runs the full ESP-Claw stack (LLM, event router, memory,
* skills, capabilities) and sends hardware commands to the UNO over
* serial. The UNO executes them and returns results.
*
* Supported commands:
* id β€” Board identification
* pi β€” Ping / heartbeat
* pm β€” pinMode
* dw β€” digitalWrite
* dr β€” digitalRead
* ar β€” analogRead (ADC)
* pw β€” analogWrite (PWM)
* sa β€” Servo attach
* sv β€” Servo write angle
* sr β€” Servo read angle
* sd β€” Servo detach
* i2w β€” I2C write
* i2r β€” I2C read
* i2s β€” I2C bus scan
*
* Protocol: newline-delimited JSON, 115200 baud
* Example:
* Send: {"c":"dw","p":13,"v":1}
* Receive: {"ok":true}
*
* SPDX-License-Identifier: Apache-2.0
*/

#include "claw_serial_protocol.h"
#include "board_defs.h"

/* ───────────────────── Status LED ───────────────────── */
#define STATUS_LED_PIN BOARD_LED_BUILTIN
#define STATUS_BLINK_MS 1000 /* Blink interval when idle */
#define ACTIVITY_FLASH_MS 50 /* Brief flash on command */

static unsigned long s_last_blink = 0;
static bool s_led_state = false;
static unsigned long s_activity_until = 0;

/**
* Blink the built-in LED to indicate the firmware is running.
* Briefly holds LED on when a command is processed.
*/
static void update_status_led(bool had_activity)
{
unsigned long now = millis();

if (had_activity) {
s_activity_until = now + ACTIVITY_FLASH_MS;
digitalWrite(STATUS_LED_PIN, HIGH);
return;
}

/* Activity flash still active */
if (now < s_activity_until) {
return;
}

/* Normal idle blink */
if ((now - s_last_blink) >= STATUS_BLINK_MS) {
s_last_blink = now;
s_led_state = !s_led_state;
digitalWrite(STATUS_LED_PIN, s_led_state ? HIGH : LOW);
}
}

/* ───────────────────── Arduino Entry Points ───────────────────── */

void setup()
{
/* Initialize status LED */
pinMode(STATUS_LED_PIN, OUTPUT);
digitalWrite(STATUS_LED_PIN, HIGH);

/* Initialize serial protocol (opens Serial at 115200) */
proto_init();

/* Send startup identification */
delay(100); /* Brief delay for serial to stabilize */
proto_respond_identify();

/* Status LED off after init */
digitalWrite(STATUS_LED_PIN, LOW);
}

void loop()
{
/* Poll for serial commands (non-blocking) */
bool had_command = proto_poll();

/* Update status LED */
update_status_led(had_command);
}
Loading