Inside the Wire

The full architecture. Two loops. Twenty tools. Rules that run locally, forever.

Two Loops

Everything runs inside a single loop() on a single core.

Rule Loop

Runs every iteration. Reads sensors, checks conditions, fires actions. No network calls, no LLM, no latency. Edge-triggered: fires once per condition transition, then auto-resets. Persisted to flash - survives reboots.

AI Loop

Activates only when a message arrives - via Telegram, serial, or NATS. Calls the LLM through OpenRouter, Ollama, llama.cpp, or any OpenAI-compatible endpoint. HTTP mode for local LLMs saves ~40% RAM. Up to 5 tool-call iterations. Creates rules, registers devices, reads sensors, configures the system.

loop():
  rulesEvaluate()     // always runs (incl. clock sensors)
  checkTelegram()    // only if message
  checkSerial()      // only if message
  checkNATS()        // only if message

The LLM is the compiler. The ESP32 is the runtime.

From Intent to Automation

What happens when you say "When chip temp goes above 45, set LED orange."

User sends message

"When chip temp goes above 45, set LED orange"

AI Loop activates

LLM processes the intent, decides to call rule_create

Rule created

Calls rule_create("hot_alert", "chip_temp", "gt", 45, "led_set", "255,140,0")

Rule persisted

Written to /rules.json on flash (~80 bytes). Survives power loss.

Rule Loop picks it up

Every iteration: reads chip_temp, checks if > 45, fires led_set on transition.

Edge-triggered behavior

Fires once when condition becomes true. Auto-resets when condition clears. No repeated triggers.

Survives reboots

On next power-on: loads from flash, resumes evaluation immediately. No re-setup needed.

serial - rule_create via Telegram
[TG] Message: please set the led to orange, when chip temp is > 27 degrees, and fall back to cyan
--- Thinking... ---
[Agent] 2 tool call(s) in iteration 1:
-> rule_create({"rule_name": "Chip Temp High", "sensor_name": "chip_temp", "condition": "gt", "threshold": 27, "on_action": "led_set", "on_r": 255, "on_g": 165, "on_b": 0, "off_action": "led_set", "off_r": 0, "off_g": 255, "off_b": 255})
= Rule created: rule_02 'Chip Temp High' - chip_temp > 27 (every 5s) with auto-off
-> rule_list({})
= rule_01 'Room Temp Alert' ON room_temp 30 val=25 idle last=426s; rule_02 'Chip Temp High' ON chip_temp 27 val=0 idle last=0s
A rule has been created to set the LED to orange when the chip temperature exceeds 27°C, and it will fall back to cyan when it drops below that threshold.

Rule Engine

Seven conditions. Six action types. Persistent, edge-triggered automation.

Conditions

gt

Greater than threshold. Fires when sensor value exceeds the set point.

lt

Less than threshold. Fires when sensor value drops below the set point.

eq

Equal to threshold. Fires on exact match (integer comparison).

neq

Not equal. Fires when sensor value differs from the set point.

change

Any change. Fires when the sensor value changes from its previous reading.

always

Periodic. Fires every interval_seconds (default: evaluation interval). Use for heartbeats, scheduled reports, and watchdogs. Tool output shows last=Xs for verification.

chained

Chain-triggered. Skipped during normal evaluation. Only fires when another rule's chain targets it. Created automatically by chain_create.

Action Types

actuator

Set a registered actuator's value - digital, relay, or PWM output by name.

led_set

Set the onboard RGB LED to any color. Takes R,G,B values.

gpio_write

Write HIGH or LOW to a GPIO pin directly.

nats_publish

Publish a message to a NATS subject. Enables rule-triggered device-to-device messaging.

telegram

Send a Telegram alert. Configurable cooldown (default 60s) prevents message flooding.

serial_send

Send text over serial_text UART. Supports {value} and {device_name} interpolation.

Edge-triggered: fires once per transition
Auto-off: resets when condition clears
Sensor caching: avoids redundant reads
Configurable intervals (default 5s)
Persistence: /rules.json on flash
Telegram cooldown: 60s default
always condition: fires every interval_seconds
Rule chains: up to 5 steps with delays
Chain depth limit: 8 (prevents loops)

Chain Internals

chain_create generates multiple linked rules under the hood. The first rule evaluates normally against the sensor condition. Each subsequent step becomes a separate rule with condition: chained - these skip normal evaluation and only fire when the previous step queues them.

Execution Flow

sensor triggers step1 fires queues step2 with delay delay expires step2 fires queues step3...

Non-blocking Delays

Delays don't block the loop. A pending chain queue (8 entries max) tracks scheduled steps with their target timestamps. Each rulesEvaluate() iteration checks the queue and fires any steps whose delay has elapsed.

Safety Limits

Max chain depth: 8 - prevents infinite loops from circular chains
Pending queue: 8 entries - concurrent chains share the queue
5 steps per chain_create - tool limit, but chains can manually link more via rule_create
serial - /rules
> /rules
--- rules ---
rule_01 'Room Temp Alert' [ON] room_temp gt 30 val=25 idle
on: telegram "Room temperature has reached {value}°C."
rule_02 'Chip Temp High' [ON] chip_temp gt 27 val=28 FIRED
on: led_set(255,165,0)
off: led_set(0,255,255)
rule_03 'Chip Temp Change Alert' [ON] chip_temp change 0 val=28 FIRED
on: telegram "Chip temperature changed to: {value}°C"
---

Message Interpolation

{value}

The triggering sensor's reading at fire time.

{device_name}

Any named sensor's live reading (e.g., {chip_temp}).

{name:msg}

Extract the msg field from a JSON NATS payload by device name.

