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
@@ -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()},
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user