IOnode - NATS-Addressable Hardware Nodes for OpenClaw
IOnode turns any ESP32 into a NATS-addressable hardware node. Every GPIO pin, ADC channel, sensor, and actuator becomes reachable via request/reply over NATS.
- Website & docs: https://ionode.io
- GitHub: https://github.com/M64GitHub/IOnode
- Firmware version: 0.3.0
Prerequisites
Required
natsCLI - https://github.com/nats-io/natscli (must be in PATH)- NATS server accessible from both OpenClaw and IOnode devices (default port 4222)
- One or more IOnode devices on the same network
Recommended: ionode CLI
The ionode CLI provides 28 fleet management commands with formatted, colored
output. Always check if it's installed before using raw NATS commands:
command -v ionode >/dev/null 2>&1 && echo "ionode CLI available" || echo "ionode CLI not found"
If ionode is NOT installed, tell the user:
The
ionodeCLI is not installed. It provides fleet management, colored output, and easier commands. To install it:# From the IOnode repo git clone https://github.com/M64GitHub/IOnode sudo ln -sf "$(pwd)/IOnode/cli/ionode" /usr/local/bin/ionodeI can use raw
nats reqcommands in the meantime - everything still works.
If ionode IS installed, prefer it over raw nats req for all operations.
Environment
Set IONODE_NATS_URL if NATS is not at localhost:4222:
export IONODE_NATS_URL="nats://192.168.1.100:4222"
The ionode CLI and raw nats commands both respect this variable.
Quick Reference: ionode CLI
# Discovery & monitoring
ionode discover # find all nodes on the network
ionode ls # compact fleet table
ionode info <device> # deep dive - health, HAL, devices
ionode status <device> # quick health check
ionode watch # live heartbeat + event stream
ionode watch --tag <tag> # monitor a tagged group
# Read sensors
ionode read <device> <sensor> # read a registered sensor
ionode read <device> chip_temp # chip temperature (always available)
# Control actuators
ionode write <device> <actuator> <value> # set an actuator
ionode set <device> <actuator> <value> # alias for write
# Raw hardware access
ionode gpio <device> <pin> get # read GPIO pin
ionode gpio <device> <pin> set <0|1> # write GPIO pin
ionode adc <device> <pin> # read ADC (0-4095, with bar graph)
ionode pwm <device> <pin> set <0-255> # set PWM output
ionode pwm <device> <pin> get # read last PWM value
# Fleet configuration
ionode tag <device> <tag> # set fleet tag
ionode device add <dev> <name> <kind> <pin> [--unit X] [--inverted]
ionode device remove <device> <name> # remove a device
ionode event set <dev> <sensor> --above|--below <threshold> [--cooldown <sec>]
ionode event clear <device> <sensor> # remove threshold event
ionode event list <device> # list configured events
Full CLI reference: https://github.com/M64GitHub/IOnode/blob/main/docs/CLI.md
Quick Reference: Raw NATS (fallback)
Use these when the ionode CLI is not installed. All subjects use request/reply.
NATS_URL="${IONODE_NATS_URL:-nats://localhost:4222}"
# Discovery
nats req _ion.discover "" --replies=0 --timeout=3s --server "$NATS_URL"
nats req <device>.capabilities "" --server "$NATS_URL"
# Sensors
nats req <device>.hal.<sensor> "" --server "$NATS_URL"
nats req <device>.hal.system.temperature "" --server "$NATS_URL"
# Actuators
nats req <device>.hal.<actuator>.set "<value>" --server "$NATS_URL"
# GPIO / ADC / PWM
nats req <device>.hal.gpio.<pin>.get "" --server "$NATS_URL"
nats req <device>.hal.gpio.<pin>.set "1" --server "$NATS_URL"
nats req <device>.hal.adc.<pin>.read "" --server "$NATS_URL"
nats req <device>.hal.pwm.<pin>.set "128" --server "$NATS_URL"
# System
nats req <device>.hal.system.heap "" --server "$NATS_URL"
nats req <device>.hal.system.uptime "" --server "$NATS_URL"
nats req <device>.hal.device.list "" --server "$NATS_URL"
Core HAL Subjects
These are always available on every IOnode - no registration required.
GPIO
ionode gpio <device> <pin> get # → 0 or 1
ionode gpio <device> <pin> set <0|1> # → ok
ADC (Analog Read)
ionode adc <device> <pin> # → 0-4095 (12-bit, with bar graph)
PWM Output
ionode pwm <device> <pin> set <0-255> # → ok
ionode pwm <device> <pin> get # → last written value
UART / Serial Bridge
nats req <device>.hal.uart.read "" # → last received line
nats req <device>.hal.uart.write "GET_DATA" # → ok
Requires a serial_text device registered on the node.
System Info
ionode status <device> # shows all of the below in one formatted view
# Or individually:
nats req <device>.hal.system.temperature "" # chip temp °C
nats req <device>.hal.system.heap "" # free heap bytes
nats req <device>.hal.system.uptime "" # seconds since boot
nats req <device>.hal.system.rssi "" # WiFi signal dBm
nats req <device>.hal.system.reset_reason "" # last reset reason
nats req <device>.hal.system.nats_reconnects "" # reconnect count
Device List
ionode info <device> # formatted device list + system info
nats req <device>.hal.device.list "" # raw JSON array
Registered Sensors & Actuators
Devices registered on the node (via web UI, CLI, or NATS) are auto-routed
under {device}.hal.{name}:
ionode read ionode-01 room_temp # → 24.5
ionode write ionode-01 fan 1 # → ok
ionode read ionode-01 light # → 67.2
Supported Device Kinds
Sensors:
| Kind | Notes |
|---|---|
digital_in |
digitalRead → 0/1 |
analog_in |
analogRead → 0–4095 |
ntc_10k |
NTC thermistor → °C, EMA-smoothed |
ldr |
Light sensor → 0–100% |
internal_temp |
Chip temperature, always pre-registered |
clock_hour |
Current hour (0–23) from NTP |
clock_minute |
Current minute (0–59) from NTP |
clock_hhmm |
HHMM format (e.g. 1430) |
nats_value |
Subscribes to a NATS subject, stores last value |
serial_text |
Reads lines from UART1, parses numeric value |
i2c_generic |
Raw I2C register read, configurable addr/reg/len/scale |
i2c_bme280 |
BME280 temp/humidity/pressure, channel via pin 0/1/2 |
i2c_bh1750 |
BH1750 ambient light (lux) |
i2c_sht31 |
SHT31 temp/humidity, channel via pin 0/1 |
i2c_ads1115 |
ADS1115 16-bit ADC, channel via pin 0-3 |
dht11_temp |
DHT11 temperature (integer, 0–50°C) |
dht11_humi |
DHT11 humidity (integer, 20–80% RH) |
dht22_temp |
DHT22 temperature (0.1° resolution, -40–80°C) |
dht22_humi |
DHT22 humidity (0.1° resolution, 0–100% RH) |
Actuators:
| Kind | Notes |
|---|---|
digital_out |
digitalWrite |
relay |
digitalWrite with optional inversion |
pwm |
analogWrite 0-255 |
rgb_led |
Built-in RGB LED, packed 0xRRGGBB value |
ssd1306 |
SSD1306 OLED text display, template-driven |
sh1106 |
SH1106 OLED text display, template-driven |
Pre-registered Devices (always available)
chip_temp- internal chip temperature in °Cclock_hour- current hour (0–23)clock_minute- current minute (0–59)clock_hhmm- time as HHMM (e.g. 1830)rgb_led- built-in RGB LED (boards with RGB LED only)
Registering New Devices
# Via CLI (preferred)
ionode device add ionode-01 room_temp ntc_10k 2 --unit C
ionode device add ionode-01 fan relay 8 --inverted
# Via NATS
nats req ionode-01.config.device.add '{"n":"room_temp","k":"ntc_10k","p":2,"u":"C"}'
Fleet Management
Tags & Group Discovery
ionode tag ionode-01 greenhouse
ionode discover --tag greenhouse # all greenhouse nodes
# Raw NATS
nats req ionode-01.config.tag.set 'greenhouse'
nats req _ion.group.greenhouse '' # all tagged nodes respond
Tags update live - no reboot needed.
Health Heartbeats
Nodes publish periodic health reports to _ion.heartbeat (default: every 60s):
ionode watch # live heartbeat + event stream
ionode watch --tag greenhouse # filter by tag
Heartbeat JSON includes: device, tag, version, uptime, heap, rssi, nats_reconnects, sensors, actuators, events_fired.
Threshold Events
Sensors fire NATS notifications when values cross a threshold:
ionode event set ionode-01 chip_temp --above 45 --cooldown 30
ionode event list ionode-01
ionode watch # events appear in the stream
# Raw NATS
nats req ionode-01.config.event.set '{"n":"chip_temp","t":45,"d":"above","cd":30}'
nats sub 'ionode-01.events.>'
Edge-detected with configurable cooldown. Events persist across reboots.
Remote Configuration
All configuration is available over NATS - add/remove devices, set tags, configure events, rename nodes. No reflash needed.
Full protocol reference: https://github.com/M64GitHub/IOnode/blob/main/docs/NATS-API.md
Fleet Dashboard (Web)
A single-file HTML dashboard connects to NATS via WebSocket for live fleet
monitoring. Located at web/dashboard/index.html in the IOnode repo.
Requires NATS WebSocket enabled:
websocket {
port: 8080
no_tls: true
}
Automation Patterns
IOnode has no on-device rule engine. Automation logic runs here in OpenClaw - as shell scripts, background jobs, or monitoring loops. This is intentional: the logic lives where it can be updated and extended without reflashing.
Polling loop
while true; do
TEMP=$(ionode read ionode-01 room_temp)
if (( $(echo "$TEMP > 28" | bc -l) )); then
ionode write ionode-01 fan 1
else
ionode write ionode-01 fan 0
fi
sleep 30
done
Cross-node automation
LIGHT=$(ionode read ionode-01 light)
if (( $(echo "$LIGHT < 20" | bc -l) )); then
ionode gpio ionode-02 8 set 1
fi
Cross-domain: CI status → physical LED
nats sub "ci.build.status" | while read -r line; do
if echo "$line" | grep -q '"status":"failed"'; then
ionode gpio ionode-01 4 set 1 # red warning LED
else
ionode gpio ionode-01 4 set 0
fi
done
Fleet-wide operations
# Read temperature from every node
ionode discover --json | jq -r '.[].device' | while read node; do
echo -n "$node: "
ionode read "$node" chip_temp
done
# Tag all nodes in a batch
for i in $(seq -w 1 10); do
ionode tag "ionode-$i" greenhouse
done
RGB LED control
# Red (0xFF0000 = 16711680)
ionode write ionode-01 rgb_led 16711680
# Green (0x00FF00 = 65280)
ionode write ionode-01 rgb_led 65280
# Off
ionode write ionode-01 rgb_led 0
WireClaw Interoperability
IOnode and WireClaw share the same .hal. protocol.
OpenClaw talks to both identically for hardware access:
ionode read wireclaw-01 chip_temp # works on WireClaw too
ionode gpio wireclaw-01 4 get # same HAL, same commands
For WireClaw-specific features (on-device AI rules, Telegram, tool-calling loop), use the WireClaw skill instead.
I2C Bus Access
ionode i2c <device> scan # scan I2C bus
ionode i2c <device> detect <addr> # check if device exists
ionode i2c <device> read <addr> --reg N --len N # read register
ionode i2c <device> write <addr> --reg N --data N,N # write register
ionode i2c <device> recover # bus recovery
I2C Sensors & Display
# BME280 - 3 channels: temp (pin=0), humidity (pin=1), pressure (pin=2)
ionode device add <dev> bme_temp i2c_bme280 --channel 0 --unit C --i2c-addr 118
ionode device add <dev> bme_humi i2c_bme280 --channel 1 --unit % --i2c-addr 118
# BH1750 light sensor
ionode device add <dev> light i2c_bh1750 --unit lux --i2c-addr 35
# SSD1306 OLED display
ionode device add <dev> display ssd1306 0 --i2c-addr 60 --template "T:{bme_temp}C H:{bme_humi}%"
# SH1106 OLED display (most cheap modules use this controller)
ionode device add <dev> display sh1106 0 --i2c-addr 60 --template "T:{bme_temp}C H:{bme_humi}%"
ionode write <dev> display "!Hello World" # raw text (! prefix)
Reserved HAL Keywords
Users cannot name their devices any of these words:
gpio, adc, pwm, dac, uart, i2c, system, device, config
Notes
- Always discover nodes first (
ionode discover) if you don't know what's on the network. - All NATS responses are plain strings (bare floats, "ok", "0"/"1") or JSON.
- IOnode has no rule engine - all automation logic runs here in OpenClaw.
- The on-device web UI at
http://{device-ip}/is useful for manual control and adding/removing devices without reflashing. - NATS server must be accessible from both OpenClaw and the IOnode devices.