diff --git a/build.sh b/build.sh index bd708075..9389001f 100755 --- a/build.sh +++ b/build.sh @@ -138,7 +138,7 @@ Options: --firmware-version : Firmware version to embed. --radio-preset : Override the USA Cascadia radio default. Stable names are usa-cascadia and target; legacy menu numbers remain accepted. --profile : Override runtime settings embedded in the firmware (not its feature set). - --build-profile : Select feature/partition policy. Auto first builds complete LoRa-OTA-capable firmware. A measured size failure triggers the reduced recipe; internal-flash nRF52 repeaters also publish the reduced image for extra delta-staging headroom. Standard applies declared portable-image reductions immediately; full requires the expanded ESP32 recipe. + --build-profile : Select feature/partition policy. Auto uses the combined Full MQTT/USB/WiFi recipe when it covers the plain infrastructure target; otherwise it first builds complete LoRa-OTA-capable firmware with a measured-size fallback. Internal-flash nRF52 repeaters also publish the reduced image for delta-staging headroom. Standard preserves portable images, including the 1.25 MiB slot; full requires the expanded ESP32 recipe. --auto|--standard|--full: Short forms of --build-profile. --skip-kiss|--include-kiss: Exclude (default) or include KISS modem targets in bulk builds. --clean|--resume: Clean output or resume existing Option 3/FULL-only artifacts. @@ -2340,6 +2340,57 @@ get_mqtt_disabled_target() { echo "$candidate" } +get_unified_full_infrastructure_target() { + local target=$1 + local candidate + local base=${target%_} + + [ "${PIO_ENV_PLATFORM_BY_NAME[$target]:-}" = "ESP32_PLATFORM" ] || return 1 + case "${base,,}" in + *_repeater|*_room_server|*_repeater_observer_mqtt|*_room_server_observer_mqtt) ;; + *) return 1 ;; + esac + + # These plain Full recipes retain more routing capacity than their MQTT + # siblings (T-Beam flood rules / room neighbors, TLora repeater neighbors). + # They are intentional alternatives, not duplicate Full artifacts. + case "${base,,}" in + tbeam_sx1262_repeater|tbeam_sx1276_repeater|\ + tbeam_sx1262_room_server|tbeam_sx1276_room_server|\ + lilygo_tlora_v2_1_1_6_repeater) return 1 ;; + esac + + # Some exact hardware recipes have a trailing underscore on either side. + # Require the same board as well as the same role/hardware name; never fold + # a display, radio, storage, Ethernet, or serial-bridge variant into this. + for candidate in "$target" "${base}_observer_mqtt" "${base}_observer_mqtt_"; do + if is_mqtt_bridge_target "$candidate" \ + && supports_esp32_full_build "$candidate" \ + && [ -n "${PIO_ENV_BOARD_BY_NAME[$target]:-}" ] \ + && [ "${PIO_ENV_BOARD_BY_NAME[$target]}" = "${PIO_ENV_BOARD_BY_NAME[$candidate]:-}" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + return 1 +} + +configure_unified_full_infrastructure_output() { + [ "$ESP32_FULL_BUILD" = "1" ] && is_mqtt_bridge_target "$1" || return 0 + case "${1,,}" in + *_repeater_observer_mqtt|*_repeater_observer_mqtt_|\ + *_room_server_observer_mqtt|*_room_server_observer_mqtt_) ;; + *) return 0 ;; + esac + # A direct/auto observer build and a matrix build must produce the same + # runtime-selectable artifact, not MQTT-only and USB+MQTT Full twins. + MESHDEBUG_OVERRIDE="off" + PACKET_LOGGING_OVERRIDE="on" + MQTT_BRIDGE_OVERRIDE="on" + MQTT_DEBUG_OVERRIDE="off" + FIRMWARE_FILENAME_INFIX="full-usb-wifi" +} + normalize_resolved_targets_for_mqtt() { local command=$1 local target @@ -4013,6 +4064,10 @@ build_firmware() { local inherited_firmware_filename_infix=$FIRMWARE_FILENAME_INFIX local ESP32_FULL_BUILD=$inherited_esp32_full_build local FIRMWARE_FILENAME_INFIX=$inherited_firmware_filename_infix + local MESHDEBUG_OVERRIDE=$MESHDEBUG_OVERRIDE + local PACKET_LOGGING_OVERRIDE=$PACKET_LOGGING_OVERRIDE + local MQTT_BRIDGE_OVERRIDE=$MQTT_BRIDGE_OVERRIDE + local MQTT_DEBUG_OVERRIDE=$MQTT_DEBUG_OVERRIDE env_platform=$(get_platform_for_env "$env_name") if ! is_supported_platform "$env_platform"; then @@ -4046,6 +4101,7 @@ build_firmware() { || is_companion_radio_full_target "$env_name"; then BUILD_PROFILE_FOR_TARGET="full" fi + configure_unified_full_infrastructure_output "$env_name" echo "Effective feature profile for ${env_name}: ${BUILD_PROFILE_FOR_TARGET}" commit_hash=$(git rev-parse --short HEAD) @@ -4874,6 +4930,7 @@ configure_effective_build_profile() { local command_name=$1 local target="" local reduced_target="" + local unified_target="" if [ "${#RESOLVED_BUILD_TARGETS[@]}" -eq 1 ]; then target=${RESOLVED_BUILD_TARGETS[0]} @@ -4908,6 +4965,12 @@ configure_effective_build_profile() { && [ "${#RESOLVED_BUILD_TARGETS[@]}" -eq 1 ]; then if is_companion_radio_full_target "$target"; then BUILD_PROFILE_EFFECTIVE="full" + elif unified_target=$(get_unified_full_infrastructure_target "$target"); then + BUILD_PROFILE_EFFECTIVE="full" + SINGLE_TARGET_FULL_BUILD=1 + RESOLVED_BUILD_TARGETS=("$unified_target") + echo "Using ${unified_target} as the combined Full image for ${target}; logging/MQTT are runtime settings." + echo "Use --build-profile standard for the original portable partition/OTA target contract." elif supports_esp32_full_build "$target"; then BUILD_PROFILE_EFFECTIVE="full" AUTO_PREFER_FULL_BUILD=1 @@ -5309,6 +5372,12 @@ run_full_esp32_profile() { for target in "${targets[@]}"; do full_profile_target=$(get_esp32_full_profile_target "$target") + # Use the same exact-board choice as single-target auto builds, including + # historical trailing-underscore recipes such as Heltec T190. + mqtt_target=$(get_unified_full_infrastructure_target "$full_profile_target") || mqtt_target="" + if [ -n "$mqtt_target" ]; then + full_profile_target=$mqtt_target + fi full_target="" if [ "$profile_mode" = "fallback" ]; then # A matching MQTT environment is emitted once by the unified profile; diff --git a/docs/cli_build_matrix.md b/docs/cli_build_matrix.md index 94b15ab5..44f2628a 100644 --- a/docs/cli_build_matrix.md +++ b/docs/cli_build_matrix.md @@ -43,6 +43,22 @@ host-mOTA controls for Full Companion, and the advanced flood-rule engine for Full room servers. Resume mode will not accept an old or failed artifact without a verified sidecar. +For a single ESP32 repeater or room-server target, `auto` selects its matching +combined Full MQTT/USB/WiFi image when that image covers the plain Full recipe. +The direct observer and Full-matrix paths use the same `full-usb-wifi-ota` +artifact and runtime output controls. Plain Full T-Beam SX1262/SX1276 profiles +and the TLora V2 repeater remain alternatives because their MQTT siblings have +lower routing-table capacities. + +Standard and reduced OTA images, including images that fit the 1.25 MiB +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. +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. + ## Role comes first | Role | Text administration CLI | diff --git a/test/test_full_infrastructure_selection.sh b/test/test_full_infrastructure_selection.sh new file mode 100644 index 00000000..32aa9b6e --- /dev/null +++ b/test/test_full_infrastructure_selection.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")/.." +source build.sh + +fail() { echo "test_full_infrastructure_selection: $*" >&2; exit 1; } + +# Resolve real board inheritance, but never compile or flash firmware. +init_project_context >/dev/null + +assert_auto_unified() ( + local plain=$1 expected=$2 + BUILD_PROFILE_OVERRIDE=auto + BUILD_PROFILE_EXPLICIT=0 + SINGLE_TARGET_FULL_BUILD=0 + RESOLVED_BUILD_TARGETS=("$plain") + configure_effective_build_profile build-firmware >/dev/null + [ "${RESOLVED_BUILD_TARGETS[*]}" = "$expected" ] || fail "$plain selected the wrong target" + [ "$SINGLE_TARGET_FULL_BUILD" = 1 ] || fail "$plain bypassed the unified recipe" + [ "$AUTO_PREFER_FULL_BUILD" = 0 ] || fail "$plain still schedules a plain Full artifact" + + local -a built=() + run_logged_build_targets() { + [ "$FIRMWARE_FILENAME_INFIX" = full-usb-wifi ] || fail "wrong artifact name" + [ "$ESP32_FULL_BUILD" = 1 ] || fail "lost expanded partitions" + [ "$PACKET_LOGGING_OVERRIDE" = on ] || fail "lost USB packet logging" + [ "$MQTT_BRIDGE_OVERRIDE" = on ] || fail "lost MQTT" + [ "$MESHDEBUG_OVERRIDE" = off ] || fail "enabled verbose debug" + built+=("$@") + } + run_command build-firmware >/dev/null + [ "${built[*]}" = "$expected" ] || fail "auto emitted duplicate Full artifacts" + built=() + run_full_esp32_build_targets all "$plain" "$expected" >/dev/null + [ "${built[*]}" = "$expected" ] || fail "matrix emitted duplicate Full artifacts" +) + +assert_auto_unified heltec_v4_r8_repeater heltec_v4_r8_repeater_observer_mqtt +assert_auto_unified heltec_v4_r8_room_server heltec_v4_r8_room_server_observer_mqtt +assert_auto_unified heltec_v4_r8_tft_repeater heltec_v4_r8_tft_repeater_observer_mqtt +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 + +# 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 + ( + BUILD_PROFILE_OVERRIDE=standard + BUILD_PROFILE_EXPLICIT=1 + SINGLE_TARGET_FULL_BUILD=0 + RESOLVED_BUILD_TARGETS=("$target") + configure_effective_build_profile build-firmware >/dev/null + [ "${RESOLVED_BUILD_TARGETS[*]}" = "$target" ] || fail "lost portable target $target" + [ "$BUILD_PROFILE_EFFECTIVE" = standard ] || fail "expanded portable image $target" + [ "$SINGLE_TARGET_FULL_BUILD" = 0 ] || fail "portable image entered Full pipeline" + ) +done + +for target in Tbeam_SX1262_repeater Tbeam_SX1276_repeater \ + Tbeam_SX1262_room_server Tbeam_SX1276_room_server \ + LilyGo_TLora_V2_1_1_6_repeater \ + heltec_v4_r8_repeater_lora_ota_no_external_sensors \ + RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors; do + if get_unified_full_infrastructure_target "$target" >/dev/null; then + fail "consolidated a distinct capacity/OTA contract: $target" + fi +done + +( + PIO_ENV_BOARD_BY_NAME[heltec_v4_r8_repeater_observer_mqtt]=different_board + if get_unified_full_infrastructure_target heltec_v4_r8_repeater >/dev/null; then + fail "accepted a different physical board" + fi +) + +# Direct observer promotion must use the matrix's exact output recipe even if +# interactive flags previously requested MQTT without USB logging. Stop at the +# existing-artifact check to exercise the real build entry point without pio run. +( + ESP32_FULL_BUILD=0 + BUILD_PROFILE_EFFECTIVE=standard + BUILD_PROFILE_OVERRIDE=auto + BUILD_PROFILE_EXPLICIT=0 + FIRMWARE_VERSION=vtest + RESUME_BUILD_OUTPUT=1 + FIRMWARE_FILENAME_INFIX="" + MESHDEBUG_OVERRIDE=on + PACKET_LOGGING_OVERRIDE=off + MQTT_BRIDGE_OVERRIDE=off + MQTT_DEBUG_OVERRIDE=on + observed_filename="" + observed_output="" + build_artifacts_exist() { + observed_filename=$2 + observed_output="$PACKET_LOGGING_OVERRIDE/$MQTT_BRIDGE_OVERRIDE/$MESHDEBUG_OVERRIDE/$MQTT_DEBUG_OVERRIDE" + return 0 + } + build_firmware heltec_v4_r8_repeater_observer_mqtt >/dev/null + [[ "$observed_filename" == heltec_v4_r8_repeater_observer_mqtt-full-usb-wifi-ota-vtest-* ]] \ + || fail "direct observer kept a duplicate name" + [ "$observed_output" = on/on/off/off ] || fail "direct observer differs from unified recipe" + [ "$PACKET_LOGGING_OVERRIDE/$MQTT_BRIDGE_OVERRIDE/$MESHDEBUG_OVERRIDE/$MQTT_DEBUG_OVERRIDE" = off/off/on/on ] \ + || fail "observer output settings leaked into subsequent builds" +) + +( + standard_built="" + full_built="" + REQUIRE_OTA_UPDATES=0 + run_logged_build_targets() { + if [ "$ESP32_FULL_BUILD" = 0 ]; then standard_built="$*"; else full_built="$*"; fi + } + run_logging_matrix_build_targets heltec_v4_r8_repeater \ + heltec_v4_r8_repeater_lora_ota_no_external_sensors \ + heltec_v4_r8_repeater_observer_mqtt >/dev/null + [ "$standard_built" = 'heltec_v4_r8_repeater heltec_v4_r8_repeater_lora_ota_no_external_sensors' ] \ + || fail "matrix removed a portable build" + [ "$full_built" = heltec_v4_r8_repeater_observer_mqtt ] || fail "matrix duplicated Full" + [ "$ESP32_LORA_OTA_APP_LIMIT" = 1310720 ] || fail "changed the 1.25 MiB portable limit" +) + +# 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" +PLATFORMIO_BUILD_FLAGS="" +apply_firmware_profile_overrides +[[ "$PLATFORMIO_BUILD_FLAGS" == *-DDEFAULT_POWERSAVING_ENABLED=1* ]] || fail "device power saving disabled" +[[ "$PLATFORMIO_BUILD_FLAGS" == *-DDEFAULT_RXPS_ENABLED=1* ]] || fail "RX power saving disabled" + +echo "test_full_infrastructure_selection: OK"