Show how to drive OTA from a terminal

d76b734 · Gabriel Morell · 2026-09-16 19:25

1 files +74 -4
Message
{commit_body(@commit)}

Files changed

modified README.md
+74 −4
@@ -147,14 +147,84 @@ https://gg.neiam.co/pkg/raw/neiam/lambent-flex-c6/manifest.json
147 147
148 148 Every 6 hours for a CI build; **a locally flashed one never polls on its own**,
149 149 so `cargo run --release` is not quietly undone a minute later by the forge's
150 −build. `POST /ota` forces a check either way. `GET /`
151 −reports `version`, `ota_slot`, `ota_state`, and whatever the last check found.
150 +build.
151 +
152 +### Driving it by hand
153 +
154 +`GET /` says what a box is running — note it answers **218**, not 200, which
155 +the packet sender matches on — and `POST /ota` makes it go and look now rather
156 +than at the next interval:
157 +
158 +```console
159 +$ curl -s http://172.16.15.139/ | jq '{version, ota_slot, ota_state, ota_available}'
160 +{
161 + "version": "0.1.0",
162 + "ota_slot": "ota_0",
163 + "ota_state": "unknown",
164 + "ota_available": null
165 +}
166 +
167 +$ curl -s -i -X POST http://172.16.15.139/ota | head -1
168 +HTTP/1.1 202 Accepted
169 +
170 +$ curl -s -X POST http://172.16.15.139/ota | jq
171 +{
172 + "queued": true,
173 + "running": "0.1.0",
174 + "available": null,
175 + "last_error": null,
176 + "checked": false
177 +}
178 +```
179 +
180 +**202 means queued, not done.** The check and the download run on the OTA
181 +thread, so the response says nothing about the outcome — and a successful
182 +update ends in a reboot, so the device stops answering for a few seconds.
183 +Ask again once it is back:
184 +
185 +```console
186 +$ curl -s http://172.16.15.139/ | jq '{version, ota_slot, ota_state}'
187 +{
188 + "version": "0.1.0+ci20260916.g2131e3c",
189 + "ota_slot": "ota_1",
190 + "ota_state": "valid"
191 +}
192 +```
193 +
194 +It moved slots, and `valid` means this image reached the point of serving HTTP
195 +and cancelled its own rollback. A failed check leaves the device exactly where
196 +it was and says why:
152 197
153 198 ```console
154 −$ curl -X POST http://<device-ip>/ota
155 −{"queued":true,"running":"0.1.0+ci20260916.g1a2b3c4","available":null,…}
199 +$ curl -s http://172.16.15.139/ | jq '{ota_available, ota_error}'
200 +{
201 + "ota_available": null,
202 + "ota_error": "fetching https://gg.neiam.co/pkg/raw/neiam/lambent-flex-c6/manifest.json: HTTP 503"
203 +}
204 +```
205 +
206 +`ota_state` reads `unknown` on a board flashed over USB, which is not a fault:
207 +`--erase-parts otadata` leaves no state entry to read. It becomes `valid` after
208 +the first over-the-air install.
209 +
210 +### A fleet
211 +
212 +```sh
213 +# what is everyone running?
214 +for ip in 172.16.15.{139,140,141}; do
215 + printf '%-16s %s\n' "$ip" "$(curl -s --max-time 5 "http://$ip/" | jq -r '.version // "unreachable"')"
216 +done
217 +
218 +# everyone, go and look now
219 +for ip in 172.16.15.{139,140,141}; do
220 + curl -s --max-time 5 -X POST "http://$ip/ota" > /dev/null &
221 +done; wait
156 222 ```
157 223
224 +Nothing needs coordinating: each device downloads for itself, verifies the
225 +SHA-256 the manifest gave it, and reboots when it is ready. One that is offline
226 +or mid-frame simply picks the update up at its next check.
227 +
158 228 **A device flashed before OTA existed needs one USB flash to migrate** onto the
159 229 two-slot partition table — the app slot moved and there is no otadata. NVS
160 230 keeps its offset and size, so credentials and pins survive; do *not* erase it.

Parents: b76c527