From 0c4e1f92fa2785188c610bea095d8a49276157ee Mon Sep 17 00:00:00 2001 From: Elliot Matson Date: Mon, 5 Oct 2026 07:55:26 -0500 Subject: [PATCH] Keep the image small enough for cubes on the old partition table A cube still on v0.6.0-b1 downloaded v0.6.1 to 99% and failed with ESP_ERR_INVALID_SIZE, through the GitHub updater and the upload card alike. It has Arduino's min_spiffs.csv partition table, whose app slots are 0x1E0000 bytes; partition tables only change over USB, so it kept that layout through every OTA update. b1 fit with 57 KB to spare; telemetry and lost mode took v0.6.0 to 1,983,728 bytes, 17 KB over. Reproduced on a cube flashed with that table: "GitHub update failed (1966080 of 1983728 bytes): ESP_ERR_INVALID_SIZE". Silent assertions drop each assert's file name and expression strings, about 59 KB, with no cost in speed. scripts/size_guard.py fails the build if the image outgrows the old slot again. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 6 ++++++ platformio.ini | 1 + scripts/size_guard.py | 34 ++++++++++++++++++++++++++++++++++ sdkconfig.defaults | 6 +++++- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 scripts/size_guard.py diff --git a/CLAUDE.md b/CLAUDE.md index 9724b0a..6374522 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -152,6 +152,12 @@ scripts/capture_patterns.py --host cube.local ` (frames come from watchdog's backtrace, printed mid-write, panicked the cube ("Cache disabled but cached memory region accessed", reset reason "interrupt watchdog"). Sleep a minimum each pass, as `Updates::animationLoop` and `Renderer::loop` do. +- **The image must fit 0x1E0000 bytes.** Some cubes still have Arduino's old + `min_spiffs.csv` partition table (1,966,080-byte app slots), and a partition + table only changes over USB. A bigger image fails every update path at 99% + with `ESP_ERR_INVALID_SIZE`; v0.6.0 was 17 KB over. `scripts/size_guard.py` + fails the build first. Silent assertions buy ~59 KB; `-Os` would buy more + but slows rendering. - **Never flash `firmware.factory.bin` to a cube that is set up.** The merged image's padding covers NVS (0x9000-0xDFFF) and wipes WiFi, settings and the Spotify login. Update with the upload card or ArduinoOTA; over USB write only diff --git a/platformio.ini b/platformio.ini index 02abb21..d3c94b1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,6 +28,7 @@ upload_port = cube.local ; copy in hub and lp-p2p. extra_scripts = scripts/idf_scripts.py + scripts/size_guard.py ; arduino-esp32's component manifest pulls in RainMaker, Insights, Zigbee, ; speech recognition, Modbus and more, whether or not the sketch uses them. ; The cube uses none of them. Removing them shortens the build and drops the diff --git a/scripts/size_guard.py b/scripts/size_guard.py new file mode 100644 index 0000000..5188c36 --- /dev/null +++ b/scripts/size_guard.py @@ -0,0 +1,34 @@ +"""Fails the build when firmware.bin would not fit an older cube's app slot. + +Some cubes in the field still have the partition table from before +min_spiffs_s3.csv: Arduino's min_spiffs.csv, whose app slots are 0x1E0000 +bytes. A partition table only changes over USB, so those cubes keep it through +every OTA update, and an image larger than the slot fails at 99% with +ESP_ERR_INVALID_SIZE -- the upload card, ArduinoOTA and the GitHub updater +alike. v0.6.0 was 17 KB over and could not reach them. + +Keep this until no cube with the old table is left. +""" + +Import("env") + +import os + +OLD_APP_SLOT = 0x1E0000 # app0/app1 in min_spiffs.csv + + +def check_size(source, target, env): + path = target[0].get_abspath() + size = os.path.getsize(path) + margin = OLD_APP_SLOT - size + if margin < 0: + print( + "\n*** %s is %d bytes, %d over the %d-byte app slot that cubes with the " + "old min_spiffs.csv partition table have. They could not install it. " + "See scripts/size_guard.py.\n" % (os.path.basename(path), size, -margin, OLD_APP_SLOT) + ) + env.Exit(1) + print("Old-layout app slot: %d bytes free (image %d of %d)" % (margin, size, OLD_APP_SLOT)) + + +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_size) diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 697cbe7..f08403f 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -30,8 +30,12 @@ CONFIG_ARDUINO_RUN_NO_AFFINITY=y CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_INFO=y # Optimize for speed. The IDF default (-Og) is for debugging; the render loop's -# per-pixel work runs measurably faster at -O2. Flash has plenty of room. +# per-pixel work runs measurably faster at -O2. CONFIG_COMPILER_OPTIMIZATION_PERF=y +# Assertions still stop the cube, but without compiling in each one's file +# name and expression: about 59 KB, which keeps the image inside the 0x1E0000 +# app slots of cubes still on the old partition table (scripts/size_guard.py). +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y # CPU, RTOS, console CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y