ESP32-S3 — Zephyr RTOS BLE Gateway
The ESP32-S3 serves as a BLE-to-MQTT edge gateway. It aggregates BLE advertisements from nRF52840 sensors, runs on-device TFLite Micro anomaly inference, and publishes telemetry/alerts to the MQTT broker.
Role in the Architecture
Technical Specifications
| Feature | Detail |
|---|---|
| MCU | ESP32-S3 (Xtensa LX7 dual-core, 240MHz) |
| RAM | 512KB SRAM + 8MB PSRAM |
| RTOS | Zephyr RTOS |
| Language | C (Zephyr SDK) |
| Connectivity | WiFi 802.11 b/g/n + BLE 5.0 |
| Device ID | esp32s3_gw_01 |
| Telemetry interval | 10 seconds |
| Attestation interval | 5 minutes |
Why Zephyr RTOS?
| Feature | Zephyr | FreeRTOS | ESP-IDF Native |
|---|---|---|---|
| BLE Mesh support | Native (Bluetooth Mesh) | Limited | Limited |
| TFLite Micro | Built-in module | Manual port | Manual port |
| Hardware abstraction | Devicetree + Kconfig | Minimal | ESP-specific |
| OTA updates | MCUboot integration | Manual | ESP OTA |
| Upstream support | Linux Foundation | Amazon | Espressif |
Zephyr's built-in BLE Mesh stack and TFLite Micro module make it ideal for an edge AI gateway.
System Architecture
// main.c — Entry point
void main(void) {
const char *dev_id = "esp32s3_gw_01";
// 1. MQTT first — other subsystems publish through it
mqtt_client_init(dev_id); // DNS resolve → mTLS connect
ble_scanner_init(); // BLE passive scanning
edge_inference_init(); // TFLite Micro or statistical fallback
ota_manager_init(dev_id); // MCUboot confirm + OTA subscribe
attestation_init(dev_id); // SHA-256 firmware hash + ECDSA key load
// Periodic timers
k_timer_start(&telemetry_timer,
K_MSEC(TELEMETRY_INTERVAL_MS),
K_MSEC(TELEMETRY_INTERVAL_MS));
k_timer_start(&attestation_timer,
K_MSEC(ATTESTATION_INTERVAL_MS),
K_MSEC(ATTESTATION_INTERVAL_MS));
// Publish startup status
mqtt_client_publish(TOPIC_STATUS, status_json, strlen(status_json),
MQTT_QOS_1_AT_LEAST_ONCE);
while (1) {
mqtt_client_process(); // service MQTT keepalives
k_sleep(K_MSEC(100));
}
}
Telemetry Pipeline
Every 10 seconds:
Telemetry Handler
static void telemetry_timer_handler(struct k_timer *timer) {
// 1. Aggregate BLE sensor data from all nRF52 nodes
struct sensor_aggregate agg;
ble_scanner_aggregate(&agg);
// 2. Run TFLite Micro anomaly detection
float anomaly_score = edge_inference_predict(
agg.temperature,
agg.motion_count,
agg.ble_node_count
);
// 3. Build telemetry payload
snprintk(payload, sizeof(payload),
"{\"device_id\":\"%s\","
"\"temperature\":%.1f,"
"\"motion_count\":%d,"
"\"ble_nodes\":%d,"
"\"anomaly_score\":%.3f}",
DEVICE_ID, agg.temperature, agg.motion_count,
agg.ble_node_count, anomaly_score);
// 4. Publish telemetry (QoS 1 — At Least Once)
mqtt_publish(TOPIC_TELEMETRY, payload, MQTT_QOS_1_AT_LEAST_ONCE);
// 5. Alert if anomaly detected
if (anomaly_score > CONFIG_AURORA_ANOMALY_THRESHOLD) {
snprintk(alert_payload, sizeof(alert_payload),
"{\"device_id\":\"%s\","
"\"alert_type\":\"anomaly_detected\","
"\"severity\":\"high\","
"\"anomaly_score\":%.3f,"
"\"details\":\"Edge ML anomaly threshold exceeded\"}",
DEVICE_ID, anomaly_score);
// QoS 2 — Exactly Once (critical alert must not be duplicated)
mqtt_publish(TOPIC_ALERT, alert_payload, MQTT_QOS_2_EXACTLY_ONCE);
}
}
Edge AI Inference
The edge inference engine supports two modes selected via CONFIG_AURORA_TFLITE:
TFLite Micro mode (CONFIG_AURORA_TFLITE=y)
A TFLite Micro autoencoder model is loaded from the model flash partition at boot. The inference pipeline:
tflite_load_model()— reads the FlatBuffer model from flash (≤ 64 KiB)tflite_init_interpreter()— allocates a 20 KiB tensor arena, registers ops (FullyConnected, Relu, Logistic, Reshape, Mul, Sub)tflite_infer()— copies the feature vector into the input tensor, invokes the model, and returns the anomaly score:- Single-output model (classifier): output neuron value is the score directly
- Multi-output model (autoencoder): MSE between input and reconstruction, mapped to [0, 1] via
mse / (mse + 1)
Model: Autoencoder (reconstruction-based anomaly detection)
Input: float[16] — feature vector from BLE aggregate
Output: anomaly_score ∈ [0.0, 1.0]
Arena: 20 KiB tensor arena (aligned 16)
Partition: "model" flash partition (≤ 64 KiB)
Statistical fallback mode
When TFLite is disabled or the model partition is empty, a robust EWMA + modified Z-score detector provides comparable accuracy:
- EWMA baseline: exponentially weighted moving average of each feature (α = 0.1)
- Priming period: 20 samples before anomaly scoring activates
- Scoring: max modified Z-score across all features, mapped through sigmoid:
z / Z_THRESHOLD / (z / Z_THRESHOLD + 1) - Threshold:
Z_THRESHOLD = 3.0(≈ 99.7% confidence)
The statistical detector always runs alongside TFLite as an automatic fallback if model invocation fails.
Why edge inference? Sending all telemetry to the cloud adds latency and bandwidth cost. On-device inference detects anomalies in real-time, sending only significant alerts upstream.
MQTT Topics
| Topic | QoS | Direction | Content |
|---|---|---|---|
aurora/sensors/esp32s3_gw_01/telemetry | 1 | Publish | Sensor readings + anomaly score |
aurora/sensors/esp32s3_gw_01/alerts | 2 | Publish | Anomaly alerts |
aurora/sensors/esp32s3_gw_01/status | 1 | Publish | Device status, OTA progress, attestation |
aurora/command/esp32s3_gw_01/action | 1 | Subscribe | Commands: reboot, isolate, attest |
aurora/firmware/esp32s3_gw_01 | 2 | Subscribe | OTA firmware images (payload + RSA-3072 sig) |
MQTT Client Details
- Broker discovery: DNS resolution of
mosquitto.aurora.localwith static-IP fallback - Transport: TLS 1.3 (mTLS) with CA cert + client cert/key via
tls_credential_add() - Reconnect: exponential back-off (1 s → 60 s max, 5 attempts)
- Command dispatch:
reboot→sys_reboot(),attest→attestation_perform(), OTA →ota_manager_apply()
BLE Scanning
The ESP32-S3 scans for BLE advertisements from nRF52840 nodes:
void ble_scanner_init(void) {
struct bt_le_scan_param scan_param = {
.type = BT_LE_SCAN_TYPE_PASSIVE,
.options = BT_LE_SCAN_OPT_NONE,
.interval = BT_GAP_SCAN_FAST_INTERVAL,
.window = BT_GAP_SCAN_FAST_WINDOW,
};
bt_le_scan_start(&scan_param, scan_callback);
}
static void scan_callback(const bt_addr_le_t *addr, int8_t rssi,
uint8_t adv_type, struct net_buf_simple *buf) {
// Parse manufacturer-specific data from nRF52840 nodes
// Extract: temperature, motion_detected, tamper_status, battery_mv
// Update aggregate sensor data
}
Attestation
Every 5 minutes, the ESP32-S3 performs firmware integrity attestation using mbedTLS:
int attestation_perform(const char *device_id)
{
// 1. Re-hash firmware (slot0_partition) with mbedTLS SHA-256
// Reads in 4 KiB chunks via flash_area_read()
compute_firmware_sha256();
// 2. Build message: device_id ‖ firmware_hash_hex ‖ boot_count_LE
// Concatenation provides replay protection (boot_count)
// and device binding (device_id)
// 3. ECDSA P-256 signature via mbedtls_ecdsa_write_signature()
// Signing key loaded from NVS ("attest/ecdsa_key")
// Random nonce from mbedtls_ctr_drbg seeded by Zephyr entropy
// 4. Build JSON and publish QoS 2
// {"device_id":"...","firmware_hash":"...",
// "boot_count":N,"signature":"..."}
mqtt_client_publish(TOPIC_STATUS, json, len,
MQTT_QOS_2_EXACTLY_ONCE);
}
Cryptographic details
| Parameter | Value |
|---|---|
| Hash | SHA-256 (mbedTLS), full application flash partition |
| Signature | ECDSA P-256 (secp256r1) |
| Key storage | NVS key attest/ecdsa_key (32-byte private scalar) |
| RNG | CTR-DRBG seeded from Zephyr hardware entropy |
| Partition | slot0_partition read via flash_area API |
OTA Firmware Updates
The OTA manager implements a secure MCUboot-based update flow:
Image format
OTA images are appended with an RSA-3072 signature (384 bytes):
[ firmware payload (N bytes) ][ RSA-3072 PKCS#1 v1.5 signature (384 bytes) ]
- Verification:
mbedtls_pk_verify()checks SHA-256(payload) against the signature using a provisioned RSA-3072 public key (DER-encoded, linked at build time) - Test upgrade:
BOOT_UPGRADE_TESTensures the device reverts to the previous image if the new firmware fails to callboot_write_img_confirmed()within the first boot cycle - Status reporting: OTA progress is published to
TOPIC_STATUSat each stage (verifying → flashing → rebooting → confirmed / rejected)
Build System
# Configure for ESP32-S3
west build -b esp32s3_devkitm firmware/esp32s3
# Flash
west flash
# Monitor serial output
west espressif monitor
Key Kconfig Options
# Networking
CONFIG_WIFI=y
CONFIG_DNS_RESOLVER=y
CONFIG_MQTT_LIB=y
CONFIG_MQTT_LIB_TLS=y
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_TLS_VERSION_1_3=y
# BLE
CONFIG_BT=y
CONFIG_BT_OBSERVER=y
# Crypto & secure boot
CONFIG_MBEDTLS_ECDSA_C=y
CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
CONFIG_MBEDTLS_SHA256_C=y
# OTA / MCUboot
CONFIG_BOOTLOADER_MCUBOOT=y
CONFIG_MCUMGR=y
CONFIG_IMG_MANAGER=y
# Edge inference
CONFIG_AURORA_TFLITE=y
CONFIG_AURORA_ANOMALY_THRESHOLD=75
# Storage
CONFIG_FLASH=y
CONFIG_NVS=y
CONFIG_SETTINGS=y