diff --git a/build.sh b/build.sh index c5e11c32..773757fb 100755 --- a/build.sh +++ b/build.sh @@ -2387,23 +2387,25 @@ get_unified_full_infrastructure_target() { return 1 } -# A same-partition FULL release keeps the logical target's mOTA identity while -# it may compile the matching observer recipe for its MQTT/TLS/ESP-NOW sources. -# That lets a deployed ordinary role update to the complete image without -# treating an observer environment name as a different target. If a board has -# no matching observer recipe, its own source tree remains the Full build base. +# A canonical FULL release keeps the logical target's mOTA identity while it +# may compile the matching observer recipe for its MQTT/TLS/ESP-NOW sources. +# Same-partition targets can receive that image through mOTA. Partition- +# migration targets require their merged image to be flashed once first, but +# then use this same normal-target identity for every later FULL update. If a +# board has no matching observer recipe, its own source tree remains the Full +# build base. get_exact_identity_full_pio_env() { local env_name=$1 local candidate="" - is_esp32_full_only_bulk_target "$env_name" || { + is_esp32_canonical_full_release_target "$env_name" || { printf '%s\n' "$env_name" return 0 } candidate=$(get_unified_full_infrastructure_target "$env_name") || candidate="" if [ -n "$candidate" ] \ - && is_esp32_full_only_bulk_target "$candidate" \ + && supports_esp32_full_build "$candidate" \ && [ "${PIO_ENV_BOARD_BY_NAME[$candidate]:-}" = "${PIO_ENV_BOARD_BY_NAME[$env_name]:-}" ]; then printf '%s\n' "$candidate" else @@ -3056,6 +3058,34 @@ is_esp32_full_only_bulk_target() { return 1 } +# 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 +# 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. +is_esp32_partition_migration_full_target() { + local env_name=${1,,} + + [ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "ESP32_PLATFORM" ] || return 1 + + case "$env_name" in + 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) + return 0 + ;; + esac + return 1 +} + +is_esp32_canonical_full_release_target() { + is_esp32_full_only_bulk_target "$1" \ + || is_esp32_partition_migration_full_target "$1" +} + requires_esp32_full_cli_profile() { local env_name=$1 @@ -3116,6 +3146,15 @@ declare_build_capability_contract() { declare_full_logging_application_contract "$env_name" record_build_capability "profile.${BUILD_PROFILE_FOR_TARGET}" + if [ "$env_platform" = "ESP32_PLATFORM" ] \ + && [ "$BUILD_PROFILE_FOR_TARGET" = "full" ]; then + 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" + fi + fi + if [ "$env_platform" = "NRF52_PLATFORM" ] && ! is_kiss_modem_target "$env_name"; then # Prove the real DFU service is linked, not merely a generic OTA CLI stub. record_build_expectation "ota.update.bluetooth" "_ZN6BLEDfu5beginEv" @@ -4369,10 +4408,14 @@ build_firmware() { fi if [ "$ESP32_FULL_BUILD" = "1" ] \ - && is_esp32_full_only_bulk_target "$env_name"; then + && is_esp32_canonical_full_release_target "$env_name"; then exact_identity_full_pio_env=$(get_exact_identity_full_pio_env "$env_name") if [ "$exact_identity_full_pio_env" != "$pio_env_name" ]; then - echo "FULL ${env_name} retains its mOTA identity while compiling observer feature base ${exact_identity_full_pio_env}." + if is_esp32_partition_migration_full_target "$env_name"; then + echo "Partition-migration FULL ${env_name} retains its mOTA identity while compiling observer feature base ${exact_identity_full_pio_env}." + else + echo "FULL ${env_name} retains its mOTA identity while compiling observer feature base ${exact_identity_full_pio_env}." + fi pio_env_name=$exact_identity_full_pio_env fi fi @@ -5690,6 +5733,14 @@ run_full_esp32_profile() { if [ -z "$full_target" ] || ! supports_esp32_full_build "$full_target"; then continue fi + # The canonical normal-target FULL artifact was already emitted by the + # partition-migration pass. Do not rebuild it as an identical fallback. + # Observer identities are intentionally not skipped: existing observer + # nodes still need their exact legacy mOTA asset during the transition. + if [ "${PARTITION_MIGRATION_FULL_PROFILE_ACTIVE:-0}" = "1" ] \ + && is_esp32_partition_migration_full_target "$full_target"; then + continue + fi # The logging matrix has already emitted these approved targets under # their exact environment names. Do not replace that identity with a # generic MQTT/unified sibling in this legacy resolution pass. @@ -5777,6 +5828,60 @@ run_full_only_esp32_profile() { return 0 } +run_partition_migration_full_esp32_profile() { + local targets=("$@") + local target + local pass_status=0 + local build_status=0 + local BUILD_PROFILE_EFFECTIVE=full + local -a migration_targets=() + local -A seen_migration_targets=() + local original_meshdebug_override=$MESHDEBUG_OVERRIDE + local original_packet_logging_override=$PACKET_LOGGING_OVERRIDE + local original_mqtt_bridge_override=$MQTT_BRIDGE_OVERRIDE + local original_mqtt_debug_override=$MQTT_DEBUG_OVERRIDE + local original_firmware_filename_infix=$FIRMWARE_FILENAME_INFIX + local original_esp32_full_build=$ESP32_FULL_BUILD + + for target in "${targets[@]}"; do + if ! is_esp32_partition_migration_full_target "$target" \ + || [ -n "${seen_migration_targets[$target]+x}" ]; then + continue + fi + migration_targets+=("$target") + seen_migration_targets["$target"]=1 + done + + if [ ${#migration_targets[@]} -eq 0 ]; then + return 0 + fi + + echo "Partition-migration FULL pass: building ${#migration_targets[@]} canonical ESP32 target(s)." + echo "Each normal-target FULL artifact keeps its mOTA identity after its matching merged image is flashed once; legacy 1.25 MiB artifacts remain published during the transition." + MESHDEBUG_OVERRIDE=off + PACKET_LOGGING_OVERRIDE=on + MQTT_BRIDGE_OVERRIDE=off + MQTT_DEBUG_OVERRIDE=off + FIRMWARE_FILENAME_INFIX=full-logging + ESP32_FULL_BUILD=1 + + run_logged_build_targets "${migration_targets[@]}" + pass_status=$? + if [ "$pass_status" -eq 130 ]; then + build_status=130 + elif [ "$pass_status" -ne 0 ]; then + build_status=1 + fi + + MESHDEBUG_OVERRIDE=$original_meshdebug_override + PACKET_LOGGING_OVERRIDE=$original_packet_logging_override + MQTT_BRIDGE_OVERRIDE=$original_mqtt_bridge_override + MQTT_DEBUG_OVERRIDE=$original_mqtt_debug_override + FIRMWARE_FILENAME_INFIX=$original_firmware_filename_infix + ESP32_FULL_BUILD=$original_esp32_full_build + return "$build_status" +} + run_full_esp32_build_targets() { local profile_mode=$1 shift @@ -5831,12 +5936,14 @@ run_logging_matrix_build_targets() { local original_profile_build_workers=$PROFILE_BUILD_WORKERS local full_only_standard_skip_count=0 local full_only_exact_count=0 + local partition_migration_full_count=0 local merged_usb_logging_count=0 local constrained_merged_logging_count=0 local build_status=0 local pass_status=0 local DEFER_ESP32_PORTABLE_OVERFLOW_TO_FULL=0 local FULL_ONLY_EXACT_PROFILE_ACTIVE=0 + local PARTITION_MIGRATION_FULL_PROFILE_ACTIVE=0 if [ ${#targets[@]} -eq 0 ]; then echo "No build targets resolved." @@ -5889,6 +5996,14 @@ run_logging_matrix_build_targets() { if [ "$full_only_exact_count" -gt 0 ]; then echo "Publishing ${full_only_exact_count} audited ESP32 target(s) as their exact-identity FULL release only; explicit --standard remains available for recovery." fi + for target in "${targets[@]}"; do + if is_esp32_partition_migration_full_target "$target"; then + partition_migration_full_count=$((partition_migration_full_count + 1)) + fi + done + if [ "$partition_migration_full_count" -gt 0 ]; then + echo "Publishing ${partition_migration_full_count} canonical ESP32 FULL migration target(s) alongside their legacy portable artifacts; flash each matching merged image once before mOTA can use FULL." + fi ESP32_FULL_BUILD=0 MESHDEBUG_OVERRIDE="" PACKET_LOGGING_OVERRIDE="" @@ -5908,6 +6023,12 @@ run_logging_matrix_build_targets() { if [ "$pass_status" -eq 130 ]; then return 130; fi if [ "$pass_status" -ne 0 ]; then build_status=1; fi + run_partition_migration_full_esp32_profile "${targets[@]}" + pass_status=$? + if [ "$pass_status" -eq 130 ]; then return 130; fi + if [ "$pass_status" -ne 0 ]; then build_status=1; fi + PARTITION_MIGRATION_FULL_PROFILE_ACTIVE=1 + FULL_ONLY_EXACT_PROFILE_ACTIVE=1 run_full_esp32_profile "FULL unified pass" "unified" "${targets[@]}" pass_status=$? diff --git a/docs/cli_build_matrix.md b/docs/cli_build_matrix.md index 44f2628a..d68c6bf2 100644 --- a/docs/cli_build_matrix.md +++ b/docs/cli_build_matrix.md @@ -55,6 +55,11 @@ portable application slot, remain available and remain in the standard matrix pass. Select `--build-profile standard` to retain the original target and partition contract. Moving from a plain target to its MQTT sibling changes the LoRa OTA target identity; existing nodes need an appropriate wired migration. +For the normal-role partition migrations, the matrix also publishes a +canonical Full image under the normal target identity. Install its matching +merged image over USB once; mOTA rejects the incompatible partition signature +until that migration is complete, then later Full updates use the normal +target ID. The portable image remains available while deployed nodes move. The default `cascade` settings profile enables device and LoRa RX power saving; saved preferences take precedence, and active USB/network services can prevent device sleep. `--profile default` instead uses the upstream power-saving defaults. diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 85e38769..a7a56b78 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -420,6 +420,17 @@ The former observer-named Full image remains available through exact filename 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. + ## Maintaining the runtime directions The online picker and downloadable HTML use the same command renderer. diff --git a/test/test_full_infrastructure_selection.sh b/test/test_full_infrastructure_selection.sh index eab9dc5a..38558ec6 100644 --- a/test/test_full_infrastructure_selection.sh +++ b/test/test_full_infrastructure_selection.sh @@ -65,12 +65,35 @@ for target in LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_ \ fi done +# Partition-changing normal roles publish a canonical Full image alongside +# their portable compatibility image. They must never be mistaken for the +# same-partition policy, which is the only policy allowed to install through +# the automatic mOTA target alias. +for target in 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; do + is_esp32_partition_migration_full_target "$target" \ + || fail "missed partition-migration Full target $target" + if is_esp32_full_only_bulk_target "$target"; then + fail "partition-migration target was incorrectly made FULL-only: $target" + fi +done + [ "$(get_exact_identity_full_pio_env heltec_v4_r8_repeater)" \ = heltec_v4_r8_repeater_observer_mqtt ] \ || fail "V4 R8 Full lost its observer feature base" [ "$(get_exact_identity_full_pio_env heltec_v4_r8_sensor)" \ = heltec_v4_r8_sensor ] \ || fail "V4 R8 sensor should retain its own feature base" +[ "$(get_exact_identity_full_pio_env Station_G2_repeater)" \ + = Station_G2_repeater_observer_mqtt ] \ + || fail "Station G2 migration Full lost its observer feature base" +[ "$(get_exact_identity_full_pio_env LilyGo_T3S3_sx1262_room_server)" \ + = LilyGo_T3S3_sx1262_room_server_observer_mqtt ] \ + || fail "T3S3 migration Full lost its observer feature base" [ "$(get_exact_identity_full_migration_target heltec_v4_r8_repeater_observer_mqtt)" \ = heltec_v4_r8_repeater ] \ || fail "legacy V4 R8 observer lacks its canonical migration target" @@ -80,6 +103,9 @@ fi if get_exact_identity_full_migration_target heltec_v4_r8_repeater_observer_mqtt_sim >/dev/null; then fail "simulated observer target unexpectedly received a migration alias" fi +if get_exact_identity_full_migration_target Station_G2_repeater_observer_mqtt >/dev/null; then + fail "partition-changing Station G2 observer unexpectedly received an mOTA alias" +fi # Portable images and exact reduced OTA identities must not be consolidated. for target in heltec_v4_r8_repeater heltec_v4_r8_room_server \ @@ -231,6 +257,18 @@ done || fail "matrix did not build exact FULL-only identities" ) +( + calls=() + run_logged_build_targets() { + calls+=("$BUILD_PROFILE_EFFECTIVE:$ESP32_FULL_BUILD:$FIRMWARE_FILENAME_INFIX:$*") + } + run_partition_migration_full_esp32_profile \ + Station_G2_repeater Station_G2_room_server Heltec_v3_repeater >/dev/null + [ "${#calls[@]}" -eq 1 ] || fail "partition-migration pass emitted the wrong number of builds" + [ "${calls[0]}" = 'full:1:full-logging:Station_G2_repeater Station_G2_room_server' ] \ + || fail "partition-migration pass lost canonical normal identities" +) + # The default build settings retain power saving; this does not overwrite # preferences already stored on a device. [ "$FIRMWARE_PROFILE_OVERRIDE" = cascade ] || fail "default profile changed"