Under the Hood

How a $5 chip runs 20 tools, a rule engine, rule chains, and an AI agent. Cloud optional.

Dual-Loop Architecture

Two systems on one chip. The AI creates. The rules execute.

Two Loops, One Chip

The main loop() runs two systems. The Rule Loop evaluates automation rules every iteration - no network, no latency. The AI Loop activates only when a message arrives via Telegram, serial, or NATS.

Rule Engine

Persistent automation. Reads sensors, checks conditions (gt, lt, eq, neq, change, always), fires actions (actuator, LED, GPIO, NATS, Telegram, Serial). Edge-triggered conditions fire once per transition, then auto-reset. Chain rules together with delays via chain_create. Message interpolation: {value}, {device_name}, {name:msg}. Survives reboots.

AI Loop

LLM via OpenRouter, Ollama, llama.cpp, or any OpenAI-compatible endpoint. HTTPS for cloud, HTTP for local (saves ~40% RAM). 20 tools. Up to 5 iterations per request. Creates rules, registers devices, reads sensors, writes files. The AI is the compiler - it turns intent into persistent automation.

Device Registry

Named sensors and actuators persisted to flash. Register once, reference by name everywhere. chip_temp is pre-registered on first boot. Eight sensor types, three actuator types. Clock sensors auto-register on boot.

Time-Based Automation

Virtual clock sensors (clock_hour, clock_minute, clock_hhmm) auto-register on boot. NTP-synced with timezone and DST. Schedule anything - the same rule engine that watches temperature can watch the clock.

NATS Mesh Communication

Devices talk to each other over NATS. Register nats_value sensors that subscribe to external subjects and feed data into the rule engine. The remote_chat tool lets one device's AI ask another a natural language question. Publish sensor data, trigger cross-device rules, build a mesh of $5 autonomous agents.

Inputs & Outputs

What it can read, what it can control, and how rules react.

Sensor Types (10)

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 from any NATS subject. No GPIO pin.

serial_text

Text lines from UART1 serial port. Connects Arduinos, GPS, CO2 sensors. One device max.

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).

Actuator Types (3)

digital_out

Digital output pin. Set HIGH or LOW.

relay

Relay control. On/off switching with optional inverted logic.

pwm

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

Rule Action Types (6)

actuator

Set a registered actuator's value by name. Auto on/off.

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. Rule-triggered device-to-device messaging.

telegram

Send a Telegram alert. Configurable cooldown prevents flooding.

serial_send

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

Rule Chains

Chain up to 5 actions with delays in a single tool call. The AI creates the chain. The firmware runs it.

Sensor
Step 1
Delay
Step 2
...
serial - chain_create via Telegram
[TG] Message: send me a telegram when test > 100, wait 5s, send "hello test", set LED green, after 10s turn LED off
--- Thinking... ---
[Agent] 1 tool call(s) in iteration 1:
-> chain_create(sensor_name="test", condition="gt", threshold=100,
step1_action="telegram", step1_message="Test exceeded 100: {value}",
step2_action="telegram", step2_delay=5, step2_message="hello test",
step3_action="led_set", step3_r=0, step3_g=255, step3_b=0,
step4_action="led_set", step4_delay=10, step4_r=0, step4_g=0, step4_b=0)
= Chain created: rule_04 test>100 -> telegram -> 5s -> telegram -> LED(0,255,0) -> 10s -> LED(0,0,0)
Chain internals →

20 Tools

Each tool maps to a real hardware or system capability.

Hardware (4) led_set, gpio_write, gpio_read, temperature_read
Device Registry (5) device_register, device_list, device_remove, sensor_read, actuator_set
Rule Engine (5) rule_create, rule_list, rule_delete, rule_enable, chain_create
System (3) device_info, file_read, file_write
Messaging (3) nats_publish, serial_send, remote_chat
Full tool reference →

LED Feedback System

The onboard LED tells you what the agent is doing at a glance.

Connecting
Thinking
Tool Loop
Success
Error

Memory That Survives Reboots

Conversation Buffer

A 6-turn circular buffer stored on flash. Context persists across power cycles.

U:1
A:1
U:2
A:2
U:3
A:3

← oldest ··· newest → (write head)

Persistent Memory

Long-term preferences in /memory.txt on flash. The AI writes autonomously. Injected into every conversation. 512 chars. Survives reboots.

> "My favorite color is blue"
[writes to /memory.txt]
--- reboot ---
> "Set the LED to my favorite color"
LED set to blue (0, 0, 255)
/memory.txt
User's favorite color is blue. Prefers Celsius for temperature. Garden-node is the outdoor sensor.

Technical Specs

MCUESP32-C6, ESP32-S3, ESP32-C3 (4MB flash)
FrameworkArduino + PlatformIO
LanguageC++ (Arduino-flavored)
RAM Usage~280 KB with active conversation
Tools20 built-in (hardware, registry, rules, chains, filesystem, messaging)
Rule Engine7 conditions (incl. always/periodic, chained), 6 action types, edge-triggered, message interpolation ({value}, {device_name}, {name:msg}), /rules.json
Rule Chainingchain_create tool, up to 5 steps with non-blocking delays, max depth 8, 672 bytes RAM, persisted to /rules.json
Device Registry10 sensor types (incl. 3 virtual clock, NATS, serial), 3 actuator types, persisted to flash
Serial BridgeUART1, serial_text sensor + serial_send tool/action, 9600-115200 baud, text + JSON parsing
Clock Sensorsclock_hour, clock_minute, clock_hhmm - NTP-synced, POSIX timezone + DST
Telegram AlertsRule-triggered, no LLM, configurable cooldown, long polling, message interpolation
Persistent Memory/memory.txt on flash, 512 char limit, AI-managed
JSON ParsingStreaming token-by-token (ArduinoJson)
Buffer StrategyChunked HTTP reads, reused scratch buffers
Flash StorageLittleFS partition for config, history, rules, and device registry
API ProtocolHTTPS with TLS 1.2, OpenAI-compatible chat completions, or HTTP for local LLMs (Ollama, llama.cpp)
NATS SupportPublish, subscribe, request/reply over TCP
NATS Sensorsnats_value type - subscribe to external NATS subjects, feed readings into rule engine, no hardware pin needed
Local LLMOllama, llama.cpp, any OpenAI-compatible endpoint. HTTP mode saves ~40% RAM. No API key needed.
ConfigurationJSON config on flash - WiFi, API key, model, system prompt
Web ConfigBrowser UI at http://device.local/ - config, prompt, memory, status. REST API for scripting.
History Buffer6-turn circular buffer persisted to flash

Getting Started

Three steps from zero to a working AI agent.

1

Flash the Firmware

Clone the repo and upload with PlatformIO: pio run -t upload

2

Configure on Flash

Edit system_prompt.txt and config.json with your WiFi credentials and model. Add an API key for cloud LLMs, or point to a local Ollama endpoint - no key needed. Upload with pio run -t uploadfs

3

Talk to It

Send natural language via Telegram, serial console, or NATS - the agent handles the rest.

Web Config Portal

Change config, prompt, and memory from any browser - no USB, no reflashing. REST API for automation.

Try the Demo →

Ready to Put AI on the Wire?