ionode

star 8

Control IOnode hardware nodes on your network. Use when the user wants to read sensors, control GPIO pins, relays, PWM outputs, or ADC inputs on ESP32 microcontrollers. Also use when the user references "ionode", "ion node", "ESP32 sensor nodes", or wants to interact with physical hardware over NATS.

M64GitHub By M64GitHub schedule Updated 2/25/2026

name: ionode description: > Control IOnode hardware nodes on your network. Use when the user wants to read sensors, control GPIO pins, relays, PWM outputs, or ADC inputs on ESP32 microcontrollers. Also use when the user references "ionode", "ion node", "ESP32 sensor nodes", or wants to interact with physical hardware over NATS. tools:

  • Bash
  • Read metadata: openclaw: requires: binaries: - nats env:
    • IONODE_NATS_URL

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.

Prerequisites

Required

  • nats CLI - 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 ionode CLI 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/ionode

I can use raw nats req commands 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 °C
  • clock_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.
Install via CLI
npx skills add https://github.com/M64GitHub/IOnode --skill ionode
Repository Details
star Stars 8
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator