Skip to content

Add factory binary generation for release builds#5712

Draft
netmindz wants to merge 1 commit into
wled:mainfrom
netmindz:factory-bin
Draft

Add factory binary generation for release builds#5712
netmindz wants to merge 1 commit into
wled:mainfrom
netmindz:factory-bin

Conversation

@netmindz

@netmindz netmindz commented Jun 30, 2026

Copy link
Copy Markdown
Member

Post-build script uses esptool merge_bin to produce a combined bootloader + partition table + firmware image (.factory.bin) alongside the existing OTA bin, enabling single-file flashing of blank devices. Only runs when platformio_release.ini is active (release builds).

Summary by CodeRabbit

  • New Features
    • Added automated creation of a factory firmware binary during the build process.
    • Release builds now use a clearer naming scheme for the generated output file.
  • Bug Fixes
    • Improved handling of build offsets and flash parameters to make packaged firmware generation more reliable.
    • Added safeguards so the factory binary step is skipped on unsupported targets.

Post-build script uses esptool merge_bin to produce a combined
bootloader + partition table + firmware image (.factory.bin) alongside
the existing OTA bin, enabling single-file flashing of blank devices.
Only runs when platformio_release.ini is active (release builds).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b75be17a-0795-4ed6-9c61-4d92fd652a07

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

return 0x10000

def create_factory_bin(source, target, env):
if env["PIOPLATFORM"] == "espressif8266":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to run this gate the other way around (ie. if env["PIOPLATFORM"] != "espressif32": return).

If we add support for any other platforms in the future, they'll almost certainly need a different approach.

Comment on lines +37 to +45
release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME")
if release_name_def:
release_name = release_name_def.replace("\\\"", "")
with open("package.json") as f:
version = json.load(f)["version"]
out_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.factory.bin")
else:
out_file = os.path.join(OUTPUT_DIR, "firmware", f"{env['PIOENV']}.factory.bin")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is duplicated from output_bins.py. Normally I'd suggest making a common function to compute the filename, but I've found it kind of a pain to do with platformio scripts - it is possible to make a shared import from the pio-scripts folder, but it's a bit cumbersome; you have to play some games with the import paths.

The technique I ended up using with usermods was to store the relevant value in the environment. (WLED_MODULES exists to pass the resolved usermod list from load_usermods.py to validate_usermods.py).

Probably the thing to do here is to have output_bins.py calculate the path at script execution time, and save it for usage in both action functions.

@softhack007

Copy link
Copy Markdown
Member

@softhack007

Copy link
Copy Markdown
Member

@coderabbitai please compare this PR to #5274. Are there things we can learn & improve when comparing both implementations?

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== PR 5712 (current) ==="
gh pr view 5712 --repo wled/WLED --json title,body,files -q '.title, .body' || true
echo "--- files ---"
gh pr view 5712 --repo wled/WLED --json files -q '.files[].path' || true
echo "--- diff ---"
gh pr diff 5712 --repo wled/WLED || true

echo "=== PR 5274 ==="
gh pr view 5274 --repo wled/WLED --json title,body,files -q '.title, .body' || true
echo "--- files ---"
gh pr view 5274 --repo wled/WLED --json files -q '.files[].path' || true
echo "--- diff ---"
gh pr diff 5274 --repo wled/WLED || true

Length of output: 23215


@softhack007 I compared the diffs of both PRs. Here's the breakdown:

Scope & integration

  • This PR (#5712) hooks factory_bin.py into [env] extra_scripts inside platformio_release.ini.template, so it runs automatically for every environment whenever a release build is triggered. It also explicitly bails out on espressif8266 (if env["PIOPLATFORM"] == "espressif8266": return).
  • #5274 ships two scripts (pos-esp32.py + copy_factory.py) wired only into a single sample env (esp32dev_factory) in platformio_override.sample.ini — it's opt-in per environment rather than a blanket release-build hook, and is ESP32-only by design (adapted from Tasmota/ESPEasy).

Partition offset parsing

  • Your _get_app_offset() wraps the whole CSV read in a broad try/except and silently falls back to 0x10000 on any failure — this can mask real parsing bugs (e.g. unexpected partition-table format) without any warning.
  • #5274 explicitly filters out comment/blank/malformed rows (row[0].strip().startswith("#"), len(row) < 5) and also extracts the spiffs/littlefs offset in addition to app0. Worth borrowing the explicit row-filtering and adding a printed warning when falling back to the default offset, so silent misconfigurations don't slip through.

Firmware-size safety check

  • #5274 raises a hard exception if the built firmware exceeds upload.maximum_size, failing the build fast.
  • This PR only prints a warning if merge_bin itself fails, but doesn't pre-validate firmware size against partition capacity — worth adding as a cheap safety net before invoking merge_bin.

Safeboot awareness

  • #5274 explicitly special-cases builds named safeboot to avoid merging the safeboot binary into the app0 offset (since safeboot expects a different layout/target). If WLED's release matrix includes a safeboot environment, this PR's create_factory_bin would merge it unconditionally — worth double-checking whether platformio_release.ini includes any safeboot envs and whether that's the desired behavior.

Filesystem bundling & flash-size auto-detection

  • #5274 goes further: it can build and bundle a LittleFS image (custom_files_upload), auto-detect actual flash size via esptool flash-id, and even binary-patch partitions.bin to resize the FS partition and recompute its MD5. This is powerful but riskier — it mutates build artifacts post-hoc and fetches files from arbitrary URLs during the build (a supply-chain consideration). Your PR sensibly avoids all of this by staying config-driven and read-only against the partition table, which fits its narrower, CI-focused goal better. I wouldn't recommend pulling this complexity into a release-focused script, but it's good context for why the two scripts look so different in size.

Output handling

  • This PR writes directly to the final build_output/... path in one step. #5274 writes to the build dir first, then a second copy_factory.py post-action relocates it. Your single-step approach is simpler and has fewer failure modes (no risk of stale files if the copy step is skipped).

Bottom line: the two PRs solve different problems — yours is a minimal, safe, CI-oriented factory-bin generator for release artifacts; #5274 is a more general-purpose, feature-rich combined-binary/filesystem tool meant for local custom builds. The concrete things worth adopting here are: explicit CSV row filtering (with a printed warning on fallback), and a fail-fast firmware-size check before calling merge_bin. Safeboot handling is worth verifying against WLED's actual release environments.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pio-scripts/factory_bin.py`:
- Around line 33-34: The factory_bin script is relying on PlatformIO internal
env hooks for flash settings, which ties it to unstable implementation details.
Update the logic in the factory-bin generation path to use the documented
BoardConfig accessors instead of env["__get_board_flash_mode"] and
env["__get_board_f_flash"], and keep the existing flash_mode/flash_freq handling
through the public API if those values are available there.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dea91443-73e7-4276-beda-ee78af415b99

📥 Commits

Reviewing files that changed from the base of the PR and between 32544a9 and a1a52b4.

📒 Files selected for processing (2)
  • .github/platformio_release.ini.template
  • pio-scripts/factory_bin.py

Comment on lines +33 to +34
flash_mode = env["__get_board_flash_mode"](env)
flash_freq = env["__get_board_f_flash"](env)

@coderabbitai coderabbitai Bot Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Are "__get_board_flash_mode" and "__get_board_f_flash" documented/stable PlatformIO SCons environment functions?

💡 Result:

The functions get_board_flash_mode and get_board_f_flash are not documented, stable, or public PlatformIO API functions [1][2]. They are internal helper functions defined within specific PlatformIO development platforms, most notably the espressif32 platform [3][4][5][6]. These functions are injected into the SCons construction environment (env) by platform-specific builder scripts to facilitate the dynamic resolution of flash configuration parameters (like flash mode and flash frequency) from board configuration files during the build process [4][5][6]. Because they are implementation details of the platform's build logic, they are subject to change without notice and are not intended for use in user-level advanced scripts [1][2]. Attempting to use these functions in your own scripts can lead to runtime errors, particularly if the expected board configuration keys are missing or if the platform architecture changes [7]. If you need to access board configuration data in your scripts, you should use the official, documented methods, such as accessing the board configuration object via env.BoardConfig [1][8].

Citations:


🏁 Script executed:

rg -n "__get_board_flash_mode|__get_board_f_flash|output_bins\.py|BoardConfig" .

Repository: wled/WLED

Length of output: 623


🏁 Script executed:

sed -n '1,220p' pio-scripts/output_bins.py

Repository: wled/WLED

Length of output: 2522


🏁 Script executed:

rg -n "upload\.flash_mode|upload\.flash_freq|build\.flash_mode|build\.flash_freq|flash_mode|flash_freq" pio-scripts platformio.ini .

Repository: wled/WLED

Length of output: 3254


🏁 Script executed:

sed -n '1,160p' pio-scripts/factory_bin.py

Repository: wled/WLED

Length of output: 2467


🏁 Script executed:

rg -n 'BoardConfig\(\)\.get\("build\.flash_mode"|BoardConfig\(\)\.get\("build\.f_flash"|BoardConfig\(\)\.get\("upload\.flash_mode"|BoardConfig\(\)\.get\("upload\.flash_freq"' .

Repository: wled/WLED

Length of output: 147


Avoid PlatformIO internal env hooks here. env["__get_board_flash_mode"] and env["__get_board_f_flash"] are PlatformIO internals, not public scripting API, so this script is coupled to implementation details that can change on core/platform upgrades. Prefer the documented BoardConfig path if it exposes the needed flash settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pio-scripts/factory_bin.py` around lines 33 - 34, The factory_bin script is
relying on PlatformIO internal env hooks for flash settings, which ties it to
unstable implementation details. Update the logic in the factory-bin generation
path to use the documented BoardConfig accessors instead of
env["__get_board_flash_mode"] and env["__get_board_f_flash"], and keep the
existing flash_mode/flash_freq handling through the public API if those values
are available there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these hooks available in pioarduino and newer tasmota framework releases, as we use them in our V5 branch?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🤖 web_search executed:

Does pioarduino platform-espressif32 builder/main.py define get_board_flash_mode and get_board_f_flash functions in recent releases (2024/2025)?

Length of output: 13446


@softhack007 Good news — I checked the pioarduino fork directly, and yes, both hooks are present there too.

In pioarduino/platform-espressif32's builder/main.py, the platform defines internal helpers _get_board_flash_mode(env) and _get_board_f_flash(env), and then injects them into the SCons environment via:

env.Replace(
    __get_board_boot_mode=_get_board_boot_mode,
    __get_board_f_flash=_get_board_f_flash,
    __get_board_f_image=_get_board_f_image,
    __get_board_f_boot=_get_board_f_boot,
    __get_board_flash_mode=_get_board_flash_mode,
    __get_board_memory_type=_get_board_memory_type,
    ...
)

This is consistent across recent pioarduino commits I looked at (main, 879780c5, a9fac073), and the same pattern exists in the upstream platformio/platform-espressif32 develop branch as well. So env["__get_board_flash_mode"] and env["__get_board_f_flash"] should work fine on the pioarduino/tasmota-framework builds used in your V5 branch.

That said, my original caution still stands as a general principle: these are prefixed with double-underscore and are implementation details of the builder script (not part of PlatformIO's documented public scripting API per env.BoardConfig() docs). They happen to be consistently maintained across both the official and pioarduino forks today, but a future refactor of builder/main.py in either fork could rename/remove them without a deprecation notice, since they're not a versioned/stable contract. Given they're currently reliable in practice and already used elsewhere in your codebase (output_bins.py), I don't think this needs to block the PR — just something to keep in mind if a platform upgrade ever breaks the factory-bin script.

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.

3 participants