Firmware Over-the-Air (FOTA) — IoT Device Update Management Guide
In this tutorial, you'll learn about Firmware Over. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Firmware Over-the-Air (FOTA) is the process of remotely updating the firmware on deployed IoT devices via wireless communication, eliminating the need for physical access to apply security patches, bug fixes, or feature upgrades.
Why FOTA Matters
A deployed IoT device might be inside a concrete wall, on a factory floor, or in a weather station on a mountain. Sending a technician to update firmware costs $50-500 per device. For a fleet of 10,000 devices, that's $500,000 per update cycle. FOTA reduces this to zero. Security vulnerabilities in IoT firmware are discovered weekly. Without OTA updates, devices remain vulnerable indefinitely. The Mirai botnet exploited devices that never received updates. Durga Antivirus Pro's IoT Security module includes automated FOTA with signed firmware verification to prevent compromised updates from reaching devices.
Plain-Language Explanation
Think of your smartphone. When an OS update is available, it downloads, verifies, and installs automatically. You never plug it into a computer. FOTA does the same for IoT devices — download new firmware over WiFi, LoRaWAN, or BLE, verify its authenticity, write it to flash, and reboot.
The challenge is doing this reliably on devices with limited memory, no user interface, and potentially unreliable network connections. A failed update can brick the device permanently. FOTA strategies use techniques like A/B Partitioning, delta updates, and staged rollouts to ensure reliability.
graph TD
Cloud[FOTA Server
Firmware Repository] -->|Signed Firmware Binary| Download[Device Downloads]
Download -->|Verify Signature| Validate{Signature Valid?}
Validate -->|Yes| Write[Write to Flash]
Validate -->|No| Abort[Abort Update]
Write -->|Old Slot| SlotA[Slot A
Current Firmware]
Write -->|New Slot| SlotB[Slot B
New Firmware]
SlotB --> Reboot[Reboot Device]
Reboot -->|Success| Active[Run New Firmware]
Reboot -->|Failure| Rollback[Rollback to Slot A]
style Cloud fill:#e67e22,color:#fff
style SlotA fill:#3498db,color:#fff
style SlotB fill:#27ae60,color:#fff
style Rollback fill:#e74c3c,color:#fff
Update Strategies
A/B Partitioning (Dual Slot): Flash is divided into two slots. Slot A runs current firmware. Slot B receives the update. On reboot, the device switches to Slot B. If it fails to boot, firmware in Slot A remains intact. This requires 2x flash space but provides instant, zero-downtime rollback.
Delta Updates: Instead of transferring the entire firmware binary, the device downloads only the difference (delta) between the old and new firmware. Tools like diff/patch or bsdiff generate deltas that are 70-90% smaller than full binaries. Critical for bandwidth-constrained links like LoRaWAN.
Staged Rollout: The update is deployed to 1% of devices first. If no failures or crashes reported within 24 hours, rollout expands to 10%, then 50%, then 100%. Catches problems before they affect the entire fleet.
Firmware Signing
Every Firmware Update must be cryptographically signed. The device verifies the signature before applying the update:
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec, utils
def sign_firmware(firmware_path: str, private_key_path: str) -> bytes:
with open(firmware_path, 'rb') as f:
firmware = f.read()
firmware_hash = hashlib.sha256(firmware).digest()
with open(private_key_path, 'rb') as f:
private_key = ec.load_der_private_key(f.read(), password=None)
signature = private_key.sign(
firmware_hash,
ec.ECDSA(hashes.SHA256())
)
return signature
def verify_firmware(firmware_path: str, signature: bytes,
public_key_path: str) -> bool:
with open(firmware_path, 'rb') as f:
firmware = f.read()
firmware_hash = hashlib.sha256(firmware).digest()
with open(public_key_path, 'rb') as f:
public_key = ec.load_der_public_key(f.read())
try:
public_key.verify(signature, firmware_hash, ec.ECDSA(hashes.SHA256()))
return True
except Exception:
return False
ESP32 OTA Implementation
#include <WiFi.h>
#include <HTTPClient.h>
#include <Update.h>
const char* WIFI_SSID = "YourNetwork";
const char* WIFI_PASS = "YourPassword";
const char* FIRMWARE_URL = "https://ota.dodatech.com/firmware/esp32-v2.1.bin";
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
Serial.println("WiFi connected");
HTTPClient http;
http.begin(FIRMWARE_URL);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
int contentLength = http.getSize();
bool canUpdate = Update.begin(contentLength);
if (canUpdate) {
WiFiClient* stream = http.getStreamPtr();
size_t written = Update.writeStream(*stream);
if (written == contentLength && Update.end()) {
Serial.println("Update complete, restarting...");
ESP.restart();
} else {
Serial.printf("Update failed: %s\n", Update.errorString());
}
}
}
http.end();
}
void loop() {
// Normal device operation
}
Expected Serial output:
WiFi connected
Starting OTA update: 524288 bytes
Update complete, restarting...
Python FOTA Server
from http.server import HTTPServer, BaseHTTPRequestHandler
import os, json
FIRMWARE_DIR = "/srv/fota"
DEVICE_REGISTRY = {}
class FOTAHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/api/v1/manifest":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
manifest = {
"latest_version": "2.1.0",
"release_date": "2026-06-24",
"files": [
{
"name": "esp32-firmware-v2.1.0.bin",
"size": 524288,
"sha256": "a1b2c3...",
"url": "/firmware/esp32-v2.1.0.bin]
}
]
}
self.wfile.write(json.dumps(manifest).encode())
elif self.path.startswith("/firmware/"):
filename = self.path.split("/")[-1]
filepath = os.path.join(FIRMWARE_DIR, filename)
if os.path.exists(filepath):
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(os.path.getsize(filepath)))
self.end_headers()
with open(filepath, "rb") as f:
self.wfile.write(f.read())
else:
self.send_response(404)
self.end_headers()
server = HTTPServer(("0.0.0.0", 8080), FOTAHandler)
server.serve_forever()
Delta Updates with bsdiff
import subprocess, os
def generate_delta(old_firmware: str, new_firmware: str, delta_path: str):
subprocess.run(["bsdiff", old_firmware, new_firmware, delta_path], check=True)
def apply_delta(old_firmware: str, delta_path: str, output_path: str):
subprocess.run(["bspatch", old_firmware, output_path, delta_path], check=True)
# Delta is typically 10-30% of full binary size
old_size = os.path.getsize("firmware-v2.0.bin")
new_size = os.path.getsize("firmware-v2.1.bin")
delta_size = os.path.getsize("delta-v2.0-to-v2.1.bin")
print(f"Old: {old_size} bytes, New: {new_size} bytes, Delta: {delta_size} bytes")
print(f"Delta is {delta_size / new_size * 100:.1f}% of full binary")
Expected output:
Old: 524288 bytes, New: 530432 bytes, Delta: 85120 bytes
Delta is 16.0% of full binary
Common Mistakes
No rollback mechanism: If the new firmware fails to boot, the device is bricked. Always implement A/B Partitioning or a recovery bootloader.
Unsecured update transport: HTTP downloads without TLS allow man-in-the-middle attacks to inject malicious firmware. Always use HTTPS with certificate pinning.
Insufficient power during update: If power fails during flash write, the firmware is corrupted. Use brownout detection, a backup power source, or check battery level before starting.
Version mismatch between hardware: Not all devices in a fleet have the same peripherals. Distributing the same binary to all devices can break hardware-specific configurations.
No staged rollout: A bug in firmware v2.1 that causes all devices to crash simultaneously is catastrophic. Deploy to a small subset first, monitor telemetry, then expand.
Practice Questions
What is A/B Partitioning and why is it used? A/B Partitioning (dual slot) keeps two firmware copies on flash. One runs while the other is updated. On failure, the device boots from the known-good slot, preventing bricking.
How do delta updates reduce bandwidth consumption? Delta updates transfer only the binary difference between old and new firmware, typically 10-30% of the full binary size. Critical for bandwidth-constrained links like LoRaWAN.
Why should firmware be cryptographically signed? Signing prevents attackers from distributing malicious firmware. The device verifies the signature before applying the update, ensuring authenticity and integrity.
What is a staged rollout? A staged rollout deploys an update to a small percentage of devices first. If telemetry shows no issues, the rollout percentage increases incrementally until all devices are updated.
What happens if power is lost during a Firmware Update? Without safeguards, the device bricks. A/B Partitioning keeps the old slot intact. A recovery bootloader can reflash from a known-good image if corruption is detected.
Mini Project
Build a mock FOTA update simulation:
import hashlib, os, json
class FOTAUpdate:
def __init__(self, device_id: str, flash_size: int = 1048576):
self.device_id = device_id
self.flash = bytearray(flash_size)
self.active_slot = 0
self.slot_size = flash_size // 2
self.firmware_versions = ["", ""]
def write_firmware(self, slot: int, data: bytes, version: str):
start = slot * self.slot_size
end = start + len(data)
if end > len(self.flash):
raise ValueError("Firmware too large for slot")
self.flash[start:end] = data
self.firmware_versions[slot] = version
print(f"Wrote v{version} to Slot {'A' if slot == 0 else 'B'}")
def verify_and_boot(self, slot: int, expected_hash: str) -> bool:
start = slot * self.slot_size
data = bytes(self.flash[start:start + self.slot_size]).rstrip(b'\x00')
actual_hash = hashlib.sha256(data).hexdigest()
if actual_hash == expected_hash:
self.active_slot = slot
print(f"Booted from Slot {'A' if slot == 0 else 'B'}: v{self.firmware_versions[slot]}")
return True
print(f"Hash mismatch on Slot {'A' if slot == 0 else 'B'}")
return False
def rollout_update(self, new_firmware: bytes, version: str, firmware_hash: str):
inactive_slot = 1 if self.active_slot == 0 else 0
self.write_firmware(inactive_slot, new_firmware, version)
if self.verify_and_boot(inactive_slot, firmware_hash):
print(f"Update successful: v{self.firmware_versions[self.active_slot]}")
else:
self.verify_and_boot(self.active_slot, firmware_hash)
print("Rollback to previous firmware")
device = FOTAUpdate("sensor-01")
device.write_firmware(0, b"old_firmware_data_" * 1000, "1.0.0")
device.verify_and_boot(0, hashlib.sha256(b"old_firmware_data_" * 1000).hexdigest())
new_fw = b"new_firmware_data_" * 1100
new_hash = hashlib.sha256(new_fw).hexdigest()
device.rollout_update(new_fw, "2.0.0", new_hash)
Expected output:
Wrote v1.0.0 to Slot A
Booted from Slot A: v1.0.0
Wrote v2.0.0 to Slot B
Booted from Slot B: v2.0.0
Update successful: v2.0.0
Cross-References
- ESP32
- IoT Security
- IoT Overview
- MQTT
- LoRaWAN Deep Dive
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro