Expand ESP32 OTA partition migration across infrastructure roles
This commit is contained in:
1 parent
d68601b358
commit
e7f6d529a0
11 files changed
+651
-101
No files matched your search
@@ -3109,22 +3109,30 @@ is_esp32_full_only_bulk_target() {
|
||||
}
|
||||
|
||||
# These normal role identities still ship a portable legacy image for installed
|
||||
# nodes, but also need a canonical FULL artifact for the one-time wired layout
|
||||
# nodes, but also need a canonical FULL artifact for the one-time layout
|
||||
# migration. They are deliberately separate from the FULL-only list above:
|
||||
# their deployed 0x140000 slots differ from the expanded table, so LoRa mOTA
|
||||
# must not try to cross this boundary. The matching merged image is the
|
||||
# migration package; after it is flashed, later normal-target FULL packages
|
||||
# use the same logical mOTA identity.
|
||||
# some deployed 0x140000 slots differ from the expanded table, so LoRa mOTA
|
||||
# must not try to cross this boundary. An exact-target Wi-Fi bridge and Full
|
||||
# application are packaged separately; after migration, later normal-target
|
||||
# FULL packages use the same logical mOTA identity.
|
||||
is_esp32_partition_migration_full_target() {
|
||||
local env_name=${1,,}
|
||||
|
||||
[ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "ESP32_PLATFORM" ] || return 1
|
||||
|
||||
case "$env_name" in
|
||||
ebyte_eora-s3_repeater|ebyte_eora-s3_room_server|\
|
||||
generic_e22_sx1262_repeater|generic_e22_sx1268_repeater|\
|
||||
heltec_ct62_repeater|heltec_ct62_sensor|\
|
||||
lilygo_t3s3_sx1262_repeater|lilygo_t3s3_sx1262_room_server|\
|
||||
lilygo_t3s3_sx1276_repeater|lilygo_t3s3_sx1276_room_server|\
|
||||
station_g2_repeater|station_g2_room_server|\
|
||||
thinknode_m2_repeater|thinknode_m2_room_server)
|
||||
station_g2_repeater|station_g2_logging_repeater|station_g2_room_server|\
|
||||
thinknode_m2_repeater|thinknode_m2_room_server|\
|
||||
thinknode_m5_repeater|thinknode_m5_room_server|\
|
||||
xiao_c3_repeater|xiao_c3_room_server|\
|
||||
tenstar_c3_sx1262_repeater|tenstar_c3_sx1268_repeater|\
|
||||
nibble_zero_connect_repeater_|nibble_zero_connect_room_server_|\
|
||||
nibble_screen_connect_repeater_|nibble_screen_connect_room_server_)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
@@ -3202,7 +3210,7 @@ declare_build_capability_contract() {
|
||||
if is_esp32_full_only_bulk_target "$env_name"; then
|
||||
record_build_capability "release.full.same_partition_successor"
|
||||
elif is_esp32_partition_migration_full_target "$env_name"; then
|
||||
record_build_capability "release.full.partition_migration_usb"
|
||||
record_build_capability "release.full.partition_migration_ota"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -7,15 +7,17 @@ updater for the full image after migration. The LoRa version retains the old
|
||||
OTA-capable repeater in the other expanded slot and returns to it after
|
||||
restoring identity, so it can fetch the full image over LoRa.
|
||||
|
||||
It currently has explicit, byte-for-byte generated target plans for Arduino
|
||||
ESP32 `default_8MB.csv` and `default_16MB.csv`. It does not guess a table for
|
||||
other flash sizes. Adding a board later means adding one reviewed target plan
|
||||
and its generated table-prefix test; the bridge core is otherwise shared.
|
||||
It has explicit, byte-for-byte generated target plans for the 4 MiB MeshCore
|
||||
dual-OTA Full table and Arduino ESP32 `default_8MB.csv` and
|
||||
`default_16MB.csv`. It does not guess a table for other flash sizes. Adding a
|
||||
flash size later means adding one reviewed target plan and its generated
|
||||
table-prefix test; the bridge core is otherwise shared.
|
||||
|
||||
| Flash size | Target table | app0 / app1 size | Typical board |
|
||||
| --- | --- | --- | --- |
|
||||
| 8 MiB | `default_8MB.csv` | 0x330000 / 0x330000 | Seeed XIAO ESP32-S3 |
|
||||
| 16 MiB | `default_16MB.csv` | 0x640000 / 0x640000 | Heltec V4 / V4.3 OLED |
|
||||
| 4 MiB | `variants/dual_ota_full_4MB.csv` | 0x1F0000 / 0x1F0000 | ThinkNode M2/M5, LilyGo T3S3, Nibble, Heltec CT62 |
|
||||
| 8 MiB | `default_8MB.csv` | 0x330000 / 0x330000 | Seeed XIAO ESP32-S3, Heltec V3 |
|
||||
| 16 MiB | `default_16MB.csv` | 0x640000 / 0x640000 | Heltec V4 / V4.3, Station G2 |
|
||||
|
||||
## Repeatable ESP32 build recipe
|
||||
|
||||
@@ -25,9 +27,10 @@ From a clean commit, run the shell menu in WSL/Linux:
|
||||
sh scripts/build_esp32_partition_migration.sh
|
||||
```
|
||||
|
||||
Choose one board, or **all qualified boards**. The script asks for the
|
||||
firmware version, radio preset, and profile, then builds the Full repeater
|
||||
images first, both small bridges for each selected board, and verifies each
|
||||
Choose one exact board/role, or **all listed board/role pairs**. The menu
|
||||
also offers all repeaters, all room servers, or all sensors separately. It asks
|
||||
for the firmware version, radio preset, and profile, then builds Full application
|
||||
images first, the available bridge(s) for each selected board, and verifies each
|
||||
ZIP. Only one PlatformIO process runs at a time; `build.sh` can clean the
|
||||
shared `.pio/build` tree between Full targets. The same menu can be run
|
||||
non-interactively:
|
||||
@@ -39,12 +42,15 @@ sh scripts/build_esp32_partition_migration.sh --board heltec-v4 \
|
||||
sh scripts/build_esp32_partition_migration.sh --all \
|
||||
--version v1.17.1.7-halo-keymind-cascade-dev \
|
||||
--radio-preset usa-cascadia --profile cascade
|
||||
sh scripts/build_esp32_partition_migration.sh --role sensor \
|
||||
--version v1.17.1.7-halo-keymind-cascade-dev \
|
||||
--radio-preset usa-cascadia --profile cascade
|
||||
```
|
||||
|
||||
`--all` creates one verified ZIP per currently qualified board and a release
|
||||
`--all` creates one verified ZIP per listed board/role pair and a release
|
||||
ZIP containing all of them, their hashes, and a manifest. This is a **local
|
||||
release bundle**, not an upload to GitHub Releases. It is not an archive of
|
||||
old firmware binaries. `--list-boards` shows the presently qualified choices.
|
||||
old firmware binaries. `--list-boards` shows the configured choices.
|
||||
The release fails instead of publishing a partial bundle if one board package
|
||||
is absent or fails verification.
|
||||
|
||||
@@ -56,24 +62,40 @@ python3 -B scripts/build_esp32_partition_migration.py \
|
||||
--radio-preset usa-cascadia --profile cascade
|
||||
```
|
||||
|
||||
The default builds all currently qualified boards. Add `--board heltec-v4`
|
||||
The default builds all configured board/role recipes. Add `--board heltec-v4`
|
||||
or `--board xiao-s3-wio` to build one; repeat `--board` for a selected set.
|
||||
Use `--dry-run` to inspect the command order without building. The resulting
|
||||
ZIPs appear under `.releases/esp32-expanded-<commit>/packages/`. The recipe
|
||||
requires a clean checkout so each ZIP identifies the firmware commit it was
|
||||
built from. It creates files only; it does not flash a device or erase flash.
|
||||
|
||||
The historical 1.25 MiB release population is wider than these two boards.
|
||||
In particular, the portable LilyGo T3S3 SX1262/SX1276, Station G2, and
|
||||
ThinkNode M2 repeater/room-server targets have had 1.25 MiB slots. They are
|
||||
**not** silently treated as supported by `--all`: the 4 MiB boards require a
|
||||
different target table and a power-loss-safe LoRa receiver handoff, and every
|
||||
additional role needs an audited exact target identity. A 4 MiB Full build
|
||||
offers 1,984 KiB slots, not the 4 MiB-or-more slots possible on larger flash.
|
||||
The catalog covers canonical ESP32 repeater, room-server, and sensor roles on
|
||||
4, 8, and 16 MiB chip-family plans, including historical 1.25 MiB candidates
|
||||
now built with expanded tables. This includes ESP32-S3, original ESP32, and
|
||||
ESP32-C3 boards with an evidenced old default layout. `--list-boards` is the
|
||||
authoritative list of exact targets; specialty ESP-NOW/observer aliases are
|
||||
separate identities and are not packaged under their old IDs. With Wi-Fi OTA
|
||||
they can migrate to the
|
||||
same physical board's canonical role image; their feature settings may need
|
||||
to be recreated. The LoRa route still requires an exact old target ID. The
|
||||
catalog omits boards with no evidenced legacy default-layout build, such as
|
||||
boards introduced with `min_spiffs.csv` or another non-1.25 MiB layout. Some
|
||||
listed boards use larger slots today but had historical default-layout builds.
|
||||
A listed package is usable only when the installed old image has a working
|
||||
Wi-Fi updater and the bridge verifies its live source table. Menu inclusion
|
||||
does **not** override either check. The only LoRa
|
||||
bridge packages currently available are the exact-target Heltec V4 and XIAO
|
||||
S3 WIO repeater packages. On 4 MiB flash the expanded app1 overlaps old app1,
|
||||
so the currently safe LoRa receiver-copy method cannot be offered there. A
|
||||
4 MiB Full build offers 1,984 KiB slots, not 4 MiB per slot. A Wi-Fi-only ZIP
|
||||
can omit `full-application.mota` when its application exceeds the current
|
||||
2 KiB x 1024-block LoRa serving limit; `full-application.bin` remains the
|
||||
verified Wi-Fi update image.
|
||||
|
||||
Other ESP32 boards need a reviewed flash-size/partition plan, exact LoRa OTA
|
||||
target ID, and board entry before they can be added to this recipe. Matching
|
||||
flash capacity alone is not enough to claim a package is safe for a board.
|
||||
Other ESP32 boards need a reviewed source updater, chip/flash-size bridge,
|
||||
verified Full image, and board entry before they can be added. The LoRa route
|
||||
also needs an exact old OTA target ID and a non-overlapping receiver handoff.
|
||||
Matching flash capacity alone is not enough to claim a package is safe.
|
||||
|
||||
## What the bridge preserves
|
||||
|
||||
@@ -100,7 +122,7 @@ The bridge only accepts a source table that has all of the following:
|
||||
- NVS at `0x9000`, size `0x5000`, and OTA metadata at `0xE000`, size `0x2000`.
|
||||
- Two distinct, non-empty OTA application slots and a non-empty SPIFFS
|
||||
partition, all inside the physical flash size.
|
||||
- A known exact 8 MiB or 16 MiB physical flash capacity and a source table
|
||||
- A known exact 4, 8, or 16 MiB physical flash capacity and a source table
|
||||
different from that capacity's target table.
|
||||
|
||||
This means the source app-slot and SPIFFS sizes can vary; the bridge validates
|
||||
@@ -113,6 +135,10 @@ in legacy A or B.
|
||||
Keep power stable while the bridge is replacing the 4 KiB table sector. A loss
|
||||
of power before that point leaves the old table and source intact. After that
|
||||
small critical write, the selected A copy of the bridge is the recovery path.
|
||||
The stock ESP32 bootloader reads one partition-table sector; rewriting that
|
||||
sector is **not atomic**. A power loss during its erase/write window can leave
|
||||
an unreadable table and require cable recovery. This recipe must not be called
|
||||
power-loss-proof on a device whose USB/serial flash port is inaccessible.
|
||||
|
||||
## Migrate a Seeed XIAO ESP32-S3
|
||||
|
||||
|
||||
@@ -455,11 +455,11 @@ bool publishExpandedPartitionTable(const migration::TargetPlan& plan) {
|
||||
if (result == ESP_OK) {
|
||||
result = esp_flash_write(chip, partition_table_bytes,
|
||||
migration::kPartitionTableAddress,
|
||||
sizeof(partition_table_bytes));
|
||||
plan.partition_table_prefix_bytes);
|
||||
if (result == ESP_OK) {
|
||||
result = esp_flash_read(chip, partition_table_verified,
|
||||
migration::kPartitionTableAddress,
|
||||
sizeof(partition_table_verified));
|
||||
plan.partition_table_prefix_bytes);
|
||||
ok = result == ESP_OK && memcmp(partition_table_verified,
|
||||
plan.partition_table_prefix, plan.partition_table_prefix_bytes) == 0;
|
||||
if (!ok) strcpy(status_text, "Partition table verification failed");
|
||||
|
||||
@@ -45,10 +45,14 @@ def build_steps(boards: list[str], version: str, radio_preset: str,
|
||||
"--require-ota", "--resume",
|
||||
], True))
|
||||
# Full builds can clear .pio/build; every bridge must be built afterwards.
|
||||
built_bridges: set[str] = set()
|
||||
for name in boards:
|
||||
for key in ("wifi_bridge", "lora_bridge"):
|
||||
steps.append((["pio", "run", "-e", BOARDS[name][key],
|
||||
"-j", str(jobs)], False))
|
||||
bridge = BOARDS[name][key]
|
||||
if bridge and bridge not in built_bridges:
|
||||
steps.append((["pio", "run", "-e", bridge,
|
||||
"-j", str(jobs)], False))
|
||||
built_bridges.add(bridge)
|
||||
return steps
|
||||
|
||||
|
||||
@@ -82,13 +86,15 @@ def bundle_release(output_root: Path, package_dir: Path, boards: list[str],
|
||||
packages.append((matches[0].name, data))
|
||||
manifest = {
|
||||
"format": "meshcore-esp32-partition-migration-release-v1",
|
||||
"scope": "currently-qualified-board-recipes-only",
|
||||
"scope": "configured-legacy-layout-board-role-recipes",
|
||||
"historical_firmware_binaries_included": False,
|
||||
"source_commit": source,
|
||||
"firmware_version": version,
|
||||
"radio_preset": radio_preset,
|
||||
"profile": profile,
|
||||
"boards": boards,
|
||||
"board_roles": {name: BOARDS[name]["role"] for name in boards},
|
||||
"roles": sorted({BOARDS[name]["role"] for name in boards}),
|
||||
"packages": {name: {"bytes": len(data),
|
||||
"sha256": hashlib.sha256(data).hexdigest()}
|
||||
for name, data in packages},
|
||||
@@ -125,7 +131,9 @@ def main() -> None:
|
||||
parser.add_argument("--profile", choices=("default", "cascade"),
|
||||
default="default", help="embedded runtime profile")
|
||||
parser.add_argument("--board", action="append", choices=tuple(BOARDS),
|
||||
help="repeat to select boards (default: all supported boards)")
|
||||
help="repeat to select board/role recipes (default: all configured recipes)")
|
||||
parser.add_argument("--role", choices=("repeater", "room-server", "sensor"),
|
||||
help="build every configured board for this role")
|
||||
parser.add_argument("--jobs", type=int, default=4,
|
||||
help="workers within each serial PlatformIO build")
|
||||
parser.add_argument("--output-root", type=Path,
|
||||
@@ -133,7 +141,7 @@ def main() -> None:
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="show the build order without writing files")
|
||||
parser.add_argument("--list-boards", action="store_true",
|
||||
help="print qualified board keys and exit")
|
||||
help="print configured board/role keys and exit")
|
||||
args = parser.parse_args()
|
||||
if args.list_boards:
|
||||
for name in BOARDS:
|
||||
@@ -145,11 +153,16 @@ def main() -> None:
|
||||
parser.error("--version must be a filename-safe version token")
|
||||
if args.jobs < 1:
|
||||
parser.error("--jobs must be positive")
|
||||
if args.board and args.role:
|
||||
parser.error("choose --board or --role, not both")
|
||||
if os.name == "nt" and not args.dry_run:
|
||||
parser.error("run this recipe inside Linux/WSL, where bash and pio are available")
|
||||
|
||||
source = git_output("rev-parse", "--short=8", "HEAD")
|
||||
boards = list(dict.fromkeys(args.board or BOARDS))
|
||||
if args.role:
|
||||
boards = [name for name, spec in BOARDS.items() if spec["role"] == args.role]
|
||||
else:
|
||||
boards = list(dict.fromkeys(args.board or BOARDS))
|
||||
requested_root = args.output_root or Path(".releases") / f"esp32-expanded-{source}"
|
||||
output_root = (requested_root if requested_root.is_absolute()
|
||||
else ROOT / requested_root).resolve()
|
||||
@@ -161,7 +174,7 @@ def main() -> None:
|
||||
print(prefix + shlex.join(command), flush=True)
|
||||
print(f"Package {', '.join(boards)} into {package_dir}", flush=True)
|
||||
if set(boards) == set(BOARDS):
|
||||
print(f"Bundle complete qualified set (not all historical ESP32 targets) in {output_root}", flush=True)
|
||||
print(f"Bundle all configured board/role recipes (not historical binaries) in {output_root}", flush=True)
|
||||
if args.dry_run:
|
||||
return
|
||||
|
||||
|
||||
@@ -9,24 +9,25 @@ usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/build_esp32_partition_migration.sh [options]
|
||||
|
||||
With no arguments, choose one qualified ESP32 board or the complete qualified
|
||||
set from a menu. The complete set creates one ZIP per board plus a release ZIP.
|
||||
With no arguments, choose one ESP32 board/role or the complete configured
|
||||
set from a menu. The complete set creates one ZIP per board/role plus a release ZIP.
|
||||
|
||||
--board KEY Build one board (repeatable)
|
||||
--all Build the complete qualified set
|
||||
--role ROLE Build every repeater, room-server, or sensor board
|
||||
--all Build all configured repeater, room-server and sensor roles
|
||||
--version VERSION Firmware version (required without the menu)
|
||||
--radio-preset NAME Radio preset (required without the menu)
|
||||
--profile NAME default or cascade (default: cascade)
|
||||
--jobs N PlatformIO workers within each serial build
|
||||
--output-root PATH Release output folder
|
||||
--dry-run Print commands without building
|
||||
--list-boards Show board keys that have verified migration recipes
|
||||
--list-boards Show configured board/role keys
|
||||
--help Show this help
|
||||
|
||||
Boards without an audited flash/partition/identity plan are not offered. A
|
||||
name appearing in an old firmware release is not proof that OTA migration is
|
||||
safe on that hardware. No release ZIP is published unless every selected
|
||||
package passes the recipe's checks.
|
||||
Boards with unrelated source layout plans are not offered. A name appearing
|
||||
in an old firmware release is not proof that OTA migration is safe on that
|
||||
hardware. The bridge checks the live layout, and no release ZIP is published
|
||||
unless every selected package passes the recipe's checks.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -41,6 +42,7 @@ profile=cascade
|
||||
jobs=
|
||||
output_root=
|
||||
boards=
|
||||
role=
|
||||
all=0
|
||||
dry_run=0
|
||||
interactive=1
|
||||
@@ -48,13 +50,14 @@ interactive=1
|
||||
while [ "$#" -gt 0 ]; do
|
||||
interactive=0
|
||||
case "$1" in
|
||||
--board|--version|--radio-preset|--profile|--jobs|--output-root)
|
||||
--board|--role|--version|--radio-preset|--profile|--jobs|--output-root)
|
||||
if [ "$#" -lt 2 ]; then
|
||||
echo "Missing value for $1" >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$1" in
|
||||
--board) boards="${boards}${boards:+ }$2" ;;
|
||||
--role) role=$2 ;;
|
||||
--version) version=$2 ;;
|
||||
--radio-preset) radio_preset=$2 ;;
|
||||
--profile) profile=$2 ;;
|
||||
@@ -72,24 +75,30 @@ done
|
||||
|
||||
board_keys=$(python3 -B "$recipe" --list-boards)
|
||||
if [ -z "$board_keys" ]; then
|
||||
echo "No qualified boards are configured" >&2
|
||||
echo "No boards are configured" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$interactive" -eq 1 ]; then
|
||||
echo "ESP32 in-place partition migration"
|
||||
echo "Only audited board/layout recipes are listed."
|
||||
echo "Repeater, room-server and sensor recipes are listed."
|
||||
index=1
|
||||
for board in $board_keys; do
|
||||
echo " $index) $board"
|
||||
index=$((index + 1))
|
||||
done
|
||||
echo " a) Build all qualified boards and create a release ZIP"
|
||||
echo " a) Build all configured board/role recipes and create a release ZIP"
|
||||
echo " r) Build all repeaters"
|
||||
echo " m) Build all room servers"
|
||||
echo " s) Build all sensors"
|
||||
echo " q) Quit"
|
||||
printf 'Choose: '
|
||||
IFS= read -r choice
|
||||
case "$choice" in
|
||||
a|A|all) all=1 ;;
|
||||
r|R) role=repeater ;;
|
||||
m|M) role=room-server ;;
|
||||
s|S) role=sensor ;;
|
||||
q|Q|quit) exit 0 ;;
|
||||
*)
|
||||
index=1
|
||||
@@ -115,8 +124,9 @@ if [ "$interactive" -eq 1 ]; then
|
||||
profile=${selected_profile:-cascade}
|
||||
fi
|
||||
|
||||
if [ "$all" -eq 1 ] && [ -n "$boards" ]; then
|
||||
echo "Choose either --all or --board, not both" >&2
|
||||
if { [ "$all" -eq 1 ] && { [ -n "$boards" ] || [ -n "$role" ]; }; } \
|
||||
|| { [ -n "$boards" ] && [ -n "$role" ]; }; then
|
||||
echo "Choose only one of --all, --board, or --role" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -z "$version" ] || [ -z "$radio_preset" ]; then
|
||||
@@ -127,6 +137,11 @@ if [ "$profile" != default ] && [ "$profile" != cascade ]; then
|
||||
echo "Profile must be default or cascade" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -n "$role" ] && [ "$role" != repeater ] \
|
||||
&& [ "$role" != room-server ] && [ "$role" != sensor ]; then
|
||||
echo "Role must be repeater, room-server, or sensor" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
set -- "$recipe" --version "$version" --radio-preset "$radio_preset" --profile "$profile"
|
||||
if [ -n "$boards" ]; then
|
||||
@@ -136,12 +151,13 @@ if [ -n "$boards" ]; then
|
||||
if [ "$board" = "$known" ]; then found=1; break; fi
|
||||
done
|
||||
if [ "$found" -ne 1 ]; then
|
||||
echo "Board is not qualified for this recipe: $board" >&2
|
||||
echo "Board/role is not configured for this recipe: $board" >&2
|
||||
exit 2
|
||||
fi
|
||||
set -- "$@" --board "$board"
|
||||
done
|
||||
fi
|
||||
if [ -n "$role" ]; then set -- "$@" --role "$role"; fi
|
||||
if [ -n "$jobs" ]; then set -- "$@" --jobs "$jobs"; fi
|
||||
if [ -n "$output_root" ]; then set -- "$@" --output-root "$output_root"; fi
|
||||
if [ "$dry_run" -eq 1 ]; then set -- "$@" --dry-run; fi
|
||||
|
||||
@@ -39,7 +39,234 @@ BOARDS = {
|
||||
"slot_bytes": 0x330000,
|
||||
"slot1_address": 0x340000,
|
||||
},
|
||||
"station-g2-repeater": {
|
||||
"target": "Station_G2_repeater",
|
||||
"wifi_bridge": "esp32_s3_16mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 16 * 1024 * 1024,
|
||||
"slot_bytes": 0x640000,
|
||||
"slot1_address": 0x650000,
|
||||
},
|
||||
"station-g2-room-server": {
|
||||
"target": "Station_G2_room_server",
|
||||
"wifi_bridge": "esp32_s3_16mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 16 * 1024 * 1024,
|
||||
"slot_bytes": 0x640000,
|
||||
"slot1_address": 0x650000,
|
||||
},
|
||||
"t3s3-sx1262-repeater": {
|
||||
"target": "LilyGo_T3S3_sx1262_repeater",
|
||||
"wifi_bridge": "esp32_s3_4mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 4 * 1024 * 1024,
|
||||
"slot_bytes": 0x1F0000,
|
||||
"slot1_address": 0x200000,
|
||||
},
|
||||
"t3s3-sx1262-room-server": {
|
||||
"target": "LilyGo_T3S3_sx1262_room_server",
|
||||
"wifi_bridge": "esp32_s3_4mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 4 * 1024 * 1024,
|
||||
"slot_bytes": 0x1F0000,
|
||||
"slot1_address": 0x200000,
|
||||
},
|
||||
"t3s3-sx1276-repeater": {
|
||||
"target": "LilyGo_T3S3_sx1276_repeater",
|
||||
"wifi_bridge": "esp32_s3_4mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 4 * 1024 * 1024,
|
||||
"slot_bytes": 0x1F0000,
|
||||
"slot1_address": 0x200000,
|
||||
},
|
||||
"t3s3-sx1276-room-server": {
|
||||
"target": "LilyGo_T3S3_sx1276_room_server",
|
||||
"wifi_bridge": "esp32_s3_4mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 4 * 1024 * 1024,
|
||||
"slot_bytes": 0x1F0000,
|
||||
"slot1_address": 0x200000,
|
||||
},
|
||||
"thinknode-m2-repeater": {
|
||||
"target": "ThinkNode_M2_Repeater",
|
||||
"wifi_bridge": "esp32_s3_4mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 4 * 1024 * 1024,
|
||||
"slot_bytes": 0x1F0000,
|
||||
"slot1_address": 0x200000,
|
||||
},
|
||||
"thinknode-m2-room-server": {
|
||||
"target": "ThinkNode_M2_room_server",
|
||||
"wifi_bridge": "esp32_s3_4mb_partition_migrator",
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": 4 * 1024 * 1024,
|
||||
"slot_bytes": 0x1F0000,
|
||||
"slot1_address": 0x200000,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def role_for_target(target: str) -> str:
|
||||
lower = target.lower()
|
||||
if "_room_server" in lower:
|
||||
return "room-server"
|
||||
if "_repeater" in lower:
|
||||
return "repeater"
|
||||
if "_sensor" in lower:
|
||||
return "sensor"
|
||||
raise ValueError(f"migration target has no infrastructure role: {target}")
|
||||
|
||||
|
||||
def add_wifi_only_roles(family: str, flash_mib: int,
|
||||
targets: dict[str, str],
|
||||
bridge: str | None = None) -> None:
|
||||
"""Register exact role identities; a family can have one or all roles."""
|
||||
if flash_mib not in (4, 8, 16):
|
||||
raise ValueError(f"unsupported migration flash size: {flash_mib} MiB")
|
||||
slot_bytes, slot1_address = {
|
||||
4: (0x1F0000, 0x200000),
|
||||
8: (0x330000, 0x340000),
|
||||
16: (0x640000, 0x650000),
|
||||
}[flash_mib]
|
||||
if bridge is None:
|
||||
bridge = f"esp32_s3_{flash_mib}mb_partition_migrator"
|
||||
for role, target in targets.items():
|
||||
if role not in ("repeater", "room-server", "sensor"):
|
||||
raise ValueError(f"unsupported migration role: {role}")
|
||||
key = f"{family}-{role}"
|
||||
if key in BOARDS:
|
||||
raise ValueError(f"duplicate migration board role: {key}")
|
||||
BOARDS[key] = {
|
||||
"target": target,
|
||||
"role": role,
|
||||
"wifi_bridge": bridge,
|
||||
"lora_bridge": None,
|
||||
"flash_bytes": flash_mib * 1024 * 1024,
|
||||
"slot_bytes": slot_bytes,
|
||||
"slot1_address": slot1_address,
|
||||
}
|
||||
|
||||
|
||||
# The first two repeater keys above retain their published LoRa-capable bridge
|
||||
# names. Other role packages use the chip-family Wi-Fi bridge. These are the
|
||||
# canonical ESP32 infrastructure identities on 4/8/16 MiB boards which can
|
||||
# have a deployed Arduino default.csv 1.25 MiB table, including historically
|
||||
# default.csv boards now using a larger layout. Other boards that were only
|
||||
# ever built with unrelated layouts are not represented as 1.25 MiB migrants.
|
||||
for family, flash_mib, roles in (
|
||||
("heltec-v4", 16, {"room-server": "heltec_v4_room_server",
|
||||
"sensor": "heltec_v4_sensor"}),
|
||||
("heltec-v4-expansionkit", 16, {
|
||||
"repeater": "heltec_v4_expansionkit_repeater"}),
|
||||
("xiao-s3-wio", 8, {"room-server": "Xiao_S3_WIO_room_server",
|
||||
"sensor": "Xiao_S3_WIO_sensor"}),
|
||||
("nibble-zero", 4, {"repeater": "nibble_zero_connect_repeater_",
|
||||
"room-server": "nibble_zero_connect_room_server_"}),
|
||||
("nibble-screen", 4, {"repeater": "nibble_screen_connect_repeater_",
|
||||
"room-server": "nibble_screen_connect_room_server_"}),
|
||||
("ebyte-eora-s3", 4, {"repeater": "Ebyte_EoRa-S3_Repeater",
|
||||
"room-server": "Ebyte_EoRa-S3_room_server"}),
|
||||
("thinknode-m5", 4, {"repeater": "ThinkNode_M5_Repeater",
|
||||
"room-server": "ThinkNode_M5_room_server"}),
|
||||
("heltec-wireless-tracker", 8, {
|
||||
"repeater": "Heltec_Wireless_Tracker_repeater",
|
||||
"room-server": "Heltec_Wireless_Tracker_room_server"}),
|
||||
("heltec-tracker-v2", 8, {"repeater": "heltec_tracker_v2_repeater",
|
||||
"room-server": "heltec_tracker_v2_room_server",
|
||||
"sensor": "heltec_tracker_v2_sensor"}),
|
||||
("heltec-v3", 8, {"repeater": "Heltec_v3_repeater",
|
||||
"room-server": "Heltec_v3_room_server",
|
||||
"sensor": "Heltec_v3_sensor"}),
|
||||
("heltec-wsl3", 8, {"repeater": "Heltec_WSL3_repeater",
|
||||
"room-server": "Heltec_WSL3_room_server",
|
||||
"sensor": "Heltec_WSL3_sensor"}),
|
||||
("heltec-wireless-paper", 8, {
|
||||
"repeater": "Heltec_Wireless_Paper_repeater",
|
||||
"room-server": "Heltec_Wireless_Paper_room_server"}),
|
||||
("t-beam-s3-supreme-sx1262", 8, {
|
||||
"repeater": "T_Beam_S3_Supreme_SX1262_repeater",
|
||||
"room-server": "T_Beam_S3_Supreme_SX1262_room_server"}),
|
||||
("thinknode-m7", 8, {"repeater": "ThinkNode_M7_repeater",
|
||||
"room-server": "ThinkNode_M7_room_server"}),
|
||||
("xiao-s3", 8, {"repeater": "Xiao_S3_repeater",
|
||||
"room-server": "Xiao_S3_room_server",
|
||||
"sensor": "Xiao_S3_sensor"}),
|
||||
("mke-s3", 8, {"repeater": "MKE_s3_repeater",
|
||||
"room-server": "MKE_s3_room_server",
|
||||
"sensor": "MKE_s3_sensor"}),
|
||||
("rak-3112", 8, {"repeater": "RAK_3112_repeater",
|
||||
"room-server": "RAK_3112_room_server",
|
||||
"sensor": "RAK_3112_sensor"}),
|
||||
("heltec-e213", 16, {"repeater": "Heltec_E213_repeater",
|
||||
"room-server": "Heltec_E213_room_server"}),
|
||||
("heltec-e290", 16, {"repeater": "Heltec_E290_repeater",
|
||||
"room-server": "Heltec_E290_room_server"}),
|
||||
("heltec-rc32", 16, {"repeater": "heltec_rc32_repeater",
|
||||
"room-server": "heltec_rc32_room_server",
|
||||
"sensor": "heltec_rc32_sensor"}),
|
||||
("heltec-rc32-no-display", 16, {
|
||||
"repeater": "heltec_rc32_without_display_repeater",
|
||||
"room-server": "heltec_rc32_without_display_room_server",
|
||||
"sensor": "heltec_rc32_without_display_sensor"}),
|
||||
("heltec-t190", 16, {"repeater": "Heltec_T190_repeater_",
|
||||
"room-server": "Heltec_T190_room_server_"}),
|
||||
("heltec-v4-tft", 16, {"repeater": "heltec_v4_tft_repeater",
|
||||
"room-server": "heltec_v4_tft_room_server",
|
||||
"sensor": "heltec_v4_tft_sensor"}),
|
||||
("heltec-v4-r8", 16, {"repeater": "heltec_v4_r8_repeater",
|
||||
"room-server": "heltec_v4_r8_room_server",
|
||||
"sensor": "heltec_v4_r8_sensor"}),
|
||||
("heltec-v4-r8-tft", 16, {"repeater": "heltec_v4_r8_tft_repeater",
|
||||
"room-server": "heltec_v4_r8_tft_room_server",
|
||||
"sensor": "heltec_v4_r8_tft_sensor"}),
|
||||
("lilygo-tbeam-1w", 16, {"repeater": "LilyGo_TBeam_1W_repeater",
|
||||
"room-server": "LilyGo_TBeam_1W_room_server"}),
|
||||
("lilygo-tdeck", 16, {"repeater": "LilyGo_TDeck_repeater"}),
|
||||
("lilygo-teth-elite-sx1262", 16, {
|
||||
"repeater": "LilyGo_TETH_Elite_sx1262_repeater",
|
||||
"room-server": "LilyGo_TETH_Elite_sx1262_room_server"}),
|
||||
("meshnology-w12", 16, {"repeater": "meshnology_w12_repeater",
|
||||
"room-server": "meshnology_w12_room_server",
|
||||
"sensor": "meshnology_w12_sensor"}),
|
||||
("station-g3-esp32", 16, {"repeater": "Station_G3_ESP32_repeater",
|
||||
"room-server": "Station_G3_ESP32_room_server"}),
|
||||
("station-g3-esp32-logging", 16, {
|
||||
"repeater": "Station_G3_ESP32_logging_repeater"}),
|
||||
("station-g2-logging", 16, {
|
||||
"repeater": "Station_G2_logging_repeater"}),
|
||||
("thinknode-m9", 16, {"repeater": "ThinkNode_M9_repeater_",
|
||||
"room-server": "ThinkNode_M9_room_server_"}),
|
||||
):
|
||||
add_wifi_only_roles(family, flash_mib, roles)
|
||||
|
||||
add_wifi_only_roles("heltec-v2", 8, {
|
||||
"repeater": "Heltec_v2_repeater",
|
||||
"room-server": "Heltec_v2_room_server",
|
||||
}, bridge="esp32_8mb_partition_migrator")
|
||||
add_wifi_only_roles("heltec-ct62", 4, {
|
||||
"repeater": "Heltec_ct62_repeater",
|
||||
"sensor": "Heltec_ct62_sensor",
|
||||
}, bridge="esp32_c3_4mb_partition_migrator")
|
||||
add_wifi_only_roles("xiao-c3", 4, {
|
||||
"repeater": "Xiao_C3_repeater",
|
||||
"room-server": "Xiao_C3_room_server",
|
||||
}, bridge="esp32_c3_4mb_partition_migrator")
|
||||
add_wifi_only_roles("tenstar-c3-sx1262", 4, {
|
||||
"repeater": "Tenstar_C3_sx1262_repeater",
|
||||
}, bridge="esp32_c3_4mb_partition_migrator")
|
||||
add_wifi_only_roles("tenstar-c3-sx1268", 4, {
|
||||
"repeater": "Tenstar_C3_sx1268_repeater",
|
||||
}, bridge="esp32_c3_4mb_partition_migrator")
|
||||
add_wifi_only_roles("generic-e22-sx1262", 4, {
|
||||
"repeater": "Generic_E22_sx1262_repeater",
|
||||
}, bridge="esp32_4mb_partition_migrator")
|
||||
add_wifi_only_roles("generic-e22-sx1268", 4, {
|
||||
"repeater": "Generic_E22_sx1268_repeater",
|
||||
}, bridge="esp32_4mb_partition_migrator")
|
||||
|
||||
for specification in BOARDS.values():
|
||||
specification.setdefault("role", role_for_target(specification["target"]))
|
||||
|
||||
LEGACY_SLOT_BYTES = 0x140000
|
||||
|
||||
|
||||
@@ -92,29 +319,19 @@ def check_esp32_stage(package: bytes, image: bytes, slot_bytes: int) -> None:
|
||||
raise ValueError("mOTA image cannot be staged in the inactive ESP32 slot")
|
||||
|
||||
|
||||
def readme(board: str, spec: dict, version: str, source: str) -> str:
|
||||
return f"""# {board} in-place partition migration
|
||||
def readme(board: str, spec: dict, version: str, source: str,
|
||||
has_full_mota: bool = True) -> str:
|
||||
wifi_instructions = f"""## Wi-Fi route
|
||||
|
||||
Source: `{source}`; firmware version: `{version}`. This package is only for the
|
||||
exact `{spec['target']}` board/role with {spec['flash_bytes'] // 1048576} MiB flash.
|
||||
The current partition table must have stable NVS at 0x9000, OTA metadata at
|
||||
0xE000, two OTA apps, and SPIFFS with `/identity/_main.id`.
|
||||
|
||||
The bridge checks the live layout and refuses unsupported boards. It stages
|
||||
the 96-byte public/private identity in unchanged NVS, copies and verifies the
|
||||
required application, changes the partition table, then restores and verifies
|
||||
the identity in expanded SPIFFS. No full-chip erase or USB connection is used.
|
||||
The table sector and inactive app sectors are erased as part of migration.
|
||||
SPIFFS may be reformatted; other SPIFFS settings can be recreated.
|
||||
|
||||
## Wi-Fi route
|
||||
|
||||
1. Upload `wifi-bridge.bin` through the existing repeater's Wi-Fi OTA page.
|
||||
1. On the old node, run `start ota ap` through its authenticated terminal.
|
||||
Join its `MeshCore-OTA` Wi-Fi and upload `wifi-bridge.bin` at the exact
|
||||
`Started:` URL returned by the command (the port can vary by old build).
|
||||
2. Keep power stable. Join `MeshCore-Migrate` (password `meshcore-migrate`),
|
||||
open `http://192.168.4.1/`, and wait for **Expanded layout ready**.
|
||||
3. At that page's `/update`, upload `full-application.bin`.
|
||||
|
||||
## LoRa route
|
||||
"""
|
||||
if spec["lora_bridge"]:
|
||||
lora_instructions = """## LoRa route
|
||||
|
||||
The old repeater must already have working MeshCore LoRa mOTA, a valid EndF,
|
||||
and the exact target ID in `manifest.json`. A stock image without that updater
|
||||
@@ -131,8 +348,51 @@ table and refuses if its image or target does not match.
|
||||
A full-image LoRa transfer can take considerable time.
|
||||
|
||||
`full-application.bin` and `full-application.mota` are the same exact-target
|
||||
Full repeater image. `target-partitions.bin` is included for verification only;
|
||||
Full image. `target-partitions.bin` is included for verification only;
|
||||
do not upload it to the browser. Neither bridge is a USB merged image.
|
||||
"""
|
||||
else:
|
||||
full_mota_note = (
|
||||
"Do not upload `full-application.mota` until the expanded layout "
|
||||
"and new Full image are in place."
|
||||
if has_full_mota else
|
||||
"This ZIP does not include `full-application.mota`: the Full image "
|
||||
"exceeds the current LoRa mOTA 2 KiB x 1024-block serving limit. "
|
||||
"Use `full-application.bin` through Wi-Fi."
|
||||
)
|
||||
lora_instructions = f"""## LoRa limitation
|
||||
|
||||
This package has **no LoRa bridge**. The Wi-Fi updater in the installed old
|
||||
firmware is required. On 4 MiB flash, the expanded app1 overlaps the old app1,
|
||||
so the current LoRa receiver-copy method cannot survive a power loss safely.
|
||||
Some older room-server images also have no LoRa mOTA receiver. Do not upload
|
||||
the Full image through LoRa before the new layout is active. {full_mota_note}
|
||||
`target-partitions.bin` is for verification only, not browser upload.
|
||||
"""
|
||||
return f"""# {board} in-place partition migration
|
||||
|
||||
Source: `{source}`; firmware version: `{version}`. This package is only for the
|
||||
physical board/role represented by `{spec['target']}` with
|
||||
{spec['flash_bytes'] // 1048576} MiB flash. A legacy Wi-Fi-updatable observer or
|
||||
bridge variant on the same hardware may migrate to this canonical Full target;
|
||||
its previous feature settings are not preserved. The LoRa route, where present,
|
||||
requires the old image's exact target ID.
|
||||
The current partition table must have stable NVS at 0x9000, OTA metadata at
|
||||
0xE000, two OTA apps, and SPIFFS with `/identity/_main.id`.
|
||||
|
||||
The bridge checks the live layout and refuses unsupported boards. It stages
|
||||
the 96-byte public/private identity in unchanged NVS, copies and verifies the
|
||||
required application, changes the partition table, then restores and verifies
|
||||
the identity in expanded SPIFFS. No full-chip erase or USB connection is used.
|
||||
The table sector and inactive app sectors are erased as part of migration.
|
||||
SPIFFS may be reformatted; other SPIFFS settings can be recreated.
|
||||
The stock bootloader has no atomic backup partition table: power loss during
|
||||
the table-sector erase/write can still require cable recovery. Do not use this
|
||||
on an inaccessible node without accepting that risk and testing a sacrificial
|
||||
example of the same board first.
|
||||
|
||||
{wifi_instructions}
|
||||
{lora_instructions}
|
||||
"""
|
||||
|
||||
|
||||
@@ -141,7 +401,10 @@ def package_board(name: str, spec: dict, build_dir: Path, output_dir: Path,
|
||||
target = spec["target"]
|
||||
target_id = target_id_for_env(target)
|
||||
hw_id = hardware_id_for_env(target)
|
||||
stem = f"{target}-full-usb-wifi-ota-{version}-{source}"
|
||||
images = sorted(build_dir.glob(f"{target}-full-*-{version}-{source}.bin"))
|
||||
if len(images) != 1:
|
||||
raise ValueError(f"{name}: expected one exact-target Full image, found {len(images)}")
|
||||
stem = images[0].stem
|
||||
full_path = build_dir / (stem + ".bin")
|
||||
capability_path = build_dir / (stem + ".capabilities.json")
|
||||
validate_package(build_dir / stem)
|
||||
@@ -172,27 +435,41 @@ def package_board(name: str, spec: dict, build_dir: Path, output_dir: Path,
|
||||
raise ValueError(f"{name}: Full image identity or partition fit failed")
|
||||
|
||||
wifi_bridge = (ROOT / ".pio" / "build" / spec["wifi_bridge"] / "firmware.bin").read_bytes()
|
||||
lora_bridge_body = (ROOT / ".pio" / "build" / spec["lora_bridge"] / "firmware.bin").read_bytes()
|
||||
bridge_ident = FwIdent(pack_version(version.split("-", 1)[0]), target_id, hw_id)
|
||||
lora_bridge, _ = ensure_endf(lora_bridge_body, bridge_ident)
|
||||
if max(len(wifi_bridge), len(lora_bridge)) > LEGACY_SLOT_BYTES:
|
||||
if len(wifi_bridge) > LEGACY_SLOT_BYTES:
|
||||
raise ValueError(f"{name}: bridge does not fit the 1.25 MiB legacy slot")
|
||||
if full_ident.fw_version != bridge_ident.fw_version:
|
||||
raise ValueError(f"{name}: bridge and Full image versions differ")
|
||||
|
||||
files = {
|
||||
"wifi-bridge.bin": wifi_bridge,
|
||||
"lora-bridge.mota": mota_full(lora_bridge, bridge_ident, 1024),
|
||||
"full-application.bin": full_image,
|
||||
"full-application.mota": mota_full(full_image, full_ident, 2048),
|
||||
"target-partitions.bin": table,
|
||||
"capabilities.json": capability_path.read_bytes(),
|
||||
}
|
||||
check_esp32_stage(files["lora-bridge.mota"], lora_bridge, LEGACY_SLOT_BYTES)
|
||||
check_esp32_stage(files["full-application.mota"], full_image, slot_bytes)
|
||||
files["README.md"] = readme(name, spec, version, source).encode()
|
||||
if (len(full_image) + 2047) // 2048 <= 1024:
|
||||
candidate_mota = mota_full(full_image, full_ident, 2048)
|
||||
try:
|
||||
check_esp32_stage(candidate_mota, full_image, slot_bytes)
|
||||
except ValueError:
|
||||
if spec["lora_bridge"]:
|
||||
raise
|
||||
else:
|
||||
files["full-application.mota"] = candidate_mota
|
||||
elif spec["lora_bridge"]:
|
||||
raise ValueError(f"{name}: LoRa Full image exceeds the mOTA block limit")
|
||||
if spec["lora_bridge"]:
|
||||
lora_bridge_body = (ROOT / ".pio" / "build" / spec["lora_bridge"] / "firmware.bin").read_bytes()
|
||||
lora_bridge, _ = ensure_endf(lora_bridge_body, bridge_ident)
|
||||
if len(lora_bridge) > LEGACY_SLOT_BYTES:
|
||||
raise ValueError(f"{name}: LoRa bridge does not fit the legacy slot")
|
||||
files["lora-bridge.mota"] = mota_full(lora_bridge, bridge_ident, 1024)
|
||||
check_esp32_stage(files["lora-bridge.mota"], lora_bridge, LEGACY_SLOT_BYTES)
|
||||
files["README.md"] = readme(name, spec, version, source,
|
||||
"full-application.mota" in files).encode()
|
||||
manifest = {
|
||||
"board": name,
|
||||
"role": spec["role"],
|
||||
"target": target,
|
||||
"target_id": f"0x{target_id:08x}",
|
||||
"hardware_id": hw_id,
|
||||
@@ -201,6 +478,7 @@ def package_board(name: str, spec: dict, build_dir: Path, output_dir: Path,
|
||||
"flash_bytes": spec["flash_bytes"],
|
||||
"legacy_slot_bytes": LEGACY_SLOT_BYTES,
|
||||
"expanded_slot_bytes": slot_bytes,
|
||||
"atomic_power_loss_recovery": False,
|
||||
"files": {filename: {"bytes": len(data), "sha256": sha256(data)}
|
||||
for filename, data in files.items()},
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace esp32_partition_migration {
|
||||
|
||||
constexpr uint32_t kRequiredFlashBytes = 16U * 1024U * 1024U;
|
||||
constexpr uint32_t kXiaoRequiredFlashBytes = 8U * 1024U * 1024U;
|
||||
constexpr uint32_t kSmallRequiredFlashBytes = 4U * 1024U * 1024U;
|
||||
constexpr uint32_t kPartitionTableAddress = 0x8000;
|
||||
constexpr uint32_t kPartitionTableBytes = 0x1000;
|
||||
constexpr uint32_t kSectorBytes = 0x1000;
|
||||
@@ -59,6 +60,17 @@ constexpr PartitionGeometry kExpanded8MBLayout = {
|
||||
0x670000, 0x180000,
|
||||
};
|
||||
|
||||
// variants/dual_ota_full_4MB.csv. This is the largest practical dual-OTA
|
||||
// layout on a 4 MiB board. It preserves only a 64 KiB SPIFFS region, so the
|
||||
// private identity must be staged in stable NVS before the table is changed.
|
||||
constexpr PartitionGeometry kExpanded4MBLayout = {
|
||||
0x9000, 0x5000,
|
||||
0xE000, 0x2000,
|
||||
0x10000, 0x1F0000,
|
||||
0x200000, 0x1F0000,
|
||||
0x3F0000, 0x10000,
|
||||
};
|
||||
|
||||
constexpr bool sameGeometry(const PartitionGeometry& left,
|
||||
const PartitionGeometry& right) {
|
||||
return left.nvs_address == right.nvs_address
|
||||
@@ -83,7 +95,10 @@ constexpr bool isExpandedLayout(const PartitionGeometry& geometry) {
|
||||
|
||||
constexpr bool canMigrate(uint32_t flash_bytes,
|
||||
const PartitionGeometry& geometry) {
|
||||
return flash_bytes >= kRequiredFlashBytes && isLegacyLayout(geometry);
|
||||
return (flash_bytes == kSmallRequiredFlashBytes
|
||||
|| flash_bytes == kXiaoRequiredFlashBytes
|
||||
|| flash_bytes == kRequiredFlashBytes)
|
||||
&& isLegacyLayout(geometry);
|
||||
}
|
||||
|
||||
// Binary prefix generated from Arduino-ESP32's default_16MB.csv by
|
||||
@@ -160,6 +175,29 @@ constexpr uint8_t kExpanded8MBPartitionTablePrefix[] = {
|
||||
constexpr size_t kExpanded8MBPartitionTablePrefixBytes =
|
||||
sizeof(kExpanded8MBPartitionTablePrefix);
|
||||
|
||||
// Generated from variants/dual_ota_full_4MB.csv with gen_esp32part.py.
|
||||
constexpr uint8_t kExpanded4MBPartitionTablePrefix[] = {
|
||||
0xAA, 0x50, 0x01, 0x02, 0x00, 0x90, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00,
|
||||
0x6E, 0x76, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0x50, 0x01, 0x00,
|
||||
0x00, 0xE0, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x6F, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xAA, 0x50, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x1F, 0x00, 0x61, 0x70, 0x70, 0x30, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x00, 0x11, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x1F, 0x00,
|
||||
0x61, 0x70, 0x70, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0x50, 0x01, 0x82,
|
||||
0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x01, 0x00, 0x73, 0x70, 0x69, 0x66,
|
||||
0x66, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xEB, 0xEB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xC5, 0x69, 0x36,
|
||||
0x65, 0xF8, 0xAB, 0x77, 0x12, 0x5A, 0xB8, 0x36, 0xBD, 0x27, 0xF1, 0x1F,
|
||||
};
|
||||
|
||||
constexpr size_t kExpanded4MBPartitionTablePrefixBytes =
|
||||
sizeof(kExpanded4MBPartitionTablePrefix);
|
||||
|
||||
struct TargetPlan {
|
||||
uint32_t flash_bytes;
|
||||
const char* name;
|
||||
@@ -174,6 +212,12 @@ constexpr TargetPlan kTarget8MB = {
|
||||
kExpanded8MBPartitionTablePrefixBytes,
|
||||
};
|
||||
|
||||
constexpr TargetPlan kTarget4MB = {
|
||||
kSmallRequiredFlashBytes, "MeshCore dual_ota_full_4MB",
|
||||
kExpanded4MBLayout, kExpanded4MBPartitionTablePrefix,
|
||||
kExpanded4MBPartitionTablePrefixBytes,
|
||||
};
|
||||
|
||||
constexpr TargetPlan kTarget16MB = {
|
||||
kRequiredFlashBytes, "Arduino default_16MB",
|
||||
kExpandedLayout, kExpandedPartitionTablePrefix,
|
||||
@@ -181,7 +225,8 @@ constexpr TargetPlan kTarget16MB = {
|
||||
};
|
||||
|
||||
constexpr const TargetPlan* targetForFlash(uint32_t flash_bytes) {
|
||||
return flash_bytes == kXiaoRequiredFlashBytes ? &kTarget8MB
|
||||
return flash_bytes == kSmallRequiredFlashBytes ? &kTarget4MB
|
||||
: flash_bytes == kXiaoRequiredFlashBytes ? &kTarget8MB
|
||||
: flash_bytes == kRequiredFlashBytes ? &kTarget16MB
|
||||
: nullptr;
|
||||
}
|
||||
@@ -273,9 +318,21 @@ static_assert(kExpanded8MBLayout.app0_address + kExpanded8MBLayout.app0_size
|
||||
static_assert(kExpanded8MBLayout.app1_address + kExpanded8MBLayout.app1_size
|
||||
== kExpanded8MBLayout.spiffs_address,
|
||||
"8 MiB expanded SPIFFS must follow both OTA slots");
|
||||
static_assert(kExpanded4MBLayout.app0_address + kExpanded4MBLayout.app0_size
|
||||
== kExpanded4MBLayout.app1_address,
|
||||
"4 MiB expanded OTA slots must be adjacent");
|
||||
static_assert(kExpanded4MBLayout.app1_address + kExpanded4MBLayout.app1_size
|
||||
== kExpanded4MBLayout.spiffs_address,
|
||||
"4 MiB expanded SPIFFS must follow both OTA slots");
|
||||
static_assert(kExpanded4MBLayout.spiffs_address + kExpanded4MBLayout.spiffs_size
|
||||
== kSmallRequiredFlashBytes,
|
||||
"4 MiB target partitions must fit physical flash");
|
||||
static_assert(kExpandedPartitionTablePrefixBytes
|
||||
== kExpanded8MBPartitionTablePrefixBytes,
|
||||
"target table buffers share one fixed DRAM size");
|
||||
static_assert(kExpanded4MBPartitionTablePrefixBytes
|
||||
<= kExpandedPartitionTablePrefixBytes,
|
||||
"4 MiB partition table must fit the shared DRAM buffer");
|
||||
|
||||
} // namespace esp32_partition_migration
|
||||
} // namespace mesh
|
||||
+20
-5
@@ -4,12 +4,16 @@
|
||||
|
||||
namespace policy = mesh::esp32_partition_migration;
|
||||
|
||||
TEST(Esp32PartitionMigrationPolicy, AcceptsOnlyTheKnownLegacyV4Layout) {
|
||||
TEST(Esp32PartitionMigrationPolicy, AcceptsOnlyKnownFlashSizesWithLegacyLayout) {
|
||||
EXPECT_TRUE(policy::isLegacyLayout(policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrate(policy::kSmallRequiredFlashBytes,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrate(policy::kXiaoRequiredFlashBytes,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrate(policy::kRequiredFlashBytes,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrate(policy::kRequiredFlashBytes + 1,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_FALSE(policy::canMigrate(policy::kRequiredFlashBytes + 1,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_FALSE(policy::canMigrate(policy::kRequiredFlashBytes - 1,
|
||||
policy::kLegacyLayout));
|
||||
|
||||
@@ -33,14 +37,19 @@ TEST(Esp32PartitionMigrationPolicy, ExpandedLayoutRetainsAndGrowsSpiffs) {
|
||||
policy::kExpandedLayout.spiffs_address);
|
||||
}
|
||||
|
||||
TEST(Esp32PartitionMigrationPolicy, GenericPlansAcceptStableEightAndSixteenMiBLayouts) {
|
||||
TEST(Esp32PartitionMigrationPolicy, GenericPlansAcceptStableFourEightAndSixteenMiBLayouts) {
|
||||
EXPECT_EQ(&policy::kTarget4MB, policy::targetForFlash(4U * 1024U * 1024U));
|
||||
EXPECT_EQ(&policy::kTarget8MB, policy::targetForFlash(8U * 1024U * 1024U));
|
||||
EXPECT_EQ(&policy::kTarget16MB, policy::targetForFlash(16U * 1024U * 1024U));
|
||||
EXPECT_EQ(nullptr, policy::targetForFlash(4U * 1024U * 1024U));
|
||||
EXPECT_EQ(nullptr, policy::targetForFlash(2U * 1024U * 1024U));
|
||||
EXPECT_TRUE(policy::isTargetLayout(4U * 1024U * 1024U,
|
||||
policy::kExpanded4MBLayout));
|
||||
EXPECT_TRUE(policy::isTargetLayout(8U * 1024U * 1024U,
|
||||
policy::kExpanded8MBLayout));
|
||||
EXPECT_TRUE(policy::canMigrateGeneric(8U * 1024U * 1024U,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrateGeneric(4U * 1024U * 1024U,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrateGeneric(16U * 1024U * 1024U,
|
||||
policy::kLegacyLayout));
|
||||
|
||||
@@ -77,6 +86,9 @@ TEST(Esp32PartitionMigrationPolicy, LoRaResumePreservesTheOldApplicationInEither
|
||||
policy::kLegacyLayout.app0_address).valid);
|
||||
EXPECT_FALSE(policy::planLoRaResume(policy::kLegacyLayout,
|
||||
policy::kExpandedLayout, 0x300000).valid);
|
||||
EXPECT_FALSE(policy::planLoRaResume(policy::kLegacyLayout,
|
||||
policy::kExpanded4MBLayout,
|
||||
policy::kLegacyLayout.app0_address).valid);
|
||||
}
|
||||
|
||||
TEST(Esp32PartitionMigrationPolicy, GeneratedTablePrefixContainsAllSixEntriesAndMd5) {
|
||||
@@ -92,6 +104,9 @@ TEST(Esp32PartitionMigrationPolicy, GeneratedTablePrefixContainsAllSixEntriesAnd
|
||||
EXPECT_EQ(0x33, policy::kExpanded8MBPartitionTablePrefix[0x4A]);
|
||||
EXPECT_EQ(0x34, policy::kExpanded8MBPartitionTablePrefix[0x66]);
|
||||
EXPECT_EQ(0x67, policy::kExpanded8MBPartitionTablePrefix[0x86]);
|
||||
EXPECT_EQ(0xC0U, policy::kExpanded4MBPartitionTablePrefixBytes);
|
||||
EXPECT_EQ(0xEB, policy::kExpanded4MBPartitionTablePrefix[0xA0]);
|
||||
EXPECT_EQ(0xEB, policy::kExpanded4MBPartitionTablePrefix[0xA1]);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -16,9 +17,39 @@ sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from build_esp32_partition_migration import ( # noqa: E402
|
||||
build_steps, bundle_release, verify_archive,
|
||||
)
|
||||
from package_esp32_partition_migration import BOARDS, readme # noqa: E402
|
||||
|
||||
|
||||
class Esp32MigrationRecipeTest(unittest.TestCase):
|
||||
def test_catalog_covers_all_three_roles_and_exact_flash_plans(self):
|
||||
self.assertEqual({"repeater", "room-server", "sensor"},
|
||||
{spec["role"] for spec in BOARDS.values()})
|
||||
self.assertIn("heltec-v4-sensor", BOARDS)
|
||||
self.assertIn("xiao-s3-wio-sensor", BOARDS)
|
||||
self.assertIn("heltec-ct62-sensor", BOARDS)
|
||||
self.assertIn("nibble-zero-room-server", BOARDS)
|
||||
self.assertEqual("esp32_c3_4mb_partition_migrator",
|
||||
BOARDS["heltec-ct62-sensor"]["wifi_bridge"])
|
||||
self.assertEqual("esp32_4mb_partition_migrator",
|
||||
BOARDS["generic-e22-sx1262-repeater"]["wifi_bridge"])
|
||||
self.assertEqual("esp32_s3_8mb_partition_migrator",
|
||||
BOARDS["heltec-v3-sensor"]["wifi_bridge"])
|
||||
self.assertEqual("esp32_8mb_partition_migrator",
|
||||
BOARDS["heltec-v2-room-server"]["wifi_bridge"])
|
||||
expected_slots = {4 * 1024 * 1024: 0x1F0000,
|
||||
8 * 1024 * 1024: 0x330000,
|
||||
16 * 1024 * 1024: 0x640000}
|
||||
env_names = set()
|
||||
for config in (ROOT / "variants").glob("*/platformio.ini"):
|
||||
env_names.update(re.findall(r"^\[env:([^]]+)\]",
|
||||
config.read_text(), re.MULTILINE))
|
||||
for board, spec in BOARDS.items():
|
||||
with self.subTest(board=board):
|
||||
self.assertIn(spec["target"], env_names)
|
||||
self.assertIn(spec["wifi_bridge"], env_names)
|
||||
self.assertEqual(expected_slots[spec["flash_bytes"]],
|
||||
spec["slot_bytes"])
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "POSIX shell menu runs in WSL/Linux")
|
||||
def test_shell_menu_and_noninteractive_selection(self):
|
||||
script = str(ROOT / "scripts/build_esp32_partition_migration.sh")
|
||||
@@ -28,11 +59,18 @@ class Esp32MigrationRecipeTest(unittest.TestCase):
|
||||
cwd=ROOT, text=True, capture_output=True, check=True)
|
||||
self.assertIn("build-firmware heltec_v4_repeater", one.stdout)
|
||||
self.assertNotIn("build-firmware Xiao_S3_WIO_repeater", one.stdout)
|
||||
self.assertNotIn("Bundle complete qualified set", one.stdout)
|
||||
self.assertNotIn("Bundle all configured board/role recipes", one.stdout)
|
||||
all_boards = subprocess.run(["sh", script, "--all", *common],
|
||||
cwd=ROOT, text=True, capture_output=True, check=True)
|
||||
self.assertIn("build-firmware Xiao_S3_WIO_repeater", all_boards.stdout)
|
||||
self.assertIn("Bundle complete qualified set", all_boards.stdout)
|
||||
self.assertIn("build-firmware ThinkNode_M2_Repeater", all_boards.stdout)
|
||||
self.assertIn("Bundle all configured board/role recipes", all_boards.stdout)
|
||||
sensors = subprocess.run(["sh", script, "--role", "sensor", *common],
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
check=True)
|
||||
self.assertIn("build-firmware Xiao_S3_WIO_sensor", sensors.stdout)
|
||||
self.assertNotIn("build-firmware Xiao_S3_WIO_repeater", sensors.stdout)
|
||||
self.assertNotIn("Bundle all configured board/role recipes", sensors.stdout)
|
||||
interactive = subprocess.run(["sh", script], input="q\n",
|
||||
cwd=ROOT, text=True, capture_output=True,
|
||||
check=True)
|
||||
@@ -40,7 +78,7 @@ class Esp32MigrationRecipeTest(unittest.TestCase):
|
||||
invalid = subprocess.run(["sh", script, "--board", "unknown", *common],
|
||||
cwd=ROOT, text=True, capture_output=True)
|
||||
self.assertNotEqual(0, invalid.returncode)
|
||||
self.assertIn("not qualified", invalid.stderr)
|
||||
self.assertIn("not configured", invalid.stderr)
|
||||
|
||||
def test_full_images_precede_every_bridge_and_builds_are_serial(self):
|
||||
steps = build_steps(["heltec-v4", "xiao-s3-wio"], "v1.17.1.7-test",
|
||||
@@ -58,6 +96,31 @@ class Esp32MigrationRecipeTest(unittest.TestCase):
|
||||
"xiao_s3_partition_migrator_lora_repeater",
|
||||
], [command[3] for command, _ in steps[2:]])
|
||||
|
||||
def test_wifi_only_4mb_roles_share_one_bridge(self):
|
||||
steps = build_steps(["thinknode-m2-repeater", "thinknode-m2-room-server"],
|
||||
"v1.17.1.7-test", "usa-cascadia", "cascade", 4)
|
||||
self.assertEqual([True, True, False], [full for _, full in steps])
|
||||
self.assertEqual("ThinkNode_M2_Repeater", steps[0][0][3])
|
||||
self.assertEqual("ThinkNode_M2_room_server", steps[1][0][3])
|
||||
self.assertEqual("esp32_s3_4mb_partition_migrator", steps[2][0][3])
|
||||
guide = readme("thinknode-m2-repeater", BOARDS["thinknode-m2-repeater"],
|
||||
"v1.17.1.7-test", "01234567")
|
||||
self.assertIn("no LoRa bridge", guide)
|
||||
self.assertIn("power loss during", guide)
|
||||
self.assertIn("exact\n `Started:` URL", guide)
|
||||
self.assertNotIn("ota pull <id>", guide)
|
||||
large_guide = readme("heltec-v3-sensor", BOARDS["heltec-v3-sensor"],
|
||||
"v1.17.1.7-test", "01234567", has_full_mota=False)
|
||||
self.assertIn("does not include `full-application.mota`", large_guide)
|
||||
|
||||
def test_sensor_and_room_roles_share_the_8mb_bridge(self):
|
||||
steps = build_steps(["xiao-s3-wio-sensor", "xiao-s3-wio-room-server"],
|
||||
"v1.17.1.7-test", "usa-cascadia", "cascade", 4)
|
||||
self.assertEqual([True, True, False], [full for _, full in steps])
|
||||
self.assertEqual("Xiao_S3_WIO_sensor", steps[0][0][3])
|
||||
self.assertEqual("Xiao_S3_WIO_room_server", steps[1][0][3])
|
||||
self.assertEqual("esp32_s3_8mb_partition_migrator", steps[2][0][3])
|
||||
|
||||
def test_dry_run_has_no_build_side_effects(self):
|
||||
with tempfile.TemporaryDirectory(prefix="esp32-migration-plan-") as temporary:
|
||||
output_root = Path(temporary) / "release"
|
||||
@@ -102,7 +165,9 @@ class Esp32MigrationRecipeTest(unittest.TestCase):
|
||||
package_dir.mkdir()
|
||||
version = "v1.17.1.7-test"
|
||||
source = "01234567"
|
||||
for board in ("heltec-v4", "xiao-s3-wio"):
|
||||
sample_boards = ["heltec-v4", "xiao-s3-wio-room-server",
|
||||
"heltec-ct62-sensor"]
|
||||
for board in sample_boards:
|
||||
payload = (board + " image").encode()
|
||||
manifest = {
|
||||
"board": board, "firmware_version": version,
|
||||
@@ -118,24 +183,32 @@ class Esp32MigrationRecipeTest(unittest.TestCase):
|
||||
archive.writestr("manifest.json", json.dumps(manifest))
|
||||
archive.writestr("full-application.bin", payload)
|
||||
release = bundle_release(output_root, package_dir,
|
||||
["heltec-v4", "xiao-s3-wio"],
|
||||
sample_boards,
|
||||
version, source, "usa-cascadia", "cascade")
|
||||
with zipfile.ZipFile(release) as archive:
|
||||
self.assertIsNone(archive.testzip())
|
||||
release_manifest = json.loads(archive.read("release-manifest.json"))
|
||||
self.assertEqual(["heltec-v4", "xiao-s3-wio"],
|
||||
self.assertEqual(sample_boards,
|
||||
release_manifest["boards"])
|
||||
self.assertEqual(["repeater", "room-server", "sensor"],
|
||||
release_manifest["roles"])
|
||||
self.assertEqual("repeater",
|
||||
release_manifest["board_roles"]["heltec-v4"])
|
||||
self.assertEqual("room-server", release_manifest["board_roles"][
|
||||
"xiao-s3-wio-room-server"])
|
||||
self.assertEqual("sensor", release_manifest["board_roles"][
|
||||
"heltec-ct62-sensor"])
|
||||
for name, checks in release_manifest["packages"].items():
|
||||
data = archive.read(name)
|
||||
self.assertEqual(checks["bytes"], len(data))
|
||||
self.assertEqual(checks["sha256"], hashlib.sha256(data).hexdigest())
|
||||
self.assertEqual(release, bundle_release(
|
||||
output_root, package_dir, ["heltec-v4", "xiao-s3-wio"],
|
||||
output_root, package_dir, sample_boards,
|
||||
version, source, "usa-cascadia", "cascade"))
|
||||
(package_dir / f"xiao-s3-wio-{version}-{source}-migration.zip").unlink()
|
||||
with self.assertRaisesRegex(ValueError, "missing xiao-s3-wio"):
|
||||
(package_dir / f"heltec-ct62-sensor-{version}-{source}-migration.zip").unlink()
|
||||
with self.assertRaisesRegex(ValueError, "missing heltec-ct62-sensor"):
|
||||
bundle_release(output_root, package_dir,
|
||||
["heltec-v4", "xiao-s3-wio"],
|
||||
sample_boards,
|
||||
version, source, "usa-cascadia", "cascade")
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ XIAO_PROFILE = ROOT / "variants/xiao_s3_wio/platformio.ini"
|
||||
XIAO_MIGRATOR_BOARD = ROOT / "boards/seeed_xiao_esp32s3_migrator.json"
|
||||
PARTITIONS = Path.home() / ".platformio/packages/framework-arduinoespressif32/tools/partitions/default_16MB.csv"
|
||||
PARTITIONS_8MB = Path.home() / ".platformio/packages/framework-arduinoespressif32/tools/partitions/default_8MB.csv"
|
||||
PARTITIONS_4MB = ROOT / "variants/dual_ota_full_4MB.csv"
|
||||
GENERATOR = Path.home() / ".platformio/packages/framework-arduinoespressif32/tools/gen_esp32part.py"
|
||||
|
||||
|
||||
@@ -98,6 +99,15 @@ class HeltecV4PartitionMigratorTest(unittest.TestCase):
|
||||
embedded_8mb = bytes(int(value, 16)
|
||||
for value in re.findall(r"0x([0-9A-F]{2})", block_8mb))
|
||||
self.assertEqual(embedded_8mb, generated_8mb.read_bytes()[:len(embedded_8mb)])
|
||||
generated_4mb = Path(directory) / "default4.bin"
|
||||
subprocess.run([sys.executable, str(GENERATOR), str(PARTITIONS_4MB),
|
||||
str(generated_4mb)], check=True, capture_output=True, text=True)
|
||||
start_4mb = text.index("kExpanded4MBPartitionTablePrefix[]")
|
||||
block_4mb = text[start_4mb:text.index("};", start_4mb)]
|
||||
embedded_4mb = bytes(int(value, 16)
|
||||
for value in re.findall(r"0x([0-9A-F]{2})", block_4mb))
|
||||
self.assertEqual(0xC0, len(embedded_4mb))
|
||||
self.assertEqual(embedded_4mb, generated_4mb.read_bytes()[:len(embedded_4mb)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
; Board-independent ESP32-S3 Wi-Fi bridge for 4 MiB dual-OTA nodes. This
|
||||
; utility uses Wi-Fi, NVS and SPIFFS only; no board GPIO or PSRAM is needed.
|
||||
; DIO and disabled USB CDC minimize board-specific boot assumptions.
|
||||
[env:esp32_s3_4mb_partition_migrator]
|
||||
platform = platformio/espressif32@6.11.0
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
board_build.partitions = default.csv
|
||||
board_build.flash_mode = dio
|
||||
board_upload.flash_size = 4MB
|
||||
board_upload.maximum_size = 4194304
|
||||
extra_scripts = post:scripts/check_firmware_ram.py
|
||||
build_flags =
|
||||
-D ESP32_PLATFORM
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0
|
||||
-D MESH_MIN_RUNTIME_HEAP=100352
|
||||
-I src
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<../examples/esp32_partition_migrator>
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP @ 3.5.0
|
||||
ESP32Async/ESPAsyncWebServer @ 3.12.0
|
||||
file://arch/esp32/AsyncElegantOTA
|
||||
|
||||
; The same board-independent bridge with a 16 MiB app image header. The
|
||||
; source updater still sees Arduino default.csv's 1.25 MiB slots.
|
||||
[env:esp32_s3_16mb_partition_migrator]
|
||||
extends = env:esp32_s3_4mb_partition_migrator
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
|
||||
[env:esp32_s3_8mb_partition_migrator]
|
||||
extends = env:esp32_s3_4mb_partition_migrator
|
||||
board_upload.flash_size = 8MB
|
||||
board_upload.maximum_size = 8388608
|
||||
|
||||
; Heltec V2 and other first-generation ESP32 (non-S3) 8 MiB boards need a
|
||||
; matching chip-family image. The migration logic still verifies the live
|
||||
; partition table and flash size before touching flash.
|
||||
[env:esp32_8mb_partition_migrator]
|
||||
extends = env:esp32_s3_8mb_partition_migrator
|
||||
board = esp32dev
|
||||
|
||||
; The 4 MiB C3 bridge covers historical default.csv images such as the
|
||||
; Heltec CT62. It never assumes the old app slots are exactly 1.25 MiB: the
|
||||
; live table must still pass the common geometry and identity checks.
|
||||
[env:esp32_c3_4mb_partition_migrator]
|
||||
extends = env:esp32_s3_4mb_partition_migrator
|
||||
board = esp32-c3-devkitm-1
|
||||
|
||||
[env:esp32_4mb_partition_migrator]
|
||||
extends = env:esp32_s3_4mb_partition_migrator
|
||||
board = esp32dev
|
||||
Reference in new issue
Block a user