Install downloaded firmware during OTA update
check_for_update() wrote the download to main_ota_temp.py, stamped the new version in NVS and reset, but never replaced main.py. Devices therefore kept running the old program while reporting the newest version, so no OTA release ever reached the field. The routine now validates the download (HTTP 200, minimum size, setup/loop present, compiles), keeps the running program as main_prev.py for manual rollback, renames the download to main.py, and records the version only after the swap succeeds. A rejected download or failed swap leaves the old program and version in place so the next boot retries. CLAUDE.md describes the new flow and the field caveat: devices on the old build need one manual USB copy first, and the next version.txt must exceed any number already published because their NVS may claim a version they never ran. .gitignore drops the __pycache__ left by PC-side syntax checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>main
parent
7050a7ef51
commit
4d487d3c58
|
|
@ -0,0 +1,3 @@
|
|||
# Byte-code from PC-side syntax checks (python -m py_compile); nothing here runs on the PC
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
@ -12,10 +12,10 @@ The server that talks to this device is the UiMetrix ASP.NET Core app in `D:\Pro
|
|||
|
||||
## Releasing to devices (OTA)
|
||||
|
||||
Devices update themselves at boot from the GitHub repo `husbananjum/m5core-s3-UIMETRIX-Z87` (`main` branch): `check_for_update()` fetches `version.txt`, compares it as a float against the `fw_version` string in NVS namespace `storage` (default `"1.0"`), and if newer downloads that repo's `main.py` to `/flash/main_ota_temp.py`, stores the new version in NVS and resets. A release is therefore: push the new `main.py` and bump `version.txt`. Two things to keep in mind:
|
||||
Devices update themselves at boot from the GitHub repo `husbananjum/m5core-s3-UIMETRIX-Z87` (`main` branch): `check_for_update()` fetches `version.txt`, compares it as a float against the `fw_version` string in NVS namespace `storage` (default `"1.0"`), and if newer downloads that repo's `main.py` and hands it to `install_update()`. The download must be HTTP 200, at least 1000 bytes, contain `def setup` and `def loop`, and compile; otherwise it is rejected. Installation writes it to `/flash/main_ota_temp.py`, renames the running program to `/flash/main_prev.py` (a rollback copy for manual recovery over USB), renames the download to `/flash/main.py`, and only then stores the new version in NVS and resets. A rejected download or a failed swap leaves the old program and the old version in place, so the next boot retries. A release is therefore: push the new `main.py` and bump `version.txt`. Things to keep in mind:
|
||||
|
||||
- Versions compare as floats, so `1.10` is older than `1.9`.
|
||||
- Nothing in this file copies `main_ota_temp.py` over `main.py`; that promotion has to happen elsewhere on the device (e.g. its `boot.py`). Confirm it exists before relying on OTA, because the new version is stamped in NVS before the reset either way, after which the device reports "Already latest version".
|
||||
- Builds before 2026-09-11 downloaded the file but never installed it, while still stamping the new version into NVS. Devices running such a build never pick up an OTA release: they need one manual USB copy of the fixed `main.py` first, and because their NVS may already claim a version they never ran, the next `version.txt` must be higher than any number published so far.
|
||||
|
||||
## How the program works
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os, sys, io
|
||||
import gc
|
||||
import M5
|
||||
from M5 import *
|
||||
import network
|
||||
|
|
@ -63,40 +64,96 @@ def set_local_version(version):
|
|||
|
||||
local_version = get_local_version()
|
||||
|
||||
def _is_valid_program(code):
|
||||
"""Reject error pages, truncated downloads and anything that does not compile."""
|
||||
if not code or len(code) < 1000:
|
||||
return False
|
||||
if "def setup" not in code or "def loop" not in code:
|
||||
return False
|
||||
try:
|
||||
compile(code, "main.py", "exec")
|
||||
except SyntaxError:
|
||||
return False
|
||||
except MemoryError:
|
||||
print("Not enough memory to verify the update")
|
||||
return False
|
||||
return True
|
||||
|
||||
def install_update(new_code, remote_version):
|
||||
"""Write the new program to flash, swap it in as main.py, then record the version."""
|
||||
with open(OTA_TEMP_PATH, "w") as f:
|
||||
f.write(new_code)
|
||||
# Keep the current program as a rollback copy (restore it over USB if the new one misbehaves).
|
||||
try:
|
||||
os.remove(OTA_BACKUP_PATH)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.rename(OTA_MAIN_PATH, OTA_BACKUP_PATH)
|
||||
except OSError:
|
||||
pass # no main.py yet
|
||||
try:
|
||||
os.rename(OTA_TEMP_PATH, OTA_MAIN_PATH)
|
||||
except OSError:
|
||||
# Put the old program back so the device still boots. The old version stays
|
||||
# in NVS, so the update is retried on the next boot.
|
||||
try:
|
||||
os.rename(OTA_BACKUP_PATH, OTA_MAIN_PATH)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
set_local_version(remote_version)
|
||||
|
||||
def check_for_update():
|
||||
try:
|
||||
print("Checking for update...")
|
||||
|
||||
r = requests.get(UPDATE_VERSION_URL)
|
||||
try:
|
||||
status = getattr(r, "status_code", 200)
|
||||
if status != 200:
|
||||
print("Version check failed: HTTP", status)
|
||||
return
|
||||
remote_version = r.text.strip()
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
print("Remote:", remote_version)
|
||||
print("Local :", local_version)
|
||||
|
||||
if float(remote_version) > float(local_version):
|
||||
print("New version found. Updating...")
|
||||
if float(remote_version) <= float(local_version):
|
||||
print("Already latest version")
|
||||
return
|
||||
|
||||
print("New version found. Downloading...")
|
||||
gc.collect()
|
||||
r = requests.get(UPDATE_FILE_URL)
|
||||
try:
|
||||
status = getattr(r, "status_code", 200)
|
||||
if status != 200:
|
||||
print("Download failed: HTTP", status)
|
||||
return
|
||||
new_code = r.text
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
with open("/flash/main_ota_temp.py", "w") as f:
|
||||
f.write(new_code)
|
||||
if not _is_valid_program(new_code):
|
||||
print("Update rejected: downloaded file is not a valid program")
|
||||
return
|
||||
|
||||
print("Update downloaded. Restarting...")
|
||||
set_local_version(remote_version)
|
||||
install_update(new_code, remote_version)
|
||||
print("Update installed. Restarting...")
|
||||
machine.reset()
|
||||
|
||||
else:
|
||||
print("Already latest version")
|
||||
|
||||
except Exception as e:
|
||||
print("Update check failed:", e)
|
||||
|
||||
|
||||
UPDATE_VERSION_URL = "https://raw.githubusercontent.com/husbananjum/m5core-s3-UIMETRIX-Z87/refs/heads/main/version.txt"
|
||||
UPDATE_FILE_URL = "https://raw.githubusercontent.com/husbananjum/m5core-s3-UIMETRIX-Z87/refs/heads/main/main.py"
|
||||
OTA_MAIN_PATH = "/flash/main.py" # the program the device runs at boot
|
||||
OTA_TEMP_PATH = "/flash/main_ota_temp.py" # a download lands here first
|
||||
OTA_BACKUP_PATH = "/flash/main_prev.py" # the previous program, kept for manual rollback
|
||||
|
||||
|
||||
MQTT_CLIENT_ID = serial
|
||||
|
|
|
|||
Loading…
Reference in New Issue