Skip to content

espressif: freeze the env-collector's libraries on the 13 boards it targets - #30

Open
tyeth wants to merge 3 commits into
wifi-ap-debugfrom
espressif-freeze-envhub-modules
Open

espressif: freeze the env-collector's libraries on the 13 boards it targets#30
tyeth wants to merge 3 commits into
wifi-ap-debugfrom
espressif-freeze-envhub-modules

Conversation

@tyeth

@tyeth tyeth commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Based on wifi-ap-debug — the branch the 13 custom builds actually used (d802e5f1b, which is the 10.4.0-alpha.1-9-gd802e5f1b the bench board runs), not main.

The ESP-NOW / WiFi / BLE environmental collector ships ~20 libraries in lib/. On a board without PSRAM that is the difference between running and not.

Measured on a Feather ESP32-S3 No PSRAM (2026-09-13), with the application itself already cross-compiled to .mpy:

  • BLE + ESP-NOW + softAP all come up — and then import datastore dies with MemoryError: memory allocation failed, allocating 158 bytes;
  • with BLE off it reaches the eInk dashboard, and the SD mount fails for want of 832 bytes, with 15.9 KB free.

Cross-compiling the application removes the on-device compiler peak — which is what made the C6 unbootable — but not the resident bytecode. Freezing does: a frozen module executes in place from flash and never occupies heap.

What this does

Freezes 22 libraries on the 13 espressif boards this project targets. Nine were not yet in frozen/ and are added as submodules: IL0373, JD79667, MAX1704x, NTP, SCD4X, SCD30, SEN6x, PCF8523, DS1307. The other thirteen were already present.

Why the 22 lines are repeated in every board file

Not an aesthetic choice, and worth reading before tidying it away.

tools/ci_fetch_deps.py reads each board's mpconfigboard.mk line by line, looking for literal FROZEN_MPY_DIRS += $(TOP)/ prefixes, to decide which submodules to check out. It does not follow include.

I tried the DRY version first — one shared ports/espressif/envhub_frozen.mk and a single include line per board. It resolves correctly under make (the build's own FREEZE step listed all 22 directories), and then CI fails at MKMANIFEST with every frozen directory empty, because the submodules were never fetched:

FREEZE ../../frozen/Adafruit_CircuitPython_BLE ... (all 22)
MKMANIFEST ../../frozen/Adafruit_CircuitPython_BLE ... (all 22)
make: *** [../../py/circuitpy_mpconfig.mk:855: build-.../manifest.py] Error 1

So each copy of the block carries a comment explaining why it is duplicated.

Deliberately not frozen

  • adafruit_pixelbuf — already a built-in C module, not a library.
  • sensirion_i2c_driver / sensirion_i2c_sen5x — from a third-party circup bundle rather than a library repo, and node_sensors.py carries a minimal built-in SEN5x driver for boards without them.
  • PCF85063A — no Adafruit library exists; the project drives that chip directly from its own extrtc.py, validated against real hardware. (frozen/circuitpython-pcf85063a already exists in this tree from another author; unused here.)

Size — what CI has to answer

~162 KB of frozen bytecode from 621 KB of source, uniform across all 13:

BLE 30.6 KB
HTTPServer 32.3 KB
Display_Text 19.9 KB
Bitmap_Font 11.6 KB
Register 11.1 KB
SEN6x 9.3 KB
everything else ~47 KB

Comfortable on the 8 MB and 16 MB boards. The 4 MB ones are the risk — adafruit_qtpy_esp32s3_4mbflash_2mbpsram, adafruit_feather_huzzah32, adafruit_feather_esp32s3_tft, adafruit_feather_esp32s3_reverse_tft, adafruit_feather_esp32s3_4mbflash_2mbpsram, adafruit_feather_esp32c6_4mbflash_nopsram. If one overflows, the fix is to split the list per board rather than shrink it everywhere.

Verified on hardware

Flashed to a Feather ESP32-S3 No PSRAM running this project (2026-09-13). Same board, same application, display enabled:

before freezing with frozen libraries
dashboard built lite, mem 15936 lite, mem 40256
SD mount failedMemoryError, 832 bytes SD mounted
fuel gauge died in battery.py battery source: max17048
outcome never reached the main loop collector running / dashboard refreshed (mem 23200)

adafruit_ble costs 5,728 bytes to import frozen, against 30.6 KB of bytecode.

Firmware size on that board: 1,884,304 -> 2,050,112 bytes used of 2,097,152 (+165,808, matching the 162 KB estimate), leaving 47,040 free. It fits, but it is the tightest of the 13 -- an 8 MB board with a 2 MB firmware partition, not one of the 4 MB boards I flagged. feather_esp32s3_4mbflash_2mbpsram has a 2816 kB partition and over 1 MB spare.

Correction to an earlier version of this description: I claimed /lib is searched before .frozen, so a stale copy in lib/ would shadow the frozen one. That is wrong. sys.path on these builds is ['', '/', '.frozen', '/lib'] -- .frozen comes first, and the measurement above was taken with lib/ still fully populated. Deleting lib/ frees flash and removes ambiguity; it is not required to get the RAM back.

🤖 Generated with Claude Code

…argets

The ESP-NOW / WiFi / BLE environmental collector ships ~20 libraries in
lib/. On a board without PSRAM that is the difference between running and
not. Measured on a Feather ESP32-S3 No PSRAM (2026-09-13), with the
application itself already cross-compiled to .mpy:

  * BLE + ESP-NOW + softAP all come up, and then `import datastore` dies
    with `MemoryError: memory allocation failed, allocating 158 bytes`;
  * with BLE off it reaches the eInk dashboard and the SD mount fails for
    want of 832 bytes, with 15.9 KB free.

Cross-compiling the application removes the on-device *compiler* peak --
which is what made the C6 unbootable -- but not the resident bytecode.
Freezing does: a frozen module executes in place from flash and never
occupies heap.

Twenty-two libraries, on the 13 espressif boards this project targets --
the same 13 the custom-build workflow has been dispatched for, and on the
branch those builds actually used.

Nine were not yet in frozen/ and are added as submodules: IL0373, JD79667,
MAX1704x, NTP, SCD4X, SCD30, SEN6x, PCF8523, DS1307. The other thirteen
were already there.

The 22 lines are repeated verbatim in each board's mpconfigboard.mk rather
than factored into one include. That is not an aesthetic choice: tools/
ci_fetch_deps.py reads each board file line by line looking for
"FROZEN_MPY_DIRS += $(TOP)/" to decide which submodules to check out, and
it does not follow includes. The include version builds fine locally,
FREEZE lists all 22 dirs, and then CI fails at MKMANIFEST with every
frozen directory empty. Each copy of the block carries a comment saying so
before someone tidies it back.

Deliberately not frozen:
  * adafruit_pixelbuf -- already a built-in C module.
  * sensirion_i2c_driver / sensirion_i2c_sen5x -- a third-party circup
    bundle rather than a library repo, and node_sensors.py carries a
    minimal built-in SEN5x driver for boards without them.
  * PCF85063A -- no Adafruit library exists; the project drives that chip
    directly from its own extrtc.py.

Size: ~162 KB of frozen bytecode from 621 KB of source, uniform across all
13. Comfortable on the 8 MB and 16 MB boards; the 4 MB ones are what CI
has to answer, and if one overflows the fix is to split the list rather
than shrink it everywhere.

NOTE for whoever deploys against these builds: sys.path puts /lib ahead of
.frozen, so a copy left in lib/ SHADOWS the frozen one and the board pays
the RAM anyway. Emptying lib/ is what collects the benefit -- the same
trap as leaving a .py beside a .mpy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tyeth
tyeth force-pushed the espressif-freeze-envhub-modules branch from caaabf6 to 53cb99e Compare September 13, 2026 21:19
@tyeth
tyeth changed the base branch from main to wifi-ap-debug September 13, 2026 21:19
tyeth and others added 2 commits September 13, 2026 22:34
build-board-custom.yml calls .github/actions/deps/submodules without
version: true, so steps.set-up-submodules.outputs.version is empty. For a
board with frozen modules the mpy_cross action then runs make with
CP_VERSION="" in its environment, and get_version_info_from_git() took the
presence of the variable as authoritative, returned an empty tag, and
makeversionhdr.py aborted with "Cannot determine version". Boards without
frozen modules never run that step, which is why the Pico 2 W asset build
passed while the Pico W one failed twice on the same commit.

Fall back to git describe when CP_VERSION is present but empty.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hub serves its portal over a Nordic UART service and the nodes take
their config the same way, so _bleio is not optional for this project on
the boards it is aimed at.

Reading ports/espressif/mpconfigport.mk, it should already be on:
CIRCUITPY_BLEIO_NATIVE defaults to 1 and is forced off only for the S2 and
the P4 (no BLE hardware) and for 2MB flash -- not 4MB, which only loses
dualbank and gains -Os. So on these four boards this is a no-op that says
so out loud, and it stops a future flash-size trim taking BLE away
silently the way the 2MB block already does.

Boards: feather_esp32s3_4mbflash_2mbpsram, qtpy_esp32s3_4mbflash_2mbpsram,
feather_esp32s3_tft, feather_esp32s3_reverse_tft.

Size is not the constraint here, contrary to what a 4MB board suggests:
the last green build of feather_esp32s3_4mbflash_2mbpsram used 1,841,040
of 2,883,584 bytes of firmware space, leaving 1,042,544 free -- room for
this project's ~162 KB of frozen bytecode several times over.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant