example

Example of the component espp/ota v1.3.6
# OTA Example

This example shows how to use the `espp::Ota` component to update a device's
firmware over **three transports feeding the exact same engine**:

1. **USB vendor / WebUSB** — an `espp::UsbDevice` vendor interface
   (bInterfaceClass 0xFF, WebUSB + MS OS 2.0 descriptors) carrying the framed
   OTA stream protocol from `detail/ota_stream_protocol.hpp`. Update straight
   from a Chromium browser with the hosted
   [espp OTA Console](https://esp-cpp.github.io/espp/apps/ota_console.html)
   (source: [`components/ota/web/ota_console.html`](../web/ota_console.html)) —
   no driver, no network.
2. **WiFi HTTP push** — an `esp_http_server` with `POST /ota` that streams the
   raw request body into the engine (Content-Length is the image size):

   ```bash
   curl --data-binary @build/ota_example.bin http://<ip>/ota
   # with EXAMPLE_OTA_HTTP_TOKEN configured:
   curl -H "Authorization: Bearer <token>" --data-binary @build/ota_example.bin http://<ip>/ota
   ```

   > **Security**: by default (empty `EXAMPLE_OTA_HTTP_TOKEN`) this endpoint is
   > **unauthenticated** — any peer that can reach the device can install a
   > structurally-valid image; a warning is logged at startup. Set
   > `EXAMPLE_OTA_HTTP_TOKEN` in menuconfig to require a bearer token (the
   > upload page has a matching field). A shared token is demo-grade gating
   > only: for real deployments enable **secure boot / signed images** so the
   > bootloader rejects unauthorized firmware regardless of transport.

3. **Browser HTTP upload** — `GET /ota` serves a tiny self-contained upload
   page (file picker + progress bar), so any browser on the LAN can update the
   board.

The HTTP server binds to every network interface, so the **same code works
unchanged over the espp `ethernet` component** (or any other `esp_netif`) —
Ethernet needs no separate OTA code path, just bring up its netif instead of
(or in addition to) `espp::WifiSta`.

## How to use example

### Hardware Required

An ESP32-S3 (the native USB-OTG peripheral is required for the USB / WebUSB
transport; the target is pinned in `sdkconfig.defaults`).

**Console routing.** On the ESP32-S3 the USB-Serial-JTAG console and the USB-OTG
controller share the same internal USB PHY / port, so the console cannot stay on
USB-Serial-JTAG once this example hands that port to TinyUSB — doing so
reboot-loops the device. Instead the console is set up so a **single native USB
cable carries both the OTA stream and the logs**:

- **primary console: UART0** — always available (connect a UART / USB-UART
  adapter for `idf.py monitor`); TinyUSB never touches it, so it is the safe
  fallback.
- **secondary console: USB-Serial-JTAG** — carries the early-boot / bootloader
  logs on the native USB port *before* the app brings up TinyUSB.
- once TinyUSB is up the example adds a **USB-CDC** interface and **reroutes the
  console (stdout) to it**, teeing to UART0 as well. So on the native USB port
  you see boot logs over USB-Serial-JTAG and then, seamlessly, the running app's
  logs over USB-CDC — alongside the OTA vendor / WebUSB interface on the same
  cable. (The CDC console only emits while a host has the CDC port open, and
  never blocks the app if nothing is reading it.)

### Configure

```bash
idf.py menuconfig
```

Set the WiFi SSID / password under `OTA Example Configuration` (only needed for
the HTTP transports; the USB transport works without any network).

### Build and Flash

Build the project and flash it to the board, then run monitor tool to view
serial output:

```bash
idf.py -p PORT flash monitor
```

(To exit the serial monitor, type ``Ctrl-]``.)

The first flash writes the app into the `ota_0` slot of the factory-less
partition table (`partitions.csv`: `otadata` + `ota_0` + `ota_1`); every OTA
update alternates to the other slot.

### Update over USB (WebUSB)

1. Open <https://esp-cpp.github.io/espp/apps/ota_console.html> in a Chromium
   browser (or open `components/ota/web/ota_console.html` from disk).
2. Connect to the "espp OTA" device, pick the new `build/ota_example.bin`, and
   press Upload. The console streams BEGIN / DATA / FINISH frames (4 KiB max
   payload each, one in flight) with a progress bar; the device validates the
   image, replies OK, and restarts into it.

### Update over HTTP (WiFi or Ethernet)

- Browser: open `http://<ip>/ota`. The page shows a **status card** — the
  currently-running firmware (project + version) and whether it is still
  **pending verify** — with **Mark valid** / **Roll back** buttons, then the
  file picker. Pick the `.bin` and upload.
- CLI: `curl --data-binary @build/ota_example.bin http://<ip>/ota` — returns
  `{"status":"ok",...}` on success or a 4xx/5xx JSON error.

The device exposes these HTTP endpoints (the mutating ones honor the same
optional `EXAMPLE_OTA_HTTP_TOKEN` bearer token as `POST /ota`):

| method + path | purpose |
|---|---|
| `GET /ota` | the upload page (status card + file picker) |
| `POST /ota` | stream a raw `.bin` image (Content-Length = size) |
| `GET /status` | JSON: `{project, version, pending_verify, rollback_supported}` |
| `POST /mark-valid` | confirm the running image (cancel rollback) |
| `POST /rollback` | reject the running image: roll back + reboot |

So the whole host-driven flow works from a plain browser on the LAN: upload →
the device reboots into the new image (now *pending verify*) → reopen the page →
it shows **PENDING VERIFY** → press **Mark valid** to confirm it.

### Rollback semantics

`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y` is set, so a freshly-installed image
boots in the `PENDING_VERIFY` state and rolls back to the previous slot on the
next reset unless it is confirmed. This example demonstrates **host-driven**
confirmation: the app deliberately does **not** mark itself valid. On boot it
logs the running partition + version, and if the image is pending verify it just
logs that it is **waiting for the host to confirm it** — a broken build could
otherwise self-validate right before failing. The **host** confirms the image
(`MARK_VALID` over the OTA protocol) once it has checked the device is healthy:
the [ota-console web app](../web/ota_console.html) prompts to confirm on
reconnect, and `espp-ota flash` auto-verifies (reconnects after the reboot,
reads status, and marks the image valid if it booted and responded). `status`,
`mark-valid`, and `rollback` (mark invalid + reboot to the previous image) are
also available to do it manually.

(If your own product prefers device self-validation instead, run your health
checks at boot and call `espp::Ota::mark_app_valid()` /
`mark_app_invalid_and_rollback()` directly — see the commented note in
`ota_example.cpp`.)

Note: `espp::Ota::finish()` only validates the image and sets the boot
partition; the restart is a separate explicit `restart()` call (this example
restarts ~750 ms after replying to the host, on any transport).

## Standard USB services

Like every espp USB example, this one serves the standard service set on its
framed USB link(s) next to its own protocol, so the hosted consoles and the
[Device Hub](https://esp-cpp.github.io/espp/apps/dispatcher_hub.html) (which
finds each service through discovery, by protocol id) work against it:

| Service | Module (default) | Protocol id | Console |
|---|---|---|---|
| `espp::SystemService` -- device info, reboot, reboot into the bootloader | 7 | `espp.system` | [system console](https://esp-cpp.github.io/espp/apps/system_console.html) |
| `espp::MonitorService` -- heap regions + task table, on request or streamed | 8 | `espp.monitor` | system console |
| `espp::OtaService` -- firmware update (host-driven rollback confirmation) | 0 | `espp.ota` | [OTA console](https://esp-cpp.github.io/espp/apps/ota_console.html) |
| `espp::CoreDumpService` -- last-crash report, core dump download / erase | 4 | `espp.coredump` | [coredump console](https://esp-cpp.github.io/espp/apps/coredump_console.html) |

`partitions.csv` therefore carries the OTA layout (`otadata`, `ota_0`, `ota_1`)
plus a `coredump` partition, and `sdkconfig.defaults` enables core dumps to
flash, OTA rollback and the FreeRTOS run-time statistics the task monitor
reads. Every device->host write on a transport goes through one mutex, so the
services (and any streaming) never interleave frames.

## Example Output

```
I (608) OtaExample: Running 'ota_example' version 'v1.2.3-14-g35e120b' (built Aug 19 2026 12:34:56) from partition 'ota_0' (1966080 bytes)
I (618) OtaExample: Next update will target partition 'ota_1' (1966080 bytes)
I (668) espp_UsbDevice: USB device initialized (vendor/WebUSB interface ready)
I (670) OtaExample: Routing console to USB-CDC (single cable: OTA + logs; UART0 stays teed).
I (672) OtaExample: Console is now also on USB-CDC.
I (5178) OtaExample: got IP: 192.168.1.23
I (5178) OtaExample:   browser upload page: http://192.168.1.23/ota
I (5178) OtaExample:   curl --data-binary @build/ota_example.bin http://192.168.1.23/ota
I (5188) OtaExample: HTTP OTA server ready: GET /ota (upload page), POST /ota (raw image)
I (5198) OtaExample: OTA example ready; transports: USB vendor/WebUSB, HTTP POST /ota (WiFi/Ethernet)
I (42198) Ota: incoming firmware: project 'ota_example', version 'v1.2.4', built Aug 20 2026 09:00:00 (IDF v6.0)
I (55123) Ota: finish: 1204224 bytes validated; boot partition set to 'ota_1' — call restart() to boot the new image
...reboot...
W (612) OtaExample: This image is PENDING VERIFY (first boot after an OTA update). Waiting for the host to confirm it (MARK_VALID); it rolls back on the next reset if not.
...host reconnects and confirms (ota-console prompt / `espp-ota flash` auto-verify)...
I (9051) Ota: running app marked valid; rollback cancelled
```

To create a project from this example, run:

idf.py create-project-from-example "espp/ota=1.3.6:example"

or download archive (~17.63 KB)