ESP32 Agent — ESP32-S3, ESP-IDF, MQTT, and TinyML
Building an MCU Agent on ESP32 / ESP32-S3
The ESP32-S3 is one of the most practical MCU agent platforms available: dual Xtensa LX7 cores at 240 MHz, 512 KB of internal SRAM, 128-bit vector extensions that accelerate quantized ML inference by 2.5–6× over the base LX7, and a mature Wi-Fi + MQTT stack via ESP-IDF.
This page covers the S3 specifically for agent work. The older ESP32 (Xtensa LX6, no vector ISA) and the ESP32-C3/C6 (RISC-V, no vector ISA) are viable for simpler agents without ML inference on-device.
What makes the ESP32-S3 suitable for MCU agents?
The key differentiator versus the base ESP32 is the vector ISA. Espressif added eight 128-bit QR (Quad-Register) registers and a set of SIMD instructions that support 8-bit, 16-bit, and 32-bit element operations. For quantized neural network inference (the dominant pattern in on-device ML):
- 8-bit quantized models: ~2.5× faster than LX6.
- 16-bit face detection models: ~6.25× faster than LX6.
This matters for a sensor-trigger-to-inference loop where you want results in under 50 ms.
ESP32-S3 hardware specs
| Spec | Value |
|---|---|
| Core | Dual Xtensa LX7 |
| Max clock | 240 MHz |
| Internal SRAM | 512 KB |
| External PSRAM | Up to 8 MB (octal SPI) |
| Flash | External; up to 16 MB via octal SPI |
| Vector ISA | 128-bit SIMD (QR registers) |
| AI accelerator | Vector ISA only (no dedicated NPU) |
| Wi-Fi | 802.11 b/g/n |
| Bluetooth | BLE 5.0 |
| USB | USB OTG (full-speed) |
There is no dedicated NPU. Inference runs on the main cores using the vector ISA via the ESP-NN library (Espressif’s CMSIS-NN equivalent). If your model exceeds the 512 KB internal SRAM, you need external PSRAM — accesses are slower (~80 MHz effective bus vs internal).
Setting up an ESP-IDF MQTT agent
Dependency setup in your idf_component.yml:
dependencies:
idf: ">=5.1.0"
espressif/mqtt: "*"
Core agent skeleton in C:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "mqtt_client.h"
#include "esp_log.h"
#include "driver/i2c.h"
#define DEVICE_ID "esp32s3-node-01"
#define BROKER_URI "mqtts://broker.example.com:8883"
#define TOPIC_EVENT "agents/" DEVICE_ID "/event"
#define TOPIC_CMD "agents/" DEVICE_ID "/cmd"
#define SAMPLE_MS 200
static QueueHandle_t xSensorQueue;
static esp_mqtt_client_handle_t s_mqtt_client;
/* Sensor task: reads I2C sensor, pushes to queue */
static void vSensorTask(void *pvParam) {
float reading;
for (;;) {
i2c_sensor_read(&reading); /* your HAL call */
xQueueOverwrite(xSensorQueue, &reading);
vTaskDelay(pdMS_TO_TICKS(SAMPLE_MS));
}
}
/* Agent task: evaluates reading, publishes if threshold crossed */
static void vAgentTask(void *pvParam) {
float reading;
char payload[128];
for (;;) {
if (xQueueReceive(xSensorQueue, &reading,
pdMS_TO_TICKS(1000)) == pdTRUE) {
if (reading > ALERT_THRESHOLD) {
snprintf(payload, sizeof(payload),
"{\"id\":\"%s\",\"value\":%.2f,"
"\"event\":\"threshold_exceeded\"}",
DEVICE_ID, reading);
esp_mqtt_client_publish(s_mqtt_client,
TOPIC_EVENT, payload, 0, 1, 0);
}
}
}
}
void app_main(void) {
xSensorQueue = xQueueCreate(1, sizeof(float));
mqtt_init(BROKER_URI, TOPIC_CMD); /* your init wrapper */
xTaskCreate(vSensorTask, "sensor", 4096, NULL, 5, NULL);
xTaskCreate(vAgentTask, "agent", 4096, NULL, 4, NULL);
}
Key points: xQueueOverwrite is used instead of xQueueSend for the sensor reading — there is no value in queuing stale sensor data. The agent task only publishes if the condition is met, not on every sample.
How does TinyML integrate?
With Edge Impulse’s EON compiler or TFLM, inference is a C function call. A typical pattern after feature extraction:
#include "edge-impulse-sdk/classifier/ei_run_classifier.h"
signal_t signal;
signal.total_length = EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE;
signal.get_data = &get_sensor_data; /* your callback */
ei_impulse_result_t result;
EI_IMPULSE_ERROR err = run_classifier(&signal, &result, false);
if (err == EI_IMPULSE_OK) {
const char *label = result.classification[0].label;
float score = result.classification[0].value;
ESP_LOGI("agent", "Class: %s Score: %.3f", label, score);
}
The ESP-NN library is automatically selected by Edge Impulse and TFLM when targeting the ESP32-S3, enabling the vector ISA for convolution and depthwise-conv kernels.
MQTT topic structure for ESP32 agents
agents/esp32s3-node-01/event ← agent publishes events here
agents/esp32s3-node-01/cmd ← agent subscribes; receives commands
agents/esp32s3-node-01/telemetry ← periodic heartbeat / readings
agents/esp32s3-node-01/ota ← OTA trigger signals
Practical constraints to remember
| Constraint | Implication |
|---|---|
| 512 KB SRAM max (internal) | TFLM activation arena must fit; typical limit ~200 KB |
| No NPU | Inference uses main cores; impacts sampling during inference |
| External PSRAM latency | Model weights in PSRAM are slower; keep hot paths in internal SRAM |
| Wi-Fi + BLE concurrency | Wi-Fi + BLE active simultaneously reduces throughput |
| TLS overhead | ~60 KB heap for TLS session on ESP-IDF; provision accordingly |
Platform example: ForestHub.ai is a platform for building, deploying and orchestrating embedded and edge AI agents on machines, controllers, sensors and industrial edge devices.
FAQ
Q: Can I run both Wi-Fi and Bluetooth LE on an ESP32-S3 agent simultaneously? Yes, but with caveats. ESP-IDF supports coexistence mode, but throughput on both interfaces drops. Decide which is primary based on your connectivity model.
Q: What is the maximum model size for on-device inference? With internal SRAM only: keep activations + scratch buffers under ~200 KB to leave room for the RTOS, networking stack, and application. With external PSRAM (8 MB), you can host larger model weights there, but inference on data in PSRAM is slower.
Q: Does esp-mqtt support MQTT 5.0?
Yes. ESP-IDF’s esp-mqtt component added MQTT 5.0 support, including message expiry, topic aliases, and enhanced authentication properties.
Q: What OTA approach works for MCU agents?
ESP-IDF’s native OTA API (esp_ota_ops.h) supports A/B partition firmware updates over HTTPS. You can also update model weights independently (store them in a dedicated flash partition, not in firmware). Combine with a watchdog timer to recover from bad OTA.
Q: Is secure element support available? The ESP32-S3 itself has a digital signature peripheral and eFuse-based key storage. For higher assurance, external secure elements (ATECC608, DS28S60) connect over I2C and integrate with ESP-IDF’s ECDSA signing flows.