diff --git a/build.sh b/build.sh index c41080f74..b15ea2ab5 100755 --- a/build.sh +++ b/build.sh @@ -2956,6 +2956,37 @@ supports_esp32_full_build() { && ! is_lora_ota_only_target "$env_name" } +# These targets have an exact FULL partition identity and were audited to have +# no target-specific contact, neighbour, channel, or queue capacity reduction. +# In bulk release builds their portable image is therefore redundant: publish +# the FULL image under the same environment identity instead. Do not add an +# environment here unless both its partition history and capacity limits have +# been reviewed. An explicit `--standard` build remains the recovery route. +is_esp32_full_only_bulk_target() { + local env_name=${1,,} + + [ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "ESP32_PLATFORM" ] || return 1 + + case "$env_name" in + heltec_rc32_repeater|heltec_rc32_repeater_bridge_espnow|heltec_rc32_room_server|heltec_rc32_sensor|heltec_rc32_companion_radio_full|\ + heltec_rc32_without_display_repeater|heltec_rc32_without_display_repeater_bridge_espnow|heltec_rc32_without_display_room_server|heltec_rc32_without_display_sensor|heltec_rc32_without_display_companion_radio_full|\ + thinknode_m9_repeater_|thinknode_m9_room_server_|thinknode_m9_companion_radio_full_|\ + station_g3_esp32_repeater|station_g3_esp32_logging_repeater|station_g3_esp32_room_server|station_g3_esp32_repeater_observer_mqtt|station_g3_esp32_room_server_observer_mqtt|station_g3_esp32_companion_radio_full|\ + lilygo_teth_elite_sx1262_repeater|lilygo_teth_elite_sx1262_room_server|lilygo_teth_elite_sx1262_companion_radio_full|\ + station_g2_repeater_observer_mqtt|station_g2_room_server_observer_mqtt|\ + sensecapindicator-lora-n16r2_companion_radio_full|\ + t_beam_s3_supreme_sx1262_repeater|t_beam_s3_supreme_sx1262_repeater_bridge_espnow|t_beam_s3_supreme_sx1262_repeater_observer_mqtt|t_beam_s3_supreme_sx1262_room_server|t_beam_s3_supreme_sx1262_room_server_observer_mqtt|t_beam_s3_supreme_sx1262_companion_radio_full|\ + thinknode_m7_repeater|thinknode_m7_room_server|thinknode_m7_repeater_observer_mqtt|thinknode_m7_room_server_observer_mqtt|thinknode_m7_companion_radio_full|\ + why2025_badge_repeater_|why2025_badge_companion_radio_full_|\ + sensecapindicator-lora_companion_radio_full|\ + m5stack_unit_c6l_companion_radio_full|lilygo_tlora_c6_companion_radio_full_|meshimi_companion_radio_full_|xiao_c6_companion_radio_full_|\ + lilygo_tlora_v2_1_1_6_repeater|lilygo_tlora_v2_1_1_6_room_server|lilygo_tlora_v2_1_1_6_repeater_bridge_rs232|lilygo_tlora_v2_1_1_6_repeater_bridge_espnow) + return 0 + ;; + esac + return 1 +} + requires_esp32_full_cli_profile() { local env_name=$1 @@ -4229,6 +4260,26 @@ build_firmware() { echo "Complete OTA pass uses feature-rich base environment ${pio_env_name} with stable target identity ${env_name}." fi + # Canonical bulk releases omit the redundant portable image for targets + # whose exact environment identity has been approved for FULL-only output. + # Keep an explicit standard request untouched so maintainers retain a + # portable recovery build when diagnosing field devices. + if [ "$ESP32_FULL_BUILD" != "1" ] \ + && [ "$BUILD_PROFILE_FOR_TARGET" = "standard" ] \ + && [ "${BATCH_BUILD_MODE:-0}" = "1" ] \ + && [ "${BUILD_PROFILE_EXPLICIT:-0}" != "1" ] \ + && is_esp32_full_only_bulk_target "$env_name"; then + ESP32_FULL_BUILD=1 + if ! is_companion_radio_full_target "$env_name"; then + MESHDEBUG_OVERRIDE="off" + PACKET_LOGGING_OVERRIDE="on" + MQTT_BRIDGE_OVERRIDE="off" + MQTT_DEBUG_OVERRIDE="off" + FIRMWARE_FILENAME_INFIX="full-logging" + fi + echo "Promoting ${env_name} to its canonical exact-identity FULL release image." + fi + if [ "$ESP32_FULL_BUILD" != "1" ] \ && requires_esp32_full_cli_profile "$env_name"; then if [ "$BUILD_PROFILE_EXPLICIT" = "1" ] \ @@ -5546,6 +5597,13 @@ run_full_esp32_profile() { if [ -z "$full_target" ] || ! supports_esp32_full_build "$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. + if [ "${FULL_ONLY_EXACT_PROFILE_ACTIVE:-0}" = "1" ] \ + && is_esp32_full_only_bulk_target "$full_target"; then + continue + fi if [ -z "${seen_full_targets[$full_target]+x}" ]; then full_targets+=("$full_target") seen_full_targets["$full_target"]=1 @@ -5598,6 +5656,34 @@ run_full_esp32_profile() { return "$build_status" } +run_full_only_esp32_profile() { + local targets=("$@") + local pass_status=0 + local BUILD_PROFILE_EFFECTIVE=full + local MESHDEBUG_OVERRIDE=off + local PACKET_LOGGING_OVERRIDE=on + local MQTT_BRIDGE_OVERRIDE=off + local MQTT_DEBUG_OVERRIDE=off + local FIRMWARE_FILENAME_INFIX=full-logging + local ESP32_FULL_BUILD=1 + + if [ ${#targets[@]} -eq 0 ]; then + return 0 + fi + + echo "FULL-only exact-identity pass: building ${#targets[@]} audited ESP32 target(s) with expanded dual-OTA partitions and USB packet logging." + echo "These artifacts retain each target's own mOTA identity and use filename form: name-full-logging-ota-version." + run_logged_build_targets "${targets[@]}" + pass_status=$? + if [ "$pass_status" -eq 130 ]; then + return 130 + fi + if [ "$pass_status" -ne 0 ]; then + return 1 + fi + return 0 +} + run_full_esp32_build_targets() { local profile_mode=$1 shift @@ -5642,6 +5728,7 @@ run_logging_matrix_build_targets() { local targets=("$@") local target local standard_targets=() + local full_only_targets=() local original_meshdebug_override=$MESHDEBUG_OVERRIDE local original_packet_logging_override=$PACKET_LOGGING_OVERRIDE local original_mqtt_bridge_override=$MQTT_BRIDGE_OVERRIDE @@ -5650,11 +5737,13 @@ run_logging_matrix_build_targets() { local original_esp32_full_build=$ESP32_FULL_BUILD local original_profile_build_workers=$PROFILE_BUILD_WORKERS local full_only_standard_skip_count=0 + local full_only_exact_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 if [ ${#targets[@]} -eq 0 ]; then echo "No build targets resolved." @@ -5669,6 +5758,18 @@ run_logging_matrix_build_targets() { echo "Option 3 PlatformIO policy: one target build at a time, ${OPTION3_PIO_JOBS} compiler job(s) inside that process." for target in "${targets[@]}"; do + if is_esp32_full_only_bulk_target "$target"; then + full_only_exact_count=$((full_only_exact_count + 1)) + # Full companion environments already select their complete profile in + # build_firmware, so keep them in the ordinary pass to preserve their + # established companion-specific recipe. + if is_companion_radio_full_target "$target"; then + standard_targets+=("$target") + else + full_only_targets+=("$target") + fi + continue + fi if is_mqtt_bridge_target "$target"; then continue fi @@ -5692,6 +5793,9 @@ run_logging_matrix_build_targets() { if [ "$full_only_standard_skip_count" -gt 0 ]; then echo "Deferring ${full_only_standard_skip_count} ESP32 ESP-NOW target(s) to their FULL logging fallback; its persistent USB gate also provides normal output-off operation." fi + 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 ESP32_FULL_BUILD=0 MESHDEBUG_OVERRIDE="" PACKET_LOGGING_OVERRIDE="" @@ -5706,6 +5810,12 @@ run_logging_matrix_build_targets() { if [ "$pass_status" -ne 0 ]; then build_status=1; fi fi + run_full_only_esp32_profile "${full_only_targets[@]}" + pass_status=$? + if [ "$pass_status" -eq 130 ]; then return 130; fi + if [ "$pass_status" -ne 0 ]; then build_status=1; fi + + FULL_ONLY_EXACT_PROFILE_ACTIVE=1 run_full_esp32_profile "FULL unified pass" "unified" "${targets[@]}" pass_status=$? if [ "$pass_status" -eq 130 ]; then return 130; fi diff --git a/test/test_full_infrastructure_selection.sh b/test/test_full_infrastructure_selection.sh index 32aa9b6ec..5e53149e3 100644 --- a/test/test_full_infrastructure_selection.sh +++ b/test/test_full_infrastructure_selection.sh @@ -42,6 +42,29 @@ assert_auto_unified Heltec_T190_repeater_ Heltec_T190_repeater_observer_mqtt assert_auto_unified LilyGo_TLora_V2_1_1_6_room_server LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt_ assert_auto_unified heltec_v4_r8_repeater_observer_mqtt heltec_v4_r8_repeater_observer_mqtt +# Only targets whose exact FULL environment was audited for partition history +# and every capacity limit may omit their redundant portable bulk artifact. +full_only_count=0 +for target in "${!PIO_ENV_PLATFORM_BY_NAME[@]}"; do + if is_esp32_full_only_bulk_target "$target"; then + full_only_count=$((full_only_count + 1)) + fi +done +[ "$full_only_count" -eq 47 ] || fail "expected 47 audited FULL-only targets, found $full_only_count" + +for target in heltec_rc32_repeater Station_G2_repeater_observer_mqtt \ + SenseCapIndicator-LoRa-N16R2_companion_radio_full \ + LilyGo_Tlora_C6_companion_radio_full_ \ + LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow; do + is_esp32_full_only_bulk_target "$target" || fail "missed audited FULL-only target $target" +done +for target in LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_ \ + LilyGo_TLora_V2_1_1_6_companion_radio_wifi heltec_v4_r8_repeater; do + if is_esp32_full_only_bulk_target "$target"; then + fail "consolidated target with a capacity or partition caveat: $target" + fi +done + # Portable images and exact reduced OTA identities must not be consolidated. for target in heltec_v4_r8_repeater heltec_v4_r8_room_server \ heltec_v4_r8_repeater_lora_ota_no_external_sensors; do @@ -104,6 +127,46 @@ done || fail "observer output settings leaked into subsequent builds" ) +# A canonical bulk build promotes an audited target in-place, retaining its +# environment/mOTA identity. Explicit standard builds remain untouched above. +( + ESP32_FULL_BUILD=0 + BUILD_PROFILE_EFFECTIVE=standard + BUILD_PROFILE_OVERRIDE=auto + BUILD_PROFILE_EXPLICIT=0 + BATCH_BUILD_MODE=1 + FIRMWARE_VERSION=vtest + RESUME_BUILD_OUTPUT=1 + FIRMWARE_FILENAME_INFIX="" + observed_filename="" + observed_full=0 + build_artifacts_exist() { + observed_filename=$2 + observed_full=$ESP32_FULL_BUILD + return 0 + } + build_firmware heltec_rc32_repeater >/dev/null + [ "$observed_full" = 1 ] || fail "audited bulk target did not promote to FULL" + [[ "$observed_filename" == heltec_rc32_repeater-full-logging-ota-vtest-* ]] \ + || fail "FULL-only bulk target lost its exact identity" +) +( + ESP32_FULL_BUILD=0 + BUILD_PROFILE_EFFECTIVE=standard + BUILD_PROFILE_OVERRIDE=standard + BUILD_PROFILE_EXPLICIT=1 + BATCH_BUILD_MODE=1 + FIRMWARE_VERSION=vtest + RESUME_BUILD_OUTPUT=1 + observed_full=1 + build_artifacts_exist() { + observed_full=$ESP32_FULL_BUILD + return 0 + } + build_firmware heltec_rc32_repeater >/dev/null + [ "$observed_full" = 0 ] || fail "explicit standard recovery build was promoted" +) + ( standard_built="" full_built="" @@ -120,6 +183,19 @@ done [ "$ESP32_LORA_OTA_APP_LIMIT" = 1310720 ] || fail "changed the 1.25 MiB portable limit" ) +( + calls=() + REQUIRE_OTA_UPDATES=0 + run_logged_build_targets() { + calls+=("$BUILD_PROFILE_EFFECTIVE:$ESP32_FULL_BUILD:$FIRMWARE_FILENAME_INFIX:$*") + } + run_logging_matrix_build_targets heltec_rc32_repeater \ + heltec_rc32_repeater_bridge_espnow >/dev/null + [ "${#calls[@]}" -eq 1 ] || fail "FULL-only targets emitted duplicate matrix artifacts" + [ "${calls[0]}" = 'full:1:full-logging:heltec_rc32_repeater heltec_rc32_repeater_bridge_espnow' ] \ + || fail "matrix did not build exact FULL-only 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"