Everything IOnode can do.

From a single sensor to a fleet of 100 nodes - here's how it all works.

How It Works

IOnode is a firmware. Flash it onto an ESP32, and the chip becomes a node on your NATS network. Every pin, sensor, and actuator gets a unique address. Send a message, get a response. That's it.

1. Flash the firmware

From your browser or PlatformIO. Takes 30 seconds. Works on any ESP32 - C6, S3, C3, or classic.

2. Connect to WiFi & NATS

On first boot, a setup portal appears. Enter your WiFi and NATS server address. One form, done.

3. Your hardware is on the network

Every GPIO pin, ADC channel, and registered sensor gets a NATS address. Read a temperature: ionode read ionode-01 room_temp

the idea
Every IOnode has a name. Every sensor has a name.
Put them together - that's the address.

$ ionode read ionode-01 room_temp    → 24.5
$ ionode write ionode-01 fan 1       → ok
$ ionode discover                    → all nodes respond

No SDK. No cloud. No API keys. Just NATS messages.

Under the Hood: NATS Subjects

The CLI, web UI, and dashboard are convenience layers. Underneath, every operation is a simple NATS subject. Script IOnode in any language with a NATS client.

GPIO & Peripherals
{name}.hal.gpio.{pin}.getRead pin → 0 or 1
{name}.hal.gpio.{pin}.setWrite pin → ok
{name}.hal.adc.{pin}.read12-bit ADC → 04095
{name}.hal.pwm.{pin}.set8-bit PWM → ok
{name}.hal.uart.read / writeSerial bridge (requires serial_text device)
I2C
{name}.hal.i2c.scanScan bus → array of addresses
{name}.hal.i2c.{addr}.readRead register bytes
{name}.hal.i2c.{addr}.writeWrite register bytes
{name}.hal.i2c.recoverBus recovery (9× SCL toggle)
Registered Devices
{name}.hal.{dev}Read sensor or actuator state
{name}.hal.{dev}.setSet actuator value → ok
{name}.hal.{dev}.infoJSON: name, kind, value, pin, unit
Discovery & System
_ion.discoverAll nodes respond with capabilities
{name}.capabilitiesSingle node capabilities
{name}.hal.system.*Chip temp, heap, uptime, RSSI, reset reason
{name}.hal.device.listJSON array of all registered devices

Full protocol reference on GitHub.

Device Kinds

21 device types across 6 categories. Register via CLI, NATS, web UI, or devices.json.

GPIO

digital_in · digital_out · analog_in · pwm · relay (with inversion)

Direct pin access. Read switches, drive motors, toggle relays.

Analog Sensors

ntc_10k · ldr

Thermistor (Steinhart-Hart, EMA-smoothed) and light level (0–100%). One resistor, done.

I2C Sensors

i2c_bme280 · i2c_bh1750 · i2c_sht31 · i2c_ads1115 · i2c_generic

Raw Wire.h drivers. Multi-channel, per-address caching. Any I2C sensor via i2c_generic.

OLED Display

ssd1306 · sh1106

128×64 or 128×32 text display. Template engine with live sensor tokens, auto-refreshes every 5s.

Built-in

internal_temp · rgb_led · clock_hour · clock_minute · clock_hhmm

No wiring needed. Chip temperature, RGB LED, and NTP clock - ready at boot.

Virtual

nats_value · serial_text

Subscribe to NATS subjects or read UART lines. Bridge external data into the device registry.

Full sensor & display reference →
devices.json
[
  {"n":"bme_temp",  "k":"i2c_bme280", "p":0,  "u":"C",  "i":false, "ia":118},
  {"n":"bme_humi",  "k":"i2c_bme280", "p":1,  "u":"%",  "i":false, "ia":118},
  {"n":"light",     "k":"i2c_bh1750", "p":0,  "u":"lux","i":false, "ia":35},
  {"n":"display",   "k":"ssd1306",    "p":0,  "u":"",   "i":false, "ia":60,
   "dt":"T:{bme_temp}C H:{bme_humi}%"},
  {"n":"heater",    "k":"relay",      "p":8,  "u":"",   "i":true},
  {"n":"fan",       "k":"pwm",        "p":9,  "u":"",   "i":false}
]

The Web UI

Each IOnode serves a control interface on port 80. Configure, monitor, and control from any browser.

Config

WiFi, NATS, device name, timezone. Live devices.json editor.

Devices

All registered devices with kind-appropriate widgets. Add/remove devices.

Pins

Direct GPIO/ADC/PWM access. No registration needed. Quick wiring checks.

Status

Version, uptime, heap, WiFi signal, NATS connection state.

Web UI showcase →

Fleet Management

Everything you need to manage nodes at scale - from one to hundreds.

CLI Tool

28 commands with true color output. Discover, read, write, tag, configure, monitor - all from the terminal.

Web Dashboard

Single HTML file. Connects via NATS WebSocket. Live node cards, actuator controls, event log, tag filtering.

Events & Health

Threshold events with edge detection and cooldown. Periodic heartbeats with heap, RSSI, uptime. Dead node detection.

Fleet Management →

Built to be Hacked

Adding a sensor type is a two-step process. No NATS code. No registration boilerplate.

1. Add the enum

enum DeviceKind {
    // ... existing kinds ...
    DEV_SENSOR_MY_SENSOR,      // ← add here
};

2. Add the read case + string mapping

case DEV_SENSOR_MY_SENSOR: {
    result = readMyHardware(dev->pin);
    record_history = true;
    break;
}

// deviceKindName:
case DEV_SENSOR_MY_SENSOR: return "my_sensor";

// kindFromString:
if (strcmp(s, "my_sensor") == 0) return DEV_SENSOR_MY_SENSOR;
result
Your sensor is now:
 Persisted in devices.json as {"k":"my_sensor"}
 Readable via nats req ionode-01.hal.my_sensor ""
 Listed in device.list and discovery responses
 Background-polled for history every 5 minutes
 Controllable via the web UI

No NATS code. The HAL router handles it.

Chip Compatibility

All targets use a 2MB partition layout. IOnode is lean, ~202KB free heap.

ESP32-C6Default target · USB-CDC serial · RGB LED · RISC-V
ESP32-S3More RAM · RGB LED · Dual-core Xtensa
ESP32-C3Smallest, cheapest · RISC-V · No on-die temp on some variants
ESP32Classic · Dual-core Xtensa · No on-die temp sensor
Partitionapp0 (1.69 MB) + spiffs (256 KB) + nvs (20 KB)
Free Heap~202 KB
FrameworkArduino + PlatformIO (pioarduino)

See what it can sense.