IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series ESP32-S3 Dual-Core – Bài 10: Firmware IoT Production – OTA & Health Monitor

Firmware Demo vs Firmware Production

Khi demo trên bàn làm việc, firmware chạy 30 phút là đủ. Khi deploy thiết bị thực tế, firmware cần chạy hàng tuần, hàng tháng không cần can thiệp. Sự khác biệt không nằm ở tính năng mà ở độ bền và khả năng tự phục hồi.

Demo FirmwareProduction Firmware
Crash → restart thủ côngCrash → tự restart, tự phục hồi
Update = kết nối máy tính + uploadUpdate = OTA qua WiFi
Serial.println() debugStructured logging có level
Không biết thiết bị ở đâuHealth monitoring: mem, uptime, WiFi RSSI
Hỏng phải ra hiện trườngRemote diagnostic + self-healing

OTA Update: Cập Nhật Firmware Từ Xa

ESP32-S3 có partition table hỗ trợ 2 firmware slot (app0 + app1). OTA ghi firmware mới vào slot đang không dùng, sau đó switch sang slot mới — nếu firmware mới lỗi, có thể rollback về slot cũ.

OTA Đơn Giản Với ArduinoOTA

#include <WiFi.h>
#include <ArduinoOTA.h>

void setupOTA() {
    ArduinoOTA.setHostname("esp32s3-iotlabs");
    ArduinoOTA.setPassword("iotlabs2024");  // Bảo vệ OTA

    ArduinoOTA.onStart([]() {
        Serial.println("[OTA] Bắt đầu update...");
        // Tạm dừng các task quan trọng nếu cần
    });

    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
        Serial.printf("[OTA] Progress: %u%%\n", (progress / (total / 100)));
    });

    ArduinoOTA.onEnd([]() {
        Serial.println("\n[OTA] Update xong — Restarting...");
    });

    ArduinoOTA.onError([](ota_error_t error) {
        Serial.printf("[OTA] Error[%u]: ", error);
        if (error == OTA_AUTH_ERROR)         Serial.println("Auth Failed");
        else if (error == OTA_BEGIN_ERROR)   Serial.println("Begin Failed");
        else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
        else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
        else if (error == OTA_END_ERROR)     Serial.println("End Failed");
    });

    ArduinoOTA.begin();
    Serial.println("[OTA] Ready — hostname: esp32s3-iotlabs");
}