Works in Telegram alerts and NATS publish actions
127 character max message length
Zero heap allocation - stack-based formatting
Example: "Temp is {chip_temp}°C""Temp is 36.2°C"

Device Registry

Named sensors and actuators. Register once, reference by name everywhere.

Sensor Types

digital_in

Digital input pin. Returns HIGH (1) or LOW (0).

analog_in

Analog input via ADC. Returns raw value (0-4095).

ntc_10k

10K NTC thermistor. Automatic temperature conversion.

ldr

Light-dependent resistor. Returns light level reading.

internal_temp

Chip's internal temperature sensor. No external hardware needed.

nats_value

NATS-subscribed sensor. Receives values published to a configured NATS subject. No GPIO pin. Bridges external systems into the rule engine.

serial_text

Text lines from UART1 serial port. Connects Arduinos, GPS modules, CO2 sensors. Stores last line as value + message. One device max.

Virtual Sensors (Clock)

clock_hour

Current hour (0-23). NTP-synced, POSIX timezone + DST.

clock_minute

Current minute (0-59). Updates every rule evaluation cycle.

clock_hhmm

Combined time as hour*100+minute (e.g., 1830 = 6:30 PM). Use with eq for scheduled rules.

Actuator Types

digital_out

Digital output pin. Set HIGH or LOW.

relay

Relay control. On/off switching for higher-power loads.

pwm

PWM output. Variable duty cycle for dimmers, motors, servos.

serial - /devices
> /devices
--- devices ---
chip_temp [internal_temp] pin=255 = 27.7 C
clock_hour [clock_hour] pin=255 = 4.0 h
clock_minute [clock_minute] pin=255 = 11.0 m
clock_hhmm [clock_hhmm] pin=255 = 411.0
room_temp [nats_value] nats=home.room.temp = 0.0 C
---

chip_temp and three clock sensors (clock_hour, clock_minute, clock_hhmm) are pre-registered on first boot. Persisted to /devices.json. NATS sensors (nats_value) register on demand - subscribe to any subject, feed external data into rules.

All 20 Tools

The complete tool reference, grouped by category.

Hardware
led_set
LED

Control the onboard RGB LED. Set any color with R, G, B values (0-255). Visual feedback for every action.

gpio_write
GPIO

Drive output pins HIGH or LOW. Control relays, motors, actuators, or any digital output connected to a GPIO pin.

gpio_read
GPIO

Read digital and analog values from any GPIO pin. Supports ADC readings for sensors, voltage dividers, and switches.

temperature_read
Sensor

Read the ESP32 internal temperature sensor directly. Returns degrees Celsius. No external hardware required.

Device Registry
device_register
Registry

Register a named sensor or actuator. Specify name, type (digital_in, analog_in, ntc_10k, etc.), and GPIO pin. Persisted to flash.

device_list
Registry

List all registered devices with their names, types, pins, and current values. Shows both sensors and actuators.

device_remove
Registry

Remove a registered device by name. Cleans up the registry entry and updates the persisted file.

sensor_read
Registry

Read a sensor by its registered name. Automatically handles type-specific conversions (NTC to Celsius, raw ADC, etc.).

actuator_set
Registry

Set a named actuator's output value. Supports digital on/off, relay switching, and PWM duty cycle control.

Rule Engine
rule_create
Rules

Create a persistent automation rule. Specify name, sensor, condition, threshold, and action. Immediately active and persisted to flash.

rule_list
Rules

List all rules with conditions, actions, enabled state, last trigger time, and current evaluation status.

rule_delete
Rules

Delete a rule by name. Removes from active evaluation and from the persisted rules file. Takes effect immediately.

rule_enable
Rules

Enable or disable a rule without deleting it. Disabled rules are preserved but skip evaluation.

chain_create
Rules

Create a multi-step automation chain in one call. Up to 5 steps with delays. Actions: telegram, led_set, gpio_write, nats_publish, actuator, serial_send.

System
device_info
System

Query system information: free heap, uptime, WiFi RSSI, chip temperature, flash usage, and firmware version.

file_read
Flash FS

Read any file from the LittleFS flash filesystem. Access configs, rules, device registry, logs, or custom data files.

file_write
Flash FS

Write data to the flash filesystem. Create or update config files, persist sensor readings, store notes or custom data.

Messaging
nats_publish
Messaging

Publish messages to NATS subjects. Enables device-to-device communication, event broadcasting, and IoT mesh coordination.

serial_send
UART

Send text over the serial_text UART connection. Appends newline automatically. Supports {value} and {device_name} interpolation.

remote_chat
Multi-Device

Send a natural language message to another WireClaw device by name. NATS-based request/reply with 30-second timeout.

Try With Just a Dev Board

No external hardware needed. chip_temp sensor + onboard RGB LED = full demo.

What's the chip temperature?
Set the LED to blue
When chip temp goes above 28, set LED orange
List my rules
Every day at 8 AM, send me the temp on Telegram
My favorite color is purple. Remember that.
Ask garden-node what its soil moisture is
Send me a Telegram every 2 minutes saying "system OK"
Register a NATS sensor called room_temp on subject home.room.temp
When chip temp exceeds 30, set LED red, wait 10 seconds, send me a Telegram, then set LED off
Connect an Arduino on serial at 9600 baud, alert me when it reports above 50

Boot Sequence

What happens when you power on.

1 Init hardware
2 Load config
3 Connect WiFi
4 NTP sync
5 Connect NATS
6 Load rules
7 Load devices
8 Load history
9 Load memory
10 Start dual loop

Ready to Put AI on the Wire?

Flash Now
← Back to Home