Documentation Index Fetch the complete documentation index at: https://mintlify.com/zeroclaw-labs/zeroclaw/llms.txt
Use this file to discover all available pages before exploring further.
ZeroClaw can control physical hardware through peripherals — microcontrollers, Raspberry Pi GPIO, sensors, and actuators. Peripherals expose their capabilities as tools that the agent can invoke.
Overview
Peripherals implement the Peripheral trait, which provides:
Connection management : Connect to hardware over serial, native GPIO, or network
Tool exposure : Each peripheral exposes tools the agent can use
Health monitoring : Check device connectivity and status
#[async_trait]
pub trait Peripheral : Send + Sync {
fn name ( & self ) -> & str ;
fn board_type ( & self ) -> & str ;
async fn connect ( & mut self ) -> Result <()>;
async fn disconnect ( & mut self ) -> Result <()>;
async fn health_check ( & self ) -> bool ;
fn tools ( & self ) -> Vec < Box < dyn Tool >>;
}
Supported Hardware
ZeroClaw supports multiple hardware platforms:
Raspberry Pi GPIO Native GPIO control via rppal (BCM pin numbering)
STM32 Nucleo Serial-connected microcontrollers (Nucleo-F401RE, etc.)
Arduino Arduino Uno and compatible boards
Arduino Uno Q WiFi-connected Arduino via Bridge app
Step-by-Step Setup
Build with hardware support:
cargo build --features hardware
cargo build --features "hardware,peripheral-rpi"
Add peripherals to config.toml:
[ peripherals ]
enabled = true
# Raspberry Pi GPIO (native)
[[ peripherals . boards ]]
board = "rpi-gpio"
transport = "native"
# STM32 Nucleo over serial
[[ peripherals . boards ]]
board = "nucleo-f401re"
transport = "serial"
path = "/dev/ttyACM0"
baud = 115200
# Arduino Uno over serial
[[ peripherals . boards ]]
board = "arduino-uno"
transport = "serial"
path = "/dev/ttyUSB0"
baud = 115200
# Arduino Uno Q over WiFi bridge
[[ peripherals . boards ]]
board = "uno-q"
transport = "bridge"
Flash Firmware (for microcontrollers)
zeroclaw peripheral flash --port /dev/ttyUSB0
zeroclaw peripheral flash-nucleo
List configured peripherals:
Configured peripherals:
rpi-gpio native (native)
nucleo-f401re serial /dev/ttyACM0
arduino-uno serial /dev/ttyUSB0
Start the agent and use hardware capabilities:
zeroclaw chat "Turn on GPIO pin 17"
zeroclaw chat "Read the value from GPIO pin 27"
Raspberry Pi GPIO Example
Here’s how the Raspberry Pi GPIO peripheral is implemented (src/peripherals/rpi.rs):
use crate :: peripherals :: traits :: Peripheral ;
use crate :: tools :: { Tool , ToolResult };
use async_trait :: async_trait;
use serde_json :: {json, Value };
pub struct RpiGpioPeripheral {
board : PeripheralBoardConfig ,
}
impl RpiGpioPeripheral {
pub fn new ( board : PeripheralBoardConfig ) -> Self {
Self { board }
}
pub async fn connect_from_config ( board : & PeripheralBoardConfig ) -> Result < Self > {
let mut peripheral = Self :: new ( board . clone ());
peripheral . connect () . await ? ;
Ok ( peripheral )
}
}
#[async_trait]
impl Peripheral for RpiGpioPeripheral {
fn name ( & self ) -> & str {
& self . board . board
}
fn board_type ( & self ) -> & str {
"rpi-gpio"
}
async fn connect ( & mut self ) -> Result <()> {
// Verify GPIO is accessible
let result = tokio :: task :: spawn_blocking ( || rppal :: gpio :: Gpio :: new ()) . await ?? ;
drop ( result );
Ok (())
}
async fn disconnect ( & mut self ) -> Result <()> {
Ok (())
}
async fn health_check ( & self ) -> bool {
tokio :: task :: spawn_blocking ( || rppal :: gpio :: Gpio :: new () . is_ok ())
. await
. unwrap_or ( false )
}
fn tools ( & self ) -> Vec < Box < dyn Tool >> {
vec! [
Box :: new ( RpiGpioReadTool ),
Box :: new ( RpiGpioWriteTool ),
]
}
}
struct RpiGpioReadTool ;
#[async_trait]
impl Tool for RpiGpioReadTool {
fn name ( & self ) -> & str {
"gpio_read"
}
fn description ( & self ) -> & str {
"Read the value (0 or 1) of a GPIO pin on Raspberry Pi. Uses BCM pin numbers (e.g. 17, 27)."
}
fn parameters_schema ( & self ) -> Value {
json! ({
"type" : "object" ,
"properties" : {
"pin" : {
"type" : "integer" ,
"description" : "BCM GPIO pin number (e.g. 17, 27)"
}
},
"required" : [ "pin" ]
})
}
async fn execute ( & self , args : Value ) -> Result < ToolResult > {
let pin = args
. get ( "pin" )
. and_then ( | v | v . as_u64 ())
. ok_or_else ( || anyhow :: anyhow! ( "Missing 'pin' parameter" )) ? ;
let pin_u8 = pin as u8 ;
let value = tokio :: task :: spawn_blocking ( move || {
let gpio = rppal :: gpio :: Gpio :: new () ? ;
let pin = gpio . get ( pin_u8 ) ?. into_input ();
Ok :: < _ , anyhow :: Error >( match pin . read () {
rppal :: gpio :: Level :: Low => 0 ,
rppal :: gpio :: Level :: High => 1 ,
})
})
. await ?? ;
Ok ( ToolResult {
success : true ,
output : format! ( "pin {} = {}" , pin , value ),
error : None ,
})
}
}
struct RpiGpioWriteTool ;
#[async_trait]
impl Tool for RpiGpioWriteTool {
fn name ( & self ) -> & str {
"gpio_write"
}
fn description ( & self ) -> & str {
"Set a GPIO pin high (1) or low (0) on Raspberry Pi. Uses BCM pin numbers."
}
fn parameters_schema ( & self ) -> Value {
json! ({
"type" : "object" ,
"properties" : {
"pin" : {
"type" : "integer" ,
"description" : "BCM GPIO pin number"
},
"value" : {
"type" : "integer" ,
"description" : "0 for low, 1 for high"
}
},
"required" : [ "pin" , "value" ]
})
}
async fn execute ( & self , args : Value ) -> Result < ToolResult > {
let pin = args
. get ( "pin" )
. and_then ( | v | v . as_u64 ())
. ok_or_else ( || anyhow :: anyhow! ( "Missing 'pin' parameter" )) ? ;
let value = args
. get ( "value" )
. and_then ( | v | v . as_u64 ())
. ok_or_else ( || anyhow :: anyhow! ( "Missing 'value' parameter" )) ? ;
let pin_u8 = pin as u8 ;
let level = match value {
0 => rppal :: gpio :: Level :: Low ,
_ => rppal :: gpio :: Level :: High ,
};
tokio :: task :: spawn_blocking ( move || {
let gpio = rppal :: gpio :: Gpio :: new () ? ;
let mut pin = gpio . get ( pin_u8 ) ?. into_output ();
pin . write ( level );
Ok :: < _ , anyhow :: Error >(())
})
. await ?? ;
Ok ( ToolResult {
success : true ,
output : format! ( "pin {} = {}" , pin , value ),
error : None ,
})
}
}
Serial Peripheral Communication
For STM32 and Arduino boards, ZeroClaw uses a serial protocol to communicate with firmware:
Commands are JSON-based over serial:
{ "cmd" : "gpio_read" , "pin" : 13 }
{ "cmd" : "gpio_write" , "pin" : 13 , "value" : 1 }
{ "cmd" : "capabilities" }
Responses:
{ "ok" : true , "value" : 1 }
{ "ok" : false , "error" : "Invalid pin" }
{ "capabilities" : [ "gpio_read" , "gpio_write" , "analog_read" ]}
Serial Transport
From src/peripherals/serial.rs:
pub struct SerialTransport {
port : Arc < Mutex < Box < dyn serialport :: SerialPort >>>,
}
impl SerialTransport {
pub fn new ( path : & str , baud : u32 ) -> Result < Self > {
let port = serialport :: new ( path , baud )
. timeout ( Duration :: from_secs ( 2 ))
. open () ? ;
Ok ( Self {
port : Arc :: new ( Mutex :: new ( port )),
})
}
pub async fn send_command ( & self , cmd : & str ) -> Result < String > {
let mut port = self . port . lock () . await ;
// Send command
port . write_all ( cmd . as_bytes ()) ? ;
port . write_all ( b" \n " ) ? ;
port . flush () ? ;
// Read response
let mut buffer = Vec :: new ();
let mut byte = [ 0 u8 ; 1 ];
loop {
port . read_exact ( & mut byte ) ? ;
if byte [ 0 ] == b' \n ' {
break ;
}
buffer . push ( byte [ 0 ]);
}
Ok ( String :: from_utf8 ( buffer ) ? )
}
}
Arduino Uno Q (WiFi Bridge)
For wireless control, use the Arduino Uno Q Bridge:
Setup Bridge
zeroclaw peripheral setup-uno-q --host "192.168.1.100"
This configures the Bridge app to forward commands to the Arduino.
Bridge Configuration
[[ peripherals . boards ]]
board = "uno-q"
transport = "bridge"
The bridge exposes the same GPIO tools but communicates over HTTP instead of serial.
Creating Custom Peripherals
Implement the Peripheral Trait
use crate :: peripherals :: traits :: Peripheral ;
use crate :: tools :: { Tool , ToolResult };
use async_trait :: async_trait;
pub struct MyCustomPeripheral {
device_path : String ,
connected : bool ,
}
impl MyCustomPeripheral {
pub fn new ( device_path : String ) -> Self {
Self {
device_path ,
connected : false ,
}
}
}
#[async_trait]
impl Peripheral for MyCustomPeripheral {
fn name ( & self ) -> & str {
"my-custom-peripheral"
}
fn board_type ( & self ) -> & str {
"custom"
}
async fn connect ( & mut self ) -> Result <()> {
// Initialize your hardware connection
self . connected = true ;
Ok (())
}
async fn disconnect ( & mut self ) -> Result <()> {
self . connected = false ;
Ok (())
}
async fn health_check ( & self ) -> bool {
self . connected
}
fn tools ( & self ) -> Vec < Box < dyn Tool >> {
vec! [
Box :: new ( MyCustomTool ),
]
}
}
struct MyCustomTool ;
#[async_trait]
impl Tool for MyCustomTool {
fn name ( & self ) -> & str {
"custom_sensor_read"
}
fn description ( & self ) -> & str {
"Read data from custom sensor"
}
fn parameters_schema ( & self ) -> Value {
json! ({
"type" : "object" ,
"properties" : {
"sensor_id" : {
"type" : "integer" ,
"description" : "Sensor ID (0-7)"
}
},
"required" : [ "sensor_id" ]
})
}
async fn execute ( & self , args : Value ) -> Result < ToolResult > {
let sensor_id = args [ "sensor_id" ] . as_u64 ()
. ok_or_else ( || anyhow :: anyhow! ( "Missing sensor_id" )) ? ;
// Read from your hardware
let value = read_sensor ( sensor_id as u8 ) . await ? ;
Ok ( ToolResult {
success : true ,
output : format! ( "Sensor {} = {}" , sensor_id , value ),
error : None ,
})
}
}
Add to src/peripherals/mod.rs:
pub async fn create_peripheral_tools ( config : & PeripheralsConfig ) -> Result < Vec < Box < dyn Tool >>> {
let mut tools = Vec :: new ();
for board in & config . boards {
if board . board == "my-custom" {
let peripheral = MyCustomPeripheral :: new ( board . path . clone ());
tools . extend ( peripheral . tools ());
}
}
Ok ( tools )
}
Best Practices
Use appropriate transports
Serial : STM32, Arduino, ESP32 (reliable, wired)
Native : Raspberry Pi GPIO (fastest, direct access)
Bridge/Network : Wireless devices (flexible, may have latency)
Handle connection failures gracefully
async fn connect ( & mut self ) -> Result <()> {
for attempt in 1 ..= 3 {
match self . try_connect () . await {
Ok (()) => return Ok (()),
Err ( e ) if attempt < 3 => {
tracing :: warn! ( "Connection attempt {attempt} failed: {e}" );
tokio :: time :: sleep ( Duration :: from_secs ( 1 )) . await ;
}
Err ( e ) => return Err ( e ),
}
}
unreachable! ()
}
Validate pin numbers and ranges
if pin > 40 {
return Ok ( ToolResult {
success : false ,
output : String :: new (),
error : Some ( format! ( "Invalid pin: {pin} (must be 0-40)" )),
});
}
Use blocking tasks for synchronous hardware APIs
// GPIO libraries are often blocking
let value = tokio :: task :: spawn_blocking ( move || {
let gpio = rppal :: gpio :: Gpio :: new () ? ;
let pin = gpio . get ( pin_num ) ? ;
Ok ( pin . read ())
}) . await ?? ;
Add firmware version checks
async fn connect ( & mut self ) -> Result <()> {
let version = self . query_firmware_version () . await ? ;
if version < MINIMUM_FIRMWARE_VERSION {
anyhow :: bail! (
"Firmware too old: {version}. Please flash version {MINIMUM_FIRMWARE_VERSION} or later."
);
}
Ok (())
}
Troubleshooting
Permission denied on /dev/ttyACM0 or /dev/ttyUSB0
Add your user to the dialout group: sudo usermod -a -G dialout $USER
# Log out and back in
GPIO access denied on Raspberry Pi
Run with appropriate permissions or add user to gpio group: sudo usermod -a -G gpio $USER
Serial communication hangs
Check baud rate, connection, and firmware: # Test serial connection
screen /dev/ttyACM0 115200
# Send test command
{ "cmd" : "capabilities" }
Firmware not responding after flash
Press reset button on the board after flashing: zeroclaw peripheral flash --port /dev/ttyUSB0
# Press reset button on Arduino
Next Steps
Creating Tools Learn how to create custom tools
Gateway Setup Expose your agent via HTTP