Over-the-air update

Firmware is updated remotely through three protocol commands and two status parameters. The mechanism is the same on every device; only the contents of the package differ.

ID

Ops

Meaning

51

W

Package name on the server (/repo/<name>)

55

E

Download the package (findylab.findyIoT.findyIoT.downloadUpdate())

52

E

Apply the downloaded package (findylab.findyIoT.findyIoT.update())

53

R

State: 0 idle, 1 downloading, 2 downloaded / ready, 3 updating

54

R

Result: 0 none, 1 updated successfully, 4 connection lost (partial download kept, resumable), 5 integrity check failed (download discarded), 6 unsupported package (compressed entries without deflate support), 7 invalid size / URI, 8 apply failed or the new firmware does not boot

Building a package

The package is a ZIP archive stored without compression. Its entries are copied onto the root of the device filesystem, so build it from a directory laid out exactly like the device:

cd package/                      # contains e.g. last.mpy
zip -0 -r -X ../fw2609061200.zip .

-0 disables DEFLATE (the device build has no deflate module), -X strips extra fields and streaming data descriptors. Upload the file to the server repository; the server refers to packages without the .zip suffix.

Warning

Keep firmware_core.mpy and boot.py out of routine packages. The recovery image base.py imports firmware_core - if the framework itself is broken by an update, nothing is left to receive the fix. If you must ship them, test the exact package on a bench device first.

Update flow

  1. The server writes the package name to 51 and sends command 55.

  2. downloadUpdate() sets 53 = 1, asks /repo/<name> for the total size and fetches /repo/<name>/1460/<n>/<IMEI> chunks of 1460 bytes into /update.zip (findylab.modem.eg915.downloadChunk(), up to three attempts per chunk). /update.meta pins the partial file to the package name, size and chunk size, so an interrupted download resumes from the last complete chunk on the next 55 instead of restarting. When the file is complete every ZIP entry’s CRC32 is verified (findylab.ota.ota_zip_ok()); only then 53 = 2, 54 = 0 and the command is acknowledged. The acknowledgement is processed with execute=True, so a *SET,52 queued on the server runs immediately.

  3. Command 52 - update() re-verifies the archive, sets 53 = 3, extracts it into the /next staging directory (findylab.ota.ota_extract(), CRC-checked file by file), writes the /update.commit marker, deletes the archive, acknowledges the command and resets.

  4. boot.py finds /update.commit before importing the application, copies /next onto the live filesystem, writes /update.applied, removes /next and finally the marker. The copy is idempotent - a power loss in the middle simply re-runs it on the next boot.

  5. If from last import fw succeeds, the first successful report calls fw._notifyUpdateResult(): 53 = 0, 54 = 1, marker removed. If the import fails, boot.py falls back to base.py, which reports 54 = 8, keeps the device reachable (one report every 300 s) and waits for a corrected package via the same 51 / 55 / 52 commands.

Files used on the device: /update.zip, /update.meta, /next/, /update.commit, /update.applied. All of them are transient.

Recovery image

base.py must stay tiny: it imports only firmware_core, registers the OTA parameters plus the identity/network ones, sends one samplings report per cycle (with 53/54 in its datalist so the server can drive the update from its normal telemetry view) and deep sleeps for 300 s. Never add application logic to it.