انتقل إلى المحتوى الرئيسي

OTA Updates

An OTA system has one non-negotiable requirement: no input, no timing, and no power cut may leave the device unbootable. Everything in this chapter descends from that sentence.

A/B slots and rollback, from zero

The flash carries three application slots (partition layout): factory (the USB-flashed baseline) and ota_0/ota_1. An update never touches the running slot; it writes the other OTA slot, then flips a pointer:

The pointer lives in the tiny otadata partition, written by esp_ota_set_boot_partition() as two redundant copies with sequence counters, so a power cut mid-flip preserves a consistent choice. (This partition's absence was an instructive production bug; the story is told with the partition table.)

Rollback is the half most systems skip: the new slot boots in PENDING_VERIFY state, and the app must call esp_ota_mark_app_valid_cancel_rollback() after its own health checks pass (Safi confirms late in app_main, once core services stand). Crash before confirming, or hang into the watchdog, and the bootloader's next boot returns to the previous slot as if nothing happened. The result is the property promised above: the worst possible update is a no-op.

A power cut during download is even less dramatic: the running firmware was never left, and the half-written spare slot is garbage that the next attempt overwrites.

Two transports, one pipeline

Both dashboard paths converge on esp_https_ota / esp_ota_* writes with progress events (EVENT_OTA_PROGRESS, mirrored to the display and every WebSocket client):

Upload (POST /api/ota/upload): the browser streams Safi.bin up; the handler writes it chunk-by-chunk to the spare slot. No staging buffer exists or could: the image (2+ MB) outweighs all RAM. For the person standing next to the device, this path has no external dependencies at all.

Pull (POST /api/ota/pull with a URL): the device downloads the image itself, for updating devices you are not next to. Pull is where the security model concentrates, because a URL is an instruction to fetch and execute code:

  • HTTPS with full certificate verification against the CA bundle, non-negotiable in release builds. The development-only escape hatch (CONFIG_SAFI_ALLOW_INSECURE_OTA_PULL, for a local HTTP server on the bench) is structurally excluded from production: the production gates make the build fail if both are set, a compile error being the one warning nobody can scroll past.
  • Optional URL prefix pinning (CONFIG_SAFI_OTA_SERVER_URL_PREFIX): the device refuses pulls from anywhere but your release host, shrinking "attacker with a valid TLS cert somewhere" to "attacker controlling your release host".
  • Rate limited to 2 attempts per minute like every expensive endpoint.

Image authenticity beyond TLS comes from secure boot when enabled: a signed-image chain means even a compromised release host cannot produce a bootable malicious image; see security.

Progress and failure UX

The OTA task publishes state transitions (connecting, downloading with percent, error with the esp_err name) consumed by GET /api/ota/status, the WebSocket, and the display's progress screen with its "do not power off" warning (which is politeness, not protection; the design above survives ignoring it). Errors are terminal per attempt and leave the device exactly as it was, which is the only failure UX an updater should ever have.

What is deliberately absent

No automatic update checking, no phoning home: an appliance in a bedroom fetches code when and only when its owner says so. No delta updates: full images at 2 MB over LAN or broadband finish in seconds, and delta reconstruction on-device would add a failure mode to the one subsystem that must not have any. Boring is the feature.