diff options
| author | Luke Hoersten <[email protected]> | 2026-06-20 13:03:23 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-20 13:03:23 -0500 |
| commit | 175dd50ba2a2a6c8d033a9fd4e91f4823f9e210a (patch) | |
| tree | 2f41235b0a4792822f737953e6fa553383a7bd36 /README.md | |
| parent | 3729f04458e358fab7872b9590d9fdb012f5ddf9 (diff) | |
firmware: OTA firmware updates via POST /firmware + rollback
Streams the raw .bin to the inactive ota_0/ota_1 slot via esp_ota_*, flips otadata, replies 200, reboots after 500 ms. Single-shot guarded by atomic_flag (409 on concurrent). CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE armed: new images boot pending-verify and ota_arm_healthy_timer marks them valid after 30 s of healthy uptime; otherwise the bootloader reverts on next reset. /state gains ota_state.
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 60 |
1 files changed, 59 insertions, 1 deletions
@@ -93,6 +93,23 @@ Scrypted-side discovery flow: If mDNS-SD is unavailable in the deployment (some Docker setups, certain VLAN configurations), allow the operator to set an explicit `http://<ip>:<port>` per viewport in Scrypted-side config as a fallback. +### Discover from the CLI + +The custom service type is the discovery handle — no need to know names ahead of time. From macOS: + +```sh +# List every viewport on the LAN +dns-sd -B _scrypted-viewport._tcp local. + +# Resolve one to an IPv4 address +dns-sd -G v4 viewport-kitchen.local. + +# Talk to it (mDNS resolves the hostname directly) +curl http://viewport-kitchen.local/state +``` + +On Linux: `avahi-browse -rt _scrypted-viewport._tcp` for the same effect with addresses inline. The instance name (`viewport-kitchen`) **is** the hostname prefix — they're the same string. + ## API Four endpoints. `GET /state` and `GET /config` are the read surface; `POST /config`, `POST /state`, and `POST /frame` are the write surface. @@ -192,6 +209,37 @@ Paints a frame. Does **not** change wake/sleep state. - Returns `204` once decoded and pushed to the panel. - `400` malformed JPEG, `409` device asleep, `413` over size, `500` decode/display failure. On failure the previous frame stays on screen. +### POST /firmware + +Push a new application image and reboot into it. The device's HTTP OTA +endpoint — replaces USB reflash once the device is on the LAN. + +- `Content-Type: application/octet-stream`, body is the raw built app + image (`build/scrypted-viewport.bin`, ~1.5 MB). `Content-Length` + required. +- Single-shot: a second concurrent POST returns `409 Conflict`. +- Streams straight to the inactive OTA slot (no full-image RAM buffer), + validates header + checksum on `esp_ota_end`, flips `otadata` to point + at the new slot, replies `200` with + `{"status":"ok","previous":"<git>","next":"<git>","slot":"ota_1","reboot_in_ms":500}`, + then reboots ~500 ms later so the response can flush. +- On failure (`400` bad body / validate failed, `413` over partition size, + `500` flash error) the OTA handle is aborted and the running image stays + live. No partial-write half-state. +- **Rollback armed.** The new image boots `pending-verify`; firmware + flips it to `valid` after 30 s of healthy uptime + (`ota_arm_healthy_timer`). If the new image panics or the device is + power-cycled before the timer fires, the bootloader reverts to the + previous slot on next reset. `/state` reports the current state via + `ota_state`. + +```sh +idf.py build +curl -v --data-binary @build/scrypted-viewport.bin \ + -H 'Content-Type: application/octet-stream' \ + http://<device-ip>/firmware +``` + ## Wake / Sleep Wake and sleep couple the device backlight with Scrypted's frame stream. The device owns the state. @@ -422,7 +470,7 @@ Scrypted should use the same `idle_timeout_ms` value it sent in `/config` as its ## Ops -- Firmware updates: reflash over USB. No OTA in v1 (planned post-v1: HTTP OTA from Scrypted). +- Firmware updates: `POST /firmware` with the raw built `.bin` (see [POST /firmware](#post-firmware)). First flash of any new device still needs USB to install the bootloader + initial image; every update after that is over the LAN. Rollback is armed — a panicking new image reverts to the previous slot on next reset. - Provisioning: flash the same firmware to every device. On first boot the screen shows its IP; register it from Scrypted via `POST /config`. - Viewport names must be unique across the LAN — mDNS hostnames are derived from `viewport` and two devices configured with the same name will collide. - NVS wipe: plug USB and run `idf.py erase-flash` followed by `idf.py flash`. The device boots clean and shows the info screen until `/config` is POSTed. @@ -441,6 +489,16 @@ idf.py build idf.py -p /dev/cu.usbmodem* flash monitor ``` +After the first USB flash, subsequent updates go over the LAN — no cable +needed: + +```sh +idf.py build +curl --data-binary @build/scrypted-viewport.bin \ + -H 'Content-Type: application/octet-stream' \ + http://<device-ip>/firmware +``` + ## Firmware implementation notes ### Source map (`main/`) |
