# Firmware OTA Strategy for ESP32-S3 Provider

**Authored by Antigravity**
**Date:** 2026-03-03

---

## 1. Goal

Implement a robust Over-The-Air (OTA) update mechanism for both the main firmware of the ESP32-S3 and the Svelte frontend. The update must:
- Update the core application logic and the user interface without requiring a physical USB connection.
- Keep the Firmware and Frontend in sync by allowing them to be updated together atomically.
- Provide a reliable fallback if an update fails (Rollback capability via A/B slots).
- Provide a permanent "factory" fallback as an extreme safety measure.
- Prevent accidental cross-flashing (e.g., flashing UI to firmware slots).
- Maintain a clear versioning scheme visible to the user, with accurate partition space reporting.

## 2. Chosen Approach

We implemented a **Universal Dual-Partition OTA system** using ESP-IDF's native OTA mechanisms for the firmware and LittleFS for the frontend.

Updates can be performed individually (Firmware only via `.bin`, Frontend only via `.bin`), but the primary and recommended approach is the **Universal OTA Bundle**. 
The build process generates a single `.bundle` file containing both the firmware image and the compiled frontend filesystem. This bundle is uploaded via the frontend UI, streamed directly to the inactive OTA flash partition (`ota_0` or `ota_1`) and inactive UI partition (`www_0` or `www_1`). Upon successful transfer and validation of both components, the bootloader and NVS are instructed to switch active partitions on the next restart.

## 3. Design Decisions & Trade-offs

### 3.1. Why Dual-Partition (A/B) with Factory?
- **Safety**: A failed or interrupted upload never "bricks" the device.
- **Factory Fallback**: By maintaining a dedicated 2MB `factory` partition alongside the two 2MB OTA partitions (`ota_0`, `ota_1`), we ensure that even if both OTA slots are irrecoverably corrupted, the device can always boot into a known-good state. 
- **Frontend Sync**: The frontend also uses a dual-partition layout (`www_0`, `www_1`). The Universal Bundle ensures both FW and UI switch together.

### 3.2. Automatic App Rollback
We rely on ESP-IDF's built-in "App Rollback" feature.
- **The Mechanism**: When the ESP32 boots a newly OTA-flashed firmware, it is marked as "Pending Verify". If the application crashes or fails to mark itself as "valid", the bootloader reverts to the previous working partition.
- **Validation Point**: We consider the firmware "valid" only after it successfully establishes a network connection.

### 3.3. Universal Bundle Format & Automation
- **Format**: A custom 12-byte header (`BNDL` magic + 4-byte FW size + 4-byte UI size) followed by the FW binary and UI binary.
- **Automation**: The Svelte build chain automates packaging. Running `npm run ota:bundle` automatically triggers Vite production build, LittleFS frontend packaging, applies proper semantic version sorting (to always pick the latest compiled UI), and generates the `.bundle` payload.

### 3.4. Safety & Validation
- **Magic Number Checks**: The backend enforces strict validation before writing to flash. Firmware endpoints and bundle streams check for the ESP32 image magic byte (`0xE9`), and Bundle endpoints check for the `BNDL` magic header. This prevents a user from accidentally uploading a LittleFS image to the Firmware slot, avoiding immediate boot loops.
- **Atomic Commits**: The Universal Bundle handler only sets the new boot partition and updates the NVS UI partition index if *both* firmware and frontend streams complete successfully. 

### 3.5. Versioning & Partition Metadata
- **Firmware Versioning**: Extracted natively from `esp_app_desc_t`, syncing API version with CMake `PROJECT_VER`.
- **Space Reporting**: The system dynamically scans App partitions using `esp_image_get_metadata()` to determine the exact binary size flashed in each slot. This allows the UI to display accurate "used" and "free" space per partition, regardless of the fixed partition size.

## 4. Final Architecture

### 4.1. The Partition Table
```csv
# Name,   Type, SubType,  Offset,   Size
nvs,      data, nvs,      0x9000,   0x6000
otadata,  data, ota,      ,         0x2000
phy_init, data, phy,      ,         0x1000
factory,  app,  factory,  ,         2M
ota_0,    app,  ota_0,    ,         2M
ota_1,    app,  ota_1,    ,         2M
www_0,    data, littlefs, ,         1M
www_1,    data, littlefs, ,         1M
```

### 4.2. Backend Components
- `bundle.cpp`: Handles `POST /api/ota/bundle`. Streams the file, splitting it on the fly into the inactive `ota` and `www` partitions.
- `firmware.cpp` & `frontend.cpp`: Handles individual component updates.
- `status.cpp`: Uses `esp_partition_find` and `esp_image_get_metadata` to report partition sizes and active slots.
- `main.cpp`: Calls `esp_ota_mark_app_valid_cancel_rollback()` post-network connection and manages NVS synchronization for the UI slot when booting from Factory.

### 4.3. UI/UX Implementation
- The Svelte Dashboard features a comprehensive "Update System" component supporting individual (FW/UI) and combined (Bundle) uploads.
- A "Partition Table" view provides real-time visibility into the exact binary size, available free space, and version hash of every system and app partition.

## 5. Summary

We use **ESP-IDF's native OTA APIs** with a **Factory + Dual A/B Partition** layout, synchronized with a **Dual LittleFS Partition** layout for the frontend. The system relies on custom **Universal Bundles** to guarantee atomic FW+UI upgrades, protected by **Magic Number validations** and **Automatic App Rollbacks**. The entire process is driven from a highly integrated Svelte UI that leverages backend metadata extraction to provide accurate system insights.

---
*Created by Antigravity - Last Updated: 2026-03-03*
