Skip to main content

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

FeatureDetail
MCUESP32-S3 (Xtensa LX7 dual-core, 240MHz)
RAM512KB SRAM + 8MB PSRAM
RTOSZephyr RTOS
LanguageC (Zephyr SDK)
ConnectivityWiFi 802.11 b/g/n + BLE 5.0
Device IDesp32s3_gw_01
Telemetry interval10 seconds
Attestation interval5 minutes

Why Zephyr RTOS?

FeatureZephyrFreeRTOSESP-IDF Native
BLE Mesh supportNative (Bluetooth Mesh)LimitedLimited
TFLite MicroBuilt-in moduleManual portManual port
Hardware abstractionDevicetree + KconfigMinimalESP-specific
OTA updatesMCUboot integrationManualESP OTA
Upstream supportLinux FoundationAmazonEspressif

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:

  1. tflite_load_model() — reads the FlatBuffer model from flash (≤ 64 KiB)
  2. tflite_init_interpreter() — allocates a 20 KiB tensor arena, registers ops (FullyConnected, Relu, Logistic, Reshape, Mul, Sub)
  3. 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

TopicQoSDirectionContent
aurora/sensors/esp32s3_gw_01/telemetry1PublishSensor readings + anomaly score
aurora/sensors/esp32s3_gw_01/alerts2PublishAnomaly alerts
aurora/sensors/esp32s3_gw_01/status1PublishDevice status, OTA progress, attestation
aurora/command/esp32s3_gw_01/action1SubscribeCommands: reboot, isolate, attest
aurora/firmware/esp32s3_gw_012SubscribeOTA firmware images (payload + RSA-3072 sig)

MQTT Client Details

  • Broker discovery: DNS resolution of mosquitto.aurora.local with 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: rebootsys_reboot(), attestattestation_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

ParameterValue
HashSHA-256 (mbedTLS), full application flash partition
SignatureECDSA P-256 (secp256r1)
Key storageNVS key attest/ecdsa_key (32-byte private scalar)
RNGCTR-DRBG seeded from Zephyr hardware entropy
Partitionslot0_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_TEST ensures the device reverts to the previous image if the new firmware fails to call boot_write_img_confirmed() within the first boot cycle
  • Status reporting: OTA progress is published to TOPIC_STATUS at 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