1539  Software Prototyping: Over-the-Air Updates

1539.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Design OTA Architecture: Plan secure firmware update infrastructure for IoT deployments
  • Implement OTA Updates: Add wireless firmware update capability to ESP32 and similar devices
  • Secure Update Process: Apply code signing, verification, and encrypted transport
  • Handle Rollback: Implement automatic recovery from failed updates

1539.2 Prerequisites

Before diving into this chapter, you should be familiar with:


1539.3 Why OTA Updates Matter

Moving from prototype to production requires planning for the entire device lifecycle, including how you’ll update firmware after deployment. OTA updates are essential for fixing bugs, patching security vulnerabilities, and adding features without physical access to devices.

ImportantPlan for OTA from Day One

Security patches: Fix vulnerabilities discovered after deployment (essential for internet-connected devices)

Feature updates: Add capabilities based on user feedback without hardware recall

Bug fixes: Resolve issues in the field without customer support visits

Compliance: Meet regulations like EU Cyber Resilience Act (mandatory for devices sold in EU after 2027)

Real-world example: A smart thermostat deployed in 2025 may still operate in 2035. Security standards from 2025 will be obsolete by then - OTA updates are the only way to keep devices secure over their 10+ year lifespan.

1539.4 OTA Architecture Overview

A complete OTA system involves infrastructure beyond just the device firmware:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#ECF0F1'}}}%%
graph TB
    subgraph "Development Pipeline"
        DEV["Developer<br/>Writes Code"] --> BUILD["Build Server<br/>GitHub Actions"]
        BUILD --> SIGN["Signing Service<br/>Code Signing Key"]
    end

    subgraph "Distribution Infrastructure"
        SIGN --> STORAGE["Cloud Storage<br/>AWS S3 / Azure Blob"]
        STORAGE --> UPDATE["Update Server<br/>Version Management"]
    end

    subgraph "IoT Devices"
        UPDATE --> CHECK["Device Checks<br/>for Updates"]
        CHECK --> DOWNLOAD["Download<br/>Firmware"]
        DOWNLOAD --> VERIFY["Verify Signature"]
        VERIFY --> INSTALL["Install Update"]
        INSTALL --> REBOOT["Reboot Device"]
    end

    REBOOT -.->|Health Check Fails| ROLLBACK["Rollback to<br/>Previous Version"]
    REBOOT -.->|Success| CONFIRM["Confirm Update"]

    style DEV fill:#E67E22,stroke:#2C3E50,color:#fff
    style SIGN fill:#E74C3C,stroke:#2C3E50,color:#fff
    style VERIFY fill:#E74C3C,stroke:#2C3E50,color:#fff
    style INSTALL fill:#16A085,stroke:#2C3E50,color:#fff
    style ROLLBACK fill:#E67E22,stroke:#2C3E50,color:#fff

OTA System Components:

Component Purpose Example Technologies
Build Server Compile firmware, run CI/CD GitHub Actions, Jenkins
Signing Service Cryptographically sign firmware espsecure.py, OpenSSL, AWS KMS
Distribution Store firmware files AWS S3, Azure Blob, Firebase Storage
Update Server Manage versions, orchestrate rollouts AWS IoT Jobs, Azure IoT Hub
Device Agent Download and install updates ESP-IDF OTA library, Arduino OTA

1539.5 Security Requirements Checklist

OTA updates are a critical attack surface. Implement these security controls:

WarningSecure OTA Implementation Checklist

Mandatory Security Controls:

Best Practices:

1539.6 ESP32 OTA Implementation

1539.6.1 Arduino OTA (Simple, for Prototypes)

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

void setup() {
  Serial.begin(115200);
  WiFi.begin("SSID", "PASSWORD");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // Configure OTA
  ArduinoOTA.setHostname("esp32-sensor");
  ArduinoOTA.setPassword("update-password");

  ArduinoOTA.onStart([]() {
    Serial.println("OTA Update Starting...");
  });

  ArduinoOTA.onEnd([]() {
    Serial.println("\nOTA Update Complete!");
  });

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

  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("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");
}

void loop() {
  ArduinoOTA.handle(); // Must call in loop

  // Your application code here
}

Usage: Upload new firmware wirelessly via Arduino IDE (Tools -> Port -> Network Port)

1539.6.2 ESP-IDF OTA (Production-grade, with Rollback)

#include "esp_https_ota.h"
#include "esp_ota_ops.h"

void check_for_update() {
  esp_http_client_config_t config = {
    .url = "https://your-server.com/firmware.bin",
    .cert_pem = server_cert_pem,  // Server certificate for TLS
    .timeout_ms = 30000,
  };

  esp_https_ota_config_t ota_config = {
    .http_config = &config,
  };

  ESP_LOGI(TAG, "Starting OTA update...");
  esp_err_t ret = esp_https_ota(&ota_config);

  if (ret == ESP_OK) {
    ESP_LOGI(TAG, "OTA successful! Rebooting...");
    esp_restart();
  } else {
    ESP_LOGE(TAG, "OTA failed: %s", esp_err_to_name(ret));
  }
}

void app_main() {
  // Check if this is first boot after OTA
  const esp_partition_t *running = esp_ota_get_running_partition();
  esp_ota_img_states_t ota_state;

  if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
    if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
      // First boot after update - run health checks
      if (run_health_checks()) {
        ESP_LOGI(TAG, "OTA update successful, marking as valid");
        esp_ota_mark_app_valid_cancel_rollback();
      } else {
        ESP_LOGE(TAG, "Health checks failed, rolling back");
        esp_ota_mark_app_invalid_rollback_and_reboot();
      }
    }
  }
}

Key Features: - Downloads firmware over HTTPS (TLS 1.3) - Verifies signature against public key - Writes to inactive partition - Automatically rolls back if boot fails 3 times

1539.7 Rollback Scenarios

Your OTA system must handle failures gracefully:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#ECF0F1'}}}%%
stateDiagram-v2
    [*] --> RunningV1: Device boots
    RunningV1 --> DownloadingV2: OTA update available
    DownloadingV2 --> VerifyingV2: Download complete
    VerifyingV2 --> InstallingV2: Signature valid
    VerifyingV2 --> RunningV1: Signature invalid (rollback)

    InstallingV2 --> TestingV2: Reboot to v2.0
    TestingV2 --> RunningV2: Health checks pass
    TestingV2 --> RunningV1: Health checks fail (rollback)

    RunningV2 --> DownloadingV3: Next OTA update

Rollback Triggers:

  1. Boot failure: Watchdog timeout -> automatic rollback
  2. Health check failure: Sensor offline, Wi-Fi fails, cloud unreachable
  3. Manual rollback: Operator command via device management
  4. Timeout: No “update successful” confirmation within 10 minutes

Health Check Implementation:

bool run_health_checks() {
  // Test 1: Can we read sensors?
  if (!test_sensor_readings()) {
    ESP_LOGE(TAG, "Health check failed: Sensors not responding");
    return false;
  }

  // Test 2: Can we connect to Wi-Fi?
  if (WiFi.status() != WL_CONNECTED) {
    ESP_LOGE(TAG, "Health check failed: Wi-Fi not connected");
    return false;
  }

  // Test 3: Can we reach cloud server?
  if (!test_cloud_connection()) {
    ESP_LOGE(TAG, "Health check failed: Cloud unreachable");
    return false;
  }

  ESP_LOGI(TAG, "All health checks passed!");
  return true;
}

1539.8 OTA Update Strategies

Strategy Pros Cons Best For
Full image Simple, guaranteed consistency Large download (1-5 MB) Prototypes, good bandwidth
Delta/diff Small download (10-100 KB) Complex, requires base version NB-IoT, LoRaWAN, cellular
Staged rollout Safe, detect issues early Slow, version fragmentation Production deployments
Immediate Fast, all devices updated Risky if update is broken Development/testing only

1539.9 Common OTA Pitfalls

Mistake Why It’s Bad Solution
No rollback mechanism Broken update bricks all devices Implement A/B partitions + automatic rollback
Unsigned firmware Attackers can install malicious code Always sign firmware, verify on device
Update all devices immediately Broken update affects entire fleet Staged rollout (1% -> 10% -> 100%)
No health checks Device boots but doesn’t work properly Validate sensors, Wi-Fi, cloud connectivity
Hardcoded update server URL Can’t change server after deployment Use configurable endpoint or DNS
Large firmware binaries Slow downloads, high bandwidth costs Optimize binary size, use delta updates
No version tracking Can’t identify which firmware is running Embed version in firmware, report to cloud

1539.10 Knowledge Check

Question: Select the code that correctly implements ESP32 OTA with validation and rollback capability:

  • Explanation: Option B is correct implementing production OTA: (1) Size verification before download, (2) Stream to avoid RAM exhaustion, (3) SHA256 hash check prevents corrupted firmware, (4) Atomic commit via Update.end(), (5) Explicit rollback on error via Update.abort(). ESP32’s dual partition scheme downloads to inactive partition, verifies, then swaps boot partition - failed update doesn’t affect running firmware.

    1539.11 What’s Next

    The next section covers Testing and Debugging, where you’ll learn unit testing, serial debugging, hardware debugging with JTAG/SWD, and remote logging techniques for IoT firmware.