Document ESP32 partition migration in picker and catalogs

This commit is contained in:
mikecarper
2026-09-27 12:53:13 -07:00
parent 0d9732f2f2
commit dbf387b681
9 changed files with 1488 additions and 1386 deletions
+29
View File
@@ -1043,6 +1043,23 @@
}, 0) >= 0;
}
function isExpandedEsp32Infrastructure(profile) {
return profile && profile.controls &&
profile.controls.platform === "ESP32_PLATFORM" &&
["repeater", "room", "sensor"].includes(profile.role) &&
/-full-(?:logging|usb-wifi)$/i.test(profile.target);
}
function migrationReleaseUrl(profile, asset) {
if (!isExpandedEsp32Infrastructure(profile) ||
!profile.releaseFamily || !asset || !asset.releaseUrl) return "";
const url = asset.releaseUrl.replace(
/\/releases\/tag\/[^/]+$/,
"/releases/tag/utility-" + encodeURIComponent(profile.releaseFamily)
);
return url !== asset.releaseUrl && isSafeGithubUrl(url) ? url : "";
}
function installSteps(profile, kind) {
const common = [
"Verify that the hardware name and every displayed variant match the physical board.",
@@ -1087,6 +1104,11 @@
],
};
const extra = [];
if (isExpandedEsp32Infrastructure(profile) && kind === "bin") {
extra.push(
"If the node still has a smaller layout, first flash the matching merged image over USB or, if listed, use the exact board/role migration ZIP from the utility release and follow its README. Never send this app-only image across a partition change."
);
}
if (profile.variant.includes("w25q16")) {
extra.push(
"Requires the external W25Q16 storage board, its exact documented wiring, and the matching storage-aware OTAFIX bootloader."
@@ -1422,6 +1444,12 @@
const release = createElement("a", "Open release notes");
release.href = asset.releaseUrl;
actions.appendChild(release);
const migrationUrl = migrationReleaseUrl(profile, asset);
if (migrationUrl) {
const migration = createElement("a", "Check exact ESP32 migration ZIP");
migration.href = migrationUrl;
actions.appendChild(migration);
}
card.appendChild(actions);
const steps = createElement("div");
@@ -1884,6 +1912,7 @@
runtimeDirections: runtimeDirections,
renderRuntimeDirections: renderRuntimeDirections,
installSteps: installSteps,
migrationReleaseUrl: migrationReleaseUrl,
humanizeHardware: humanizeHardware,
humanizeVariant: humanizeVariant,
labelFor: labelFor,
+27 -14
View File
@@ -27,6 +27,13 @@ from the published firmware assets.
<strong>Full install / layout migration (merged .bin)</strong>. Narrower profiles
remain available when their reduced transport or feature set is intentional.
</p>
<p>
Upgrading an existing ESP32 infrastructure node from a smaller partition
layout? The selected Full result links to the utility release. If an
exact board and role migration ZIP is listed, follow its included README
for a supported Wi-Fi or LoRa route. USB installation uses the matching
merged image instead. An app-only .bin cannot change the partition table.
</p>
</div>
<p class="firmware-picker-order-note">
@@ -229,9 +236,12 @@ and CLI. Standard profiles remain useful for boards without a FULL build, for
an intentionally narrower transport, or when retaining an existing compatible
partition layout.
Changing between standard and FULL ESP32 layouts requires the exact-board
merged image over USB. A running application cannot safely move its own active
and inactive partitions.
Changing between standard and FULL ESP32 layouts requires a partition migration.
Use the exact-board merged image over USB, or a supported exact board and role
staged migration ZIP from the [utility release](https://github.com/mikecarper/MeshCore/releases/tag/utility-v1.17.1.7-halo-keymind-cascade-dev-2d03e098).
Read that ZIP's README before choosing the Wi-Fi or LoRa route. Do not send a
loose app-only Full `.bin` directly to an older layout: the running application
cannot move its own active and inactive partitions.
Current `full-usb-wifi` profiles use one binary for no external output, USB
packet logging/USB-connected MQTT, direct WiFi MQTT, or both. The picker shows
@@ -353,12 +363,16 @@ See the [two-profile setup guide](radio_profiles.md#companion-messages-on-both-p
| --- | --- |
| <code>-merged.bin</code> | Erase/fresh install, recovery, role migration, or partition-profile change on ESP32 over USB |
| Non-merged <code>.bin</code> | Update an existing same-board, same-role, same-partition installation |
| <code>.zip</code> | Native nRF52 Serial DFU update package; it is not an extra archive |
| <code>*-migration.zip</code> | Exact ESP32 board and role staged partition migration; follow its included README for Wi-Fi or LoRa steps |
| Other <code>.zip</code> firmware shown by the picker | Native nRF52 Serial DFU update package; it is not an extra archive |
| <code>.uf2</code> | UF2 bootloader drag-and-drop install or update |
| <code>.hex</code> | Erase/recovery flash with a supported wired programmer |
Never send a merged ESP32 image through browser OTA or LoRa OTA. Back up the
node configuration and verify every filename suffix before flashing.
node identity, configuration, keys, and radio settings, and verify every
filename suffix before flashing. A staged migration writes the partition table;
power loss during that write can require cable recovery. The package README
states what each route preserves and which older layouts it accepts.
## LoRa OTA and OTAFIX
@@ -420,15 +434,14 @@ search for deployed devices that still use that mOTA identity; it is not shown
as a second recommended configuration.
Some normal ESP32 roles begin in the legacy 1.25 MiB dual-OTA layout but have
a canonical Full image with a larger partition table. Their Full image keeps
the ordinary target identity for future mOTA updates, but the first install is
always its exact `-merged.bin` over USB: LoRa mOTA intentionally rejects a
package whose partition signature differs from the running layout. During this
transition the portable artifact remains published for already-installed
nodes. The current migration set is LilyGo T3S3 SX1262/SX1276 repeater and
room-server roles, Station G2 repeater and room-server roles, and ThinkNode M2
repeater and room-server roles. Station G2 moves to two 6.25 MiB slots;
the 4 MiB T3S3 and ThinkNode M2 boards move to two 1984 KiB slots.
a Full image with larger slots. The 1.17.1.7 utility release contains 94 exact
board and role migration ZIPs. Each package checks its source layout and target
identity, contains the bridge and Full application files for its supported
routes, and documents what saved data it preserves. Use the package's own Full
application after the bridge; do not substitute a loose Full image with a
different target ID. If no exact package exists, use the matching merged USB
image. Once migrated, routine app-only updates still need the same target and
partition signature.
## Maintaining the runtime directions
+1 -1
View File
@@ -7,7 +7,7 @@ The release family contains 536 qualified firmware profiles on five GitHub relea
## Installation
- Match the exact board and radio variant. Use the application image only with a compatible bootloader and partition layout.
- For an ESP32 that needs the expanded Full layout, use its exact board and role package from the utility release page. The 94 migration packages and complete migration bundle are included there. Read the package README before installation.
- For an ESP32 that needs the expanded Full layout, flash the exact merged image over USB, or use a supported exact board and role migration ZIP from the utility release for a staged Wi-Fi or LoRa route. The 94 migration packages and complete bundle are included there. Read the package README before installation; the bridge writes the partition table and a power loss can require cable recovery. Do not send a loose app-only Full image directly to an older layout.
- nRF52 application UF2 and DFU packages do not install a bootloader. Use the matching OTAFIX 2.4.8 bootloader where required.
- The RAK 3x72, Tiny Relay, Wio-E5 Mini, and Wio-E5 USB Companion images retain their framed USB transport and 32 KiB LittleFS area. Packet diagnostics are omitted on these four 256 KiB STM32WL targets to keep the application within flash.
+1 -1
View File
@@ -14,6 +14,6 @@ The `v1.16.0` part of those filenames is a stable provider URL, not the firmware
Four 256 KiB STM32WL USB Companion images (RAK 3x72, Tiny Relay, Wio-E5 Mini, and Wio-E5) omit packet diagnostics to preserve their 32 KiB LittleFS layout. Their USB Companion transport remains available. Their entries say so explicitly. Other logging controls follow each exact image's capability manifest.
See the [1.17.1.7 release guide](../docs/releases/1.17.1.7.md) and [firmware picker](../docs/firmware_picker.md). ESP32 partition migration packages are on the utility release page. nRF52 application images require a matching OTAFIX bootloader when the installed bootloader is not already compatible.
See the [1.17.1.7 release guide](../docs/releases/1.17.1.7.md) and [firmware picker](../docs/firmware_picker.md). An ESP32 `flash-wipe` merged image installs the bootloader, partition table, and application over USB. An app-only `flash-update` image requires an already compatible layout; it cannot expand partitions. For an existing node that must retain a Wi-Fi or LoRa update path, check the [utility release](https://github.com/mikecarper/MeshCore/releases/tag/utility-v1.17.1.7-halo-keymind-cascade-dev-2d03e098) for its exact board and role migration ZIP and follow the README inside. Back up identity and settings, and plan for cable recovery if power fails during the partition-table write. nRF52 application images require a matching OTAFIX bootloader when the installed bootloader is not already compatible.
Catalogs are generated from the staged release manifest, with every asset URL checked against the staged file and tag. Historical `provider-backup.json` files are retained unchanged.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+53 -7
View File
@@ -792,9 +792,9 @@ def release_tag_for_identity(identity: str, args: argparse.Namespace) -> str:
def file_type_and_title(path: Path, device_type: str) -> tuple[str, str]:
lowered = path.name.lower()
if lowered.endswith("-merged.bin"):
return "flash-wipe", "Full install (bootloader + firmware)"
return "flash-wipe", "USB full install (bootloader + partitions + app)"
if path.suffix.lower() == ".bin" and device_type == "esp32":
return "flash-update", "Update (app only)"
return "flash-update", "App update (matching partition layout only)"
if path.suffix.lower() == ".zip" and device_type == "nrf52":
return "flash", "Serial DFU package"
if path.suffix.lower() == ".uf2":
@@ -846,8 +846,9 @@ def partition_warning(identities: list[str]) -> str | None:
return (
"PARTITION - This target fits its generated app partition but is larger "
"than the legacy 1,310,720-byte portable app ceiling. If the device has "
"an older or smaller partition table, flash the matching merged image "
"once before using app-only updates."
"an older or smaller partition table, install the matching merged image "
"over USB or use a supported exact board and role migration ZIP before "
"app-only updates."
)
@@ -858,6 +859,42 @@ def append_partition_warning(notes: str, identities: list[str]) -> str:
return notes + "\n\n" + warning
def append_esp32_full_migration_guidance(
notes: str,
identities: list[str],
role: str,
repo: str,
utility_tag: str,
) -> str:
"""Explain both supported layout-change routes on infrastructure Full entries."""
if role not in {"repeater", "roomServer", "sensor"} or not any(
marker in identity.lower()
for identity in identities
for marker in ("-full-logging-ota", "-full-usb-wifi-ota", "-full-ota")
):
return notes
utility_url = (
f"https://github.com/{repo}/releases/tag/{quote(utility_tag, safe='')}"
)
paragraph = (
"MIGRATION - If the installed ESP32 has an older or smaller partition "
"layout, do not send this loose app-only .bin over Wi-Fi or LoRa OTA. "
"Either install the matching merged .bin over USB, or check "
f"{utility_url} for an exact board and role migration ZIP and follow "
"its included README. Back up identity, configuration, keys, and radio "
"settings first; a staged partition-table write can require cable "
"recovery after power loss. Use the ZIP's included Full application "
"after its bridge, then keep future app-only updates on the same target "
"and partition signature."
)
paragraphs = [
item for item in notes.split("\n\n")
if not item.startswith("MIGRATION - ")
]
paragraphs.append(paragraph)
return "\n\n".join(paragraphs)
def append_companion_power_saving_note(notes: str, display_version: str) -> str:
paragraph = (
f"POWER SAVING - {display_version} enables Companion device power saving "
@@ -916,9 +953,10 @@ def observer_notes(
role_paragraph,
profile_paragraph,
(
"INSTALL - Flash Full install (the merged bootloader + firmware image) "
"over USB once to install the expanded partition table. Routine upgrades "
"can then use Update (the app-only image) while that layout remains installed."
"INSTALL - Flash the merged bootloader, partition table, and app image "
"over USB for a new Full layout, or use a supported exact board and "
"role migration ZIP. Routine app-only upgrades then require the same "
"target and compatible layout."
),
]
warning = partition_warning(identities)
@@ -1054,6 +1092,14 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
notes,
resolved_identities,
)
if device_type == "esp32":
notes = append_esp32_full_migration_guidance(
notes,
resolved_identities,
firmware["role"],
args.repo,
args.utility_tag,
)
if firmware["role"].startswith("companion"):
notes = append_companion_power_saving_note(notes, display_version)
+1 -1
View File
@@ -26,7 +26,7 @@ extra_css:
extra_javascript:
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
- _javascript/firmware_picker.js?v=20260921-1
- _javascript/firmware_picker.js?v=20260927-2
- _javascript/telemetry_decoder.js
- _javascript/management_decoder.js
- _javascript/filter_tool.js
+14
View File
@@ -1063,6 +1063,20 @@ const currentCatalog = picker.buildCatalog([
], currentControls);
assert.strictEqual(currentCatalog.rows.length, currentAssets.length);
assert(currentCatalog.profiles.every(profile => profile.controls && profile.chipFamily !== 'unknown'));
const expandedEsp32 = currentCatalog.profiles.find(profile =>
profile.target === 'Ebyte_EoRa-S3_Repeater-full-logging');
assert(expandedEsp32);
assert(picker.installSteps(expandedEsp32, 'bin').some(step =>
step.includes('exact board/role migration ZIP')));
assert.strictEqual(
picker.migrationReleaseUrl(expandedEsp32, expandedEsp32.files[0]),
'https://github.com/mikecarper/MeshCore/releases/tag/utility-' +
currentControls.familyTag);
const ordinaryEsp32 = currentCatalog.profiles.find(profile =>
profile.target === 'Ebyte_EoRa-S3_Repeater');
assert(ordinaryEsp32);
assert.strictEqual(
picker.migrationReleaseUrl(ordinaryEsp32, ordinaryEsp32.files[0]), '');
const ikokaNormal = currentCatalog.profiles.find(profile =>
profile.target === 'ikoka_stick_nrf_30dbm_repeater');
assert(!currentCatalog.profiles.some(profile =>