Publish flashable release artifacts - #13
Conversation
Releases published the application image only; the second-stage bootloader and the partition table stayed in the build directory. Commissioning a blank node therefore meant flashing stock WLED from install.wled.me and then pushing the RaceLink image through /update with "Ignore firmware validation" ticked -- a detour that routes the least experienced users through the most dangerous control in WLED's UI, because that checkbox disables all payload validation. Stage, per environment: the application image, the three pre-application images, and a merged factory image covering all four, plus a SHA-256 manifest and an assets.json sidecar giving environment, chip, device type, kind, flash offset, size and digest for every file. release_artifacts.py and release_staging.py are byte-identical to their RaceLink_Gateway counterparts. Both repositories publish the same asset shapes, and this is where a mistake produces an image that flashes cleanly and never boots, so there is one implementation of it rather than two. Offsets come from PlatformIO's metadata, the chip is read out of the image being merged, and the application offset comes from the generated partition table -- with a per-chip cross-check that aborts if the two sources disagree. That guard matters more here than in the gateway: the shipping set spans C3, S2 and S3, and the S2 bootloader sits at 0x1000 rather than 0x0. Metadata has to be collected inside the per-profile loop, while that profile's platformio_override.ini is still staged -- the next iteration overwrites it. Naming now states both versions, the RaceLink release and the WLED release it wraps, and the device type the master looks up: RaceLink_WLED-0.1.8-RaceLink_Node_v4_s3_llcc68-TYPE12-wled_v0.15.3-app.bin This also retires the old suffix handling, which joined Path.suffixes to preserve ".bin" but folded the whole WLED release name into the extension, because the upstream version contains dots. Nothing consumes these names: the host uploads whatever file the operator picks and WLED's /update ignores filenames. The build-and-stage loop moved into scripts/build_and_stage_profiles.sh so the new build.yml can rehearse the entire release on every pull request -- six profiles, staged, merged and verified, publishing nothing. Also records why RaceLink_Node_v7_classic_esp32_emac stays out of the shipping set: the omission was indistinguishable from an oversight.
`pio project metadata` cleans the build directory, so collecting it after `pio run` deleted the firmware.bin the staging step was about to read. The trigger is visible in the failing run: the metadata pass resolved and installed a dependency the build had not, which changes PlatformIO's project checksum and makes it wipe the build directory. Everything the metadata reports is derived from the configuration -- the flash offsets, and paths like $BUILD_DIR/bootloader.bin -- so the files it names do not have to exist yet, and collecting it first also means any dependency resolution happens before the build rather than invalidating it afterwards.
CI verificationThe first run failed and the failure was worth having: Fixed by collecting metadata before the build. Everything it reports is derived from the configuration — the offsets, and paths like Note for RaceLink_Gateway: its workflow has the same ordering and passes only because every dependency was already installed when the metadata pass ran. That is luck, not design, and should be reordered there too. Result — all six profiles, built against WLED
|
| Environment | Chip | DEV_TYPE | Factory image |
|---|---|---|---|
RaceLink_Node_v1_c3_ct62 |
esp32c3 | 10 | 1188576 B |
RaceLink_Node_v3_s2_llcc68 |
esp32s2 | 11 | 1233600 B |
RaceLink_Node_v3_s2_llcc68_epaper |
esp32s2 | 50 | 1266448 B |
RaceLink_Node_v4_s3_llcc68 |
esp32s3 | 12 | 1246192 B |
RaceLink_Node_v5_s3_eth |
esp32s3 | 13 | 1221504 B |
RaceLink_Node_v6_s3_heltec_wpaper |
esp32s3 | 51 | 1276896 B |
Each verified structurally: bootloader, partition table and application present at their declared offsets, and the embedded application byte-identical to the published application image. 32 assets staged in total; 12m22s.
The offset cross-check earns its keep
Read back from the staged manifest:
RaceLink_Node_v1_c3_ct62 esp32c3 bootloader=0x0000 partitions=0x8000 app=0x10000
RaceLink_Node_v3_s2_llcc68 esp32s2 bootloader=0x1000 partitions=0x8000 app=0x10000
RaceLink_Node_v3_s2_llcc68_epaper esp32s2 bootloader=0x1000 partitions=0x8000 app=0x10000
RaceLink_Node_v4_s3_llcc68 esp32s3 bootloader=0x0000 partitions=0x8000 app=0x10000
RaceLink_Node_v5_s3_eth esp32s3 bootloader=0x0000 partitions=0x8000 app=0x10000
RaceLink_Node_v6_s3_heltec_wpaper esp32s3 bootloader=0x0000 partitions=0x8000 app=0x10000
PlatformIO reports 0x1000 for the S2 targets and 0x0 for C3 and S3, and the per-chip table agrees in every case — so all three SoC families are now exercised rather than assumed. Hard-coding 0x0, which is what the gateway-only view would have suggested, would have produced two unbootable factory images.
Names in practice
RaceLink_WLED-0.0.0-ci-RaceLink_Node_v1_c3_ct62-TYPE10-wled_v16.0.1-app.bin
RaceLink_WLED-0.0.0-ci-RaceLink_Node_v1_c3_ct62-TYPE10-wled_v16.0.1-factory-usb-serial-only.bin
RaceLink_WLED-0.0.0-ci-RaceLink_Node_v1_c3_ct62-bootloader.bin
(0.0.0-ci is the rehearsal's placeholder version; a release uses the real one.)
Still not covered
No hardware flash test on a node. The build artifact holds factory images for all six profiles, so one can be commissioned from this PR directly.
Work packages B1 and B2 from
RaceLink_Docs/_private/plans/2026-07-30-release-factory-images-and-ota-preflight.md, matching what just landed in RaceLink_Gateway (PR #11 there).Problem
Releases published PlatformIO's application image only. Commissioning a blank node therefore meant flashing stock WLED from install.wled.me and then pushing the RaceLink image through
/updatewith Ignore firmware validation ticked — a detour that routes the least experienced users through the most dangerous control in WLED's UI, because that checkbox disables all payload validation.What ships now
Per environment: the application image, the three pre-application images (bootloader, partition table, boot_app0), and a merged factory image covering all four — plus a SHA-256 manifest and an
…-assets.jsonsidecar giving environment, chip, device type, kind, flash offset, size and digest for every file.Each asset now states both versions — its own RaceLink release and the WLED release it wraps — and the
DEV_TYPEthe master looks up rather than leaving it to be inferred.This also retires the old suffix handling.
"".join(Path.suffixes)was meant to preserve.binversus.bin.gz, butPath.suffixesreturns every dot-separated segment after the first dot, and the upstream version contains dots — soWLED_0.15.3_RaceLink_Node_V4_TYPE_12.binfolded the whole release name into the "extension". (.gzonly ever applied to ESP8266, which this repo does not build.) Nothing consumes these names:ota_service.pyuploads whatever file the operator picks, and WLED's/updateignores filenames.Shared with RaceLink_Gateway
scripts/release_artifacts.pyandscripts/release_staging.pyare byte-identical to their gateway counterparts. Both repositories publish the same asset shapes, and this is where a mistake produces an image that flashes cleanly and never boots — so there is one implementation rather than two.esptoolis read out of the ESP image header of the binary being merged.0x1000rather than0x0.Metadata is collected inside the per-profile loop, while that profile's
platformio_override.iniis still staged; the next iteration overwrites it.build.yml — compile-only CI
release.ymlwas the only thing that ever built this repo, and it tags and publishes in the same run. The build-and-stage loop moved intoscripts/build_and_stage_profiles.sh, which both workflows call — so the pull-request rehearsal and the real release cannot drift, and release day differs only in the version string.The new workflow builds all six shipping profiles against the latest published WLED release, verifies each factory image structurally, uploads the staged assets so a PR can be flashed on hardware, and publishes nothing.
B2 — v7 omission is now self-documenting
RaceLink_Node_v7_classic_esp32_emacstays out ofSHIPPING_PROFILE_FILENAMESuntil its Ethernet support ships. The comment records that, and notes it will be the first classic ESP32 in the set — the0x1000case the cross-check exists to catch.Verification
0x1000bootloader offset. That is the thing to check before merging.Not covered here
No hardware flash test on a node yet. The build artifact contains factory images for all six profiles, so a node can be commissioned from this PR directly.