// Gọi trong task riêng hoặc loop
void otaTask(void* p) {
    while (true) {
        ArduinoOTA.handle();
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

OTA Qua HTTP Server (Tự Host)

#include <HTTPUpdate.h>

void checkAndUpdateOTA(const char* updateUrl) {
    WiFiClient client;
    Serial.printf("[OTA] Checking: %s\n", updateUrl);

    t_httpUpdate_return ret = httpUpdate.update(client, updateUrl);

    switch (ret) {
        case HTTP_UPDATE_FAILED:
            Serial.printf("[OTA] FAILED: %s\n",
                httpUpdate.getLastErrorString().c_str());
            break;
        case HTTP_UPDATE_NO_UPDATES:
            Serial.println("[OTA] Already up to date");
            break;
        case HTTP_UPDATE_OK:
            Serial.println("[OTA] OK — will restart");
            // ESP tự restart sau khi update
            break;
    }
}

Hardware Watchdog: Tự Restart Khi Firmware Treo

ESP32-S3 có hardware watchdog timer. Nếu không được “feed” trong khoảng thời gian quy định, chip tự reset.

#include "esp_task_wdt.h"

#define WDT_TIMEOUT_SECONDS 30  // Reset nếu không feed trong 30s

void setupWatchdog() {
    // Khởi tạo watchdog với timeout 30 giây
    esp_task_wdt_config_t wdt_config = {
        .timeout_ms = WDT_TIMEOUT_SECONDS * 1000,
        .idle_core_mask = 0,  // Không monitor idle tasks
        .trigger_panic = true // Panic + reset khi timeout
    };
    esp_task_wdt_reconfigure(&wdt_config);
}

// Task duy nhất chịu trách nhiệm feed watchdog
void watchdogTask(void* p) {
    esp_task_wdt_add(NULL);  // Đăng ký task này với WDT

    while (true) {
        // Feed watchdog mỗi 10 giây
        esp_task_wdt_reset();

        // Kiểm tra các task critical còn sống không
        if (!isNetworkTaskAlive()) {
            Serial.println("[WDT] Network task chết — restart!");
            esp_restart();
        }

        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

Device State Machine

Firmware production cần quản lý state rõ ràng — không phải một đống if/else trong loop():

typedef enum {
    STATE_BOOT,           // Khởi tạo hardware
    STATE_WIFI_CONNECT,   // Kết nối WiFi
    STATE_PROVISIONING,   // Cấu hình lần đầu (nếu chưa có)
    STATE_RUNNING,        // Hoạt động bình thường
    STATE_OTA_UPDATE,     // Đang update firmware
    STATE_ERROR,          // Lỗi không phục hồi được
    STATE_SLEEP,          // Deep sleep tiết kiệm điện
} DeviceState;

DeviceState currentState = STATE_BOOT;
uint32_t stateEnteredAt = 0;

void changeState(DeviceState newState) {
    Serial.printf("[State] %s → %s\n",
        stateToString(currentState),
        stateToString(newState));
    currentState    = newState;
    stateEnteredAt  = millis();
}

void stateMachineTask(void* p) {
    while (true) {
        switch (currentState) {
            case STATE_BOOT:
                initHardware();
                changeState(STATE_WIFI_CONNECT);
                break;

            case STATE_WIFI_CONNECT:
                if (WiFi.status() == WL_CONNECTED) {
                    changeState(STATE_RUNNING);
                } else if (millis() - stateEnteredAt > 30000) {
                    // Timeout 30s → thử lại
                    WiFi.reconnect();
                    stateEnteredAt = millis();
                }
                break;

            case STATE_RUNNING:
                if (WiFi.status() != WL_CONNECTED) {
                    changeState(STATE_WIFI_CONNECT);
                }
                // Kiểm tra OTA mỗi giờ
                if (millis() - lastOtaCheck > 3600000) {
                    changeState(STATE_OTA_UPDATE);
                }
                break;

            case STATE_OTA_UPDATE:
                checkAndUpdateOTA(OTA_URL);
                changeState(STATE_RUNNING);
                lastOtaCheck = millis();
                break;

            case STATE_ERROR:
                Serial.println("[State] ERROR — restart sau 10s");
                vTaskDelay(pdMS_TO_TICKS(10000));
                esp_restart();
                break;
        }

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

Health Monitoring

struct DeviceHealth {
    uint32_t uptimeSeconds;
    uint32_t freeHeap;
    uint32_t freePsram;
    int8_t   wifiRssi;
    uint32_t mqttMessagesSent;
    uint32_t errorCount;
    char     firmwareVersion[16];
};

void collectHealth(DeviceHealth* health) {
    health->uptimeSeconds    = millis() / 1000;
    health->freeHeap         = ESP.getFreeHeap();
    health->freePsram        = psramFound() ? ESP.getFreePsram() : 0;
    health->wifiRssi         = WiFi.RSSI();
    health->mqttMessagesSent = g_mqttSentCount;
    health->errorCount       = g_errorCount;
    strncpy(health->firmwareVersion, FIRMWARE_VERSION, sizeof(health->firmwareVersion));
}

void healthTask(void* p) {
    DeviceHealth health;
    char payload[256];

    while (true) {
        vTaskDelay(pdMS_TO_TICKS(60000));  // Mỗi phút

        collectHealth(&health);

        // Cảnh báo nếu heap thấp
        if (health.freeHeap < 50 * 1024) {
            Serial.printf("[WARN] Low heap: %lu KB!\n", health.freeHeap / 1024);
            g_errorCount++;
        }

        // Gửi lên MQTT/HTTP
        snprintf(payload, sizeof(payload),
            "{\"uptime\":%lu,\"heap\":%lu,\"psram\":%lu,\"rssi\":%d,\"errors\":%lu}",
            health.uptimeSeconds, health.freeHeap / 1024,
            health.freePsram / 1024, health.wifiRssi, health.errorCount);

        Serial.printf("[Health] %s\n", payload);
        // mqttClient.publish("device/health", payload);
    }
}

Structured Logging

typedef enum { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel;

#define LOG_MIN_LEVEL LOG_INFO  // Thay đổi khi debug

void logMessage(LogLevel level, const char* module, const char* fmt, ...) {
    if (level < LOG_MIN_LEVEL) return;

    const char* levelStr[] = {"DEBUG", "INFO ", "WARN ", "ERROR"};
    char msg[256];

    va_list args;
    va_start(args, fmt);
    vsnprintf(msg, sizeof(msg), fmt, args);
    va_end(args);

    // Format: [LEVEL][uptime][module] message
    Serial.printf("[%s][%8lu][%-12s] %s\n",
        levelStr[level], millis() / 1000, module, msg);

    // Lưu ERROR vào NVS để đọc sau reset
    if (level == LOG_ERROR) {
        saveErrorToNVS(msg);
    }
}

// Dùng trong code:
// logMessage(LOG_INFO,  "WiFi",    "Connected: %s RSSI:%d", ssid, WiFi.RSSI());
// logMessage(LOG_ERROR, "Camera",  "Init failed: 0x%x", err);
// logMessage(LOG_WARN,  "Memory",  "Low heap: %lu KB", heap/1024);

Checklist Firmware Production

Trước khi deploy:

  • [ ] OTA endpoint hoạt động và có authentication
  • [ ] Hardware watchdog được bật và feed đúng cách
  • [ ] State machine có xử lý mọi lỗi và timeout
  • [ ] Heap monitor cảnh báo khi < 50 KB
  • [ ] Error count được tracking và gửi về server
  • [ ] Firmware version được log khi boot
  • [ ] WiFi reconnect tự động khi mất kết nối
  • [ ] Task tối quan trọng có priority cao và không blocking
  • [ ] Stack size được đo bằng HighWaterMark
  • [ ] NVS lưu crash log để đọc sau restart

Tổng Kết Series

Bạn đã hoàn thành Series Sức Mạnh ESP32-S3 Dual-Core:

BàiChủ ĐềĐiểm Chính
1ESP32-S3 là gìLX7, PSRAM, USB native, AI instructions
2Dual-Core LX7Core 0/1, cache, IRAM, pinToCore
3FreeRTOSTask, Queue, Semaphore, Mutex
4Thiết kế firmwareAnti-pattern, blueprint, checklist
5Memory architectureSRAM, PSRAM, heap, fragmentation
6DMA, LCD, CameraSPI DMA, I2S DMA, framebuffer
7USB OTGCDC, HID, Host, native vs UART
8Edge AISIMD, ESP-DSP, TFLite Micro
9Project thực tếCamera + LCD + Dual-Core hoàn chỉnh
10Production firmwareOTA, watchdog, state machine, health

Bước tiếp theo: Thực hành với board thật, tham gia cộng đồng IoTLabs và theo dõi các series tiếp theo tại IoTLabs.vn.


📚 Series: Sức Mạnh ESP32-S3 Dual-Core

⬅️ Bài trước: S3 Dual-Core – Bài 9: Project Camera + LCD + Dual-Core

ESP32-S3 còn được dùng để xây dựng hệ thống nhận diện giọng nói offline. Xem thêm: Hướng dẫn wake word detection với ESP32-S3, WakeNet và ESP-SR.