feat: Add v0.3 OTA updates — dual partition, esp-ota tool, rollback

Dual OTA partition table (ota_0/ota_1, 1920 KB each) on 4MB flash.
Firmware gains OTA command, LED_OTA double-blink, version in STATUS,
and automatic rollback validation. Pi-side esp-ota tool serves firmware
via HTTP and orchestrates the update flow. esp-fleet gains ota subcommand.
This commit is contained in:
user
2026-02-04 16:19:09 +01:00
parent 44bd549761
commit d65ac208b9
8 changed files with 368 additions and 36 deletions

View File

@@ -2,7 +2,9 @@
"""Query all ESP32 CSI sensors in parallel."""
import concurrent.futures
import os
import socket
import subprocess
import sys
DEFAULT_PORT = 5501
@@ -14,6 +16,8 @@ SENSORS = [
("hollow-acorn", "hollow-acorn.local"),
]
ESP_OTA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "esp-ota")
USAGE = """\
Usage: esp-fleet <command> [args...]
@@ -25,11 +29,14 @@ Commands:
rate <10-100> Set ping rate on all devices
power <2-20> Set TX power on all devices
reboot Reboot all devices
ota [firmware.bin] OTA update all devices (sequentially)
Examples:
esp-fleet status
esp-fleet identify
esp-fleet rate 50"""
esp-fleet rate 50
esp-fleet ota
esp-fleet ota /path/to/firmware.bin"""
def query(name, host, cmd):
@@ -54,11 +61,33 @@ def query(name, host, cmd):
sock.close()
def run_ota(firmware=None):
"""Run OTA on each sensor sequentially."""
for name, host in SENSORS:
print(f"\n{'='*40}")
print(f"OTA: {name} ({host})")
print(f"{'='*40}")
cmd = [ESP_OTA, host]
if firmware:
cmd += ["-f", firmware]
result = subprocess.run(cmd)
if result.returncode != 0:
print(f"ERR: OTA failed for {name}, stopping fleet OTA", file=sys.stderr)
sys.exit(1)
print(f"\nAll {len(SENSORS)} devices updated.")
def main():
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
print(USAGE)
sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2)
# Handle OTA subcommand separately (sequential, not parallel)
if sys.argv[1].lower() == "ota":
firmware = sys.argv[2] if len(sys.argv) > 2 else None
run_ota(firmware)
return
cmd = " ".join(sys.argv[1:]).strip().upper()
with concurrent.futures.ThreadPoolExecutor(max_workers=len(SENSORS)) as pool: