diff --git a/build.sh b/build.sh index 322d35357..a98b13d23 100755 --- a/build.sh +++ b/build.sh @@ -2448,9 +2448,19 @@ declare_build_capability_contract() { if is_companion_radio_full_target "$env_name"; then record_build_expectation "companion.temp_radio" "tempradio" record_build_expectation "companion.ota_cli" "ota folder" + record_build_expectation "companion.usb" "+++MESHCORE-TERM-START" + record_build_expectation "companion.bluetooth" \ + "Companion: starting Bluetooth" + if is_nrf52_companion_radio_full_target "$env_name" \ + || is_esp32_dual_cdc_companion_radio_full_target "$env_name"; then + record_build_expectation "companion.dedicated_usb_logging" \ + "get usb.logging" + fi if [ "$env_platform" = "ESP32_PLATFORM" ]; then record_build_expectation "companion.network_terminal" \ - "Full Companion terminal listening" + "USB currently owns the Full Companion terminal" + record_build_expectation "companion.wifi_ota_seeder" \ + "OTA seeder listening on :" record_build_expectation "web.webconfig" "start webconfig" else record_build_expectation "companion.usb_mota_source" "ota folder on" @@ -2729,6 +2739,29 @@ append_platformio_extra_script() { export PLATFORMIO_EXTRA_SCRIPTS } +apply_lora_ota_flag_order_fix() { + local env_name=$1 + local pio_env_name=$2 + + if ! is_lora_ota_build "$env_name" \ + && ! is_companion_radio_full_target "$env_name"; then + unset MESHCORE_FORCE_LORA_OTA + return 0 + fi + + # A few legacy recipes deliberately finish with -UENABLE_OTA. PlatformIO + # retains that raw undefine after externally supplied definitions, so an OTA + # overlay can otherwise advertise its controls while compiling out the + # protocol implementation. Remove the legacy undefine before flags are + # parsed for every OTA overlay, not just for one platform or role. + if pio_env_option_contains "$pio_env_name" build_flags "-UENABLE_OTA"; then + export MESHCORE_FORCE_LORA_OTA=1 + append_platformio_extra_script "pre:scripts/force_lora_ota.py" + else + unset MESHCORE_FORCE_LORA_OTA + fi +} + apply_nrf52_lora_ota_build_recipe() { local env_name=$1 local pio_env_name=$2 @@ -2753,17 +2786,6 @@ apply_nrf52_lora_ota_build_recipe() { append_platformio_extra_script "post:tools/mota/pio_endf.py" fi - # A few feature-rich legacy environments deliberately end their own flags - # with -UENABLE_OTA. PlatformIO build_unflags cannot remove a trailing -U, - # so a pre-script must make the intentional OTA overlay authoritative for - # both compilation and the EndF geometry hook. - if pio_env_option_contains "$pio_env_name" build_flags "-UENABLE_OTA"; then - export MESHCORE_FORCE_LORA_OTA=1 - append_platformio_extra_script "pre:scripts/force_lora_ota.py" - else - unset MESHCORE_FORCE_LORA_OTA - fi - if supports_nrf52_internal_bootloader_update "$env_name"; then export MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE=1 else @@ -2904,6 +2926,8 @@ apply_companion_radio_full_profile() { if requires_esp32_companion_full_ota_fallback "$pio_env_name"; then append_platformio_build_unflags "-DMAX_CONTACTS=350 -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=512 -DOFFLINE_QUEUE_SIZE=256 -DOFFLINE_QUEUE_SIZE=128" export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=8 -DOFFLINE_QUEUE_SIZE=16" + record_build_reduction \ + "companion.capacity limited to 100 contacts, 8 channels, and 16 queued frames by measured internal DRAM" fi # A few WiFi recipes list only their WiFi implementation instead of the @@ -3354,6 +3378,7 @@ build_firmware() { apply_lora_ota_override "$env_name" apply_logical_ota_tuning_flags "$env_name" "$pio_env_name" apply_companion_radio_full_profile "$env_name" "$pio_env_name" + apply_lora_ota_flag_order_fix "$env_name" "$pio_env_name" apply_nrf52_lora_ota_build_recipe "$env_name" "$pio_env_name" apply_esp32_lora_ota_size_profile "$env_name" apply_esp32_constrained_companion_size_profile "$env_name" diff --git a/docs/cli_build_matrix.md b/docs/cli_build_matrix.md index bce0514ab..fb0bf2032 100644 --- a/docs/cli_build_matrix.md +++ b/docs/cli_build_matrix.md @@ -104,6 +104,11 @@ available from a canonical image: interface `02` for plaintext logs. Its LoRa OTA support is source-only: it can serve a host file to another node but has no staging store and cannot update itself over LoRa. + Installing an ESP32 Full Companion may require one merged-image erase/flash + to adopt its expanded partition table. Once migrated, later Full Companion + releases use normal app updates. The transport-specific non-Full images stay + available as explicit build targets, but are not canonical release artifacts + when the qualified Full image fits. The old aliases still work with `build-firmware` and `build-matching-firmwares`. Dedicated repeater LoRa OTA receiver images are not diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index 9ab005f69..caab26fd3 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -163,8 +163,11 @@ target recipes, but application behavior no longer uses that umbrella as an unrelated compile guard. In particular, ESP32 WiFi/WebConfig terminal controls are compiled from their actual WiFi/WebConfig capability, and BLE is always started before WiFi on any combined ESP32 WiFi+BLE Companion to avoid heap -fragmentation. The build's capability sidecar verifies the Full controls after -linking. +fragmentation. Compile-time prerequisite checks reject inconsistent feature +flags. After linking, the capability sidecar verifies USB, BLE, the OTA CLI, +TempRadio, and each platform's host-folder transport; it also verifies the TCP +terminal, WebConfig, and WiFi seeder on ESP32, plus dedicated logging whenever +the board uses dual CDC. On 4 MB ESP32 boards, the full target uses a single 3 MB application partition so WiFi, BLE, WebConfig, and source-only mOTA fit together. The T-Beam 1W Full diff --git a/examples/companion_radio/CompanionFeatures.h b/examples/companion_radio/CompanionFeatures.h index c34484481..30b0f51f1 100644 --- a/examples/companion_radio/CompanionFeatures.h +++ b/examples/companion_radio/CompanionFeatures.h @@ -11,11 +11,13 @@ #ifndef COMPANION_FEATURE_OTA_CLI #define COMPANION_FEATURE_OTA_CLI 1 #endif - #ifndef COMPANION_FEATURE_NETWORK_TERMINAL - #define COMPANION_FEATURE_NETWORK_TERMINAL 1 - #endif - #ifndef COMPANION_FEATURE_MEMORY_DIAGNOSTICS - #define COMPANION_FEATURE_MEMORY_DIAGNOSTICS 1 + #if defined(ESP32_PLATFORM) + #ifndef COMPANION_FEATURE_NETWORK_TERMINAL + #define COMPANION_FEATURE_NETWORK_TERMINAL 1 + #endif + #ifndef COMPANION_FEATURE_MEMORY_DIAGNOSTICS + #define COMPANION_FEATURE_MEMORY_DIAGNOSTICS 1 + #endif #endif #if defined(MESH_DUAL_CDC_LOGGING) #ifndef COMPANION_FEATURE_DEDICATED_USB_LOGGING @@ -48,12 +50,36 @@ #define COMPANION_FEATURE_DEDICATED_USB_LOGGING 0 #endif +#if COMPANION_FEATURE_OTA_CLI && !defined(ENABLE_OTA) + #error "COMPANION_FEATURE_OTA_CLI requires ENABLE_OTA" +#endif + #if COMPANION_FEATURE_NETWORK_TERMINAL && !defined(ENABLE_USB_INTERFACE) #error "COMPANION_FEATURE_NETWORK_TERMINAL requires ENABLE_USB_INTERFACE" #endif +#if COMPANION_FEATURE_NETWORK_TERMINAL \ + && !(defined(ESP32_PLATFORM) && defined(WIFI_SSID) \ + && defined(ENABLE_OTA)) + #error "COMPANION_FEATURE_NETWORK_TERMINAL requires ESP32 WiFi and the OTA source overlay" +#endif + #if COMPANION_FEATURE_USB_MOTA_SOURCE \ && !(defined(NRF52_PLATFORM) && defined(OTA_FOLDER_SERIAL) \ - && defined(ENABLE_USB_INTERFACE)) + && defined(ENABLE_USB_INTERFACE) && defined(ENABLE_OTA)) #error "COMPANION_FEATURE_USB_MOTA_SOURCE requires nRF52 USB folder seeding" #endif + +#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS && !defined(ESP32_PLATFORM) + #error "COMPANION_FEATURE_MEMORY_DIAGNOSTICS requires ESP32" +#endif + +#if COMPANION_FEATURE_DEDICATED_USB_LOGGING \ + && !(defined(MESH_DUAL_CDC_LOGGING) \ + && defined(ENABLE_USB_INTERFACE)) + #error "COMPANION_FEATURE_DEDICATED_USB_LOGGING requires dual-CDC USB" +#endif + +#if defined(COMPANION_RADIO_FULL) && !defined(BLE_PIN_CODE) + #error "COMPANION_RADIO_FULL requires the Bluetooth Companion transport" +#endif diff --git a/test/test_build_profiles.sh b/test/test_build_profiles.sh index 992bcc160..a3ccfb8ab 100644 --- a/test/test_build_profiles.sh +++ b/test/test_build_profiles.sh @@ -149,4 +149,84 @@ verify_reduced_sensor_flags \ verify_reduced_sensor_flags \ Heltec_t114_repeater_lora_ota_no_external_sensors 0 +# A qualified Full Companion is the one canonical release artifact for its +# board. The individual USB/BLE transports remain explicitly buildable, but +# must not be republished beside the Full image. +SUPPORTED_PIO_ENVS=( + Station_G2_companion_radio_usb + Station_G2_companion_radio_ble + Station_G2_companion_radio_wifi + Station_G2_companion_radio_full +) +for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do + PIO_ENV_PLATFORM_BY_NAME[$env_name]=ESP32_PLATFORM + PIO_ENV_BOARD_BY_NAME[$env_name]=station-g2 +done +mapfile -t companion_release_targets < <( + print_release_firmware_targets get-companion-firmwares-to-build +) +[ "${#companion_release_targets[@]}" -eq 1 ] \ + || fail "Station G2 release did not collapse transports to one Full Companion" +[ "${companion_release_targets[0]}" = Station_G2_companion_radio_full ] \ + || fail "Station G2 release selected a non-Full Companion artifact" + +# A WiFi base's final -UENABLE_OTA must not win over the Full Companion's +# source-only OTA overlay. That mismatch compiles out both the TCP terminal and +# seeder while still leaving a superficially valid binary. +pio_env_option_contains() { + [ "$2" = build_flags ] && [ "$3" = -UENABLE_OTA ] +} +PLATFORMIO_BUILD_FLAGS="" +PLATFORMIO_BUILD_UNFLAGS="" +PLATFORMIO_EXTRA_SCRIPTS="" +unset MESHCORE_FORCE_LORA_OTA +apply_companion_radio_full_profile \ + Station_G2_companion_radio_full Station_G2_companion_radio_wifi +apply_lora_ota_flag_order_fix \ + Station_G2_companion_radio_full Station_G2_companion_radio_wifi +[ "${MESHCORE_FORCE_LORA_OTA:-}" = 1 ] \ + || fail "ESP32 Full Companion did not force its OTA source overlay" +[[ "$PLATFORMIO_EXTRA_SCRIPTS" == *"pre:scripts/force_lora_ota.py"* ]] \ + || fail "ESP32 Full Companion did not install the OTA flag-order fix" + +# The same protection applies to explicitly requested standalone OTA +# transports even though the canonical release publishes the Full replacement. +PIO_ENV_OTA_BY_NAME[Station_G2_companion_radio_wifi]=1 +PLATFORMIO_EXTRA_SCRIPTS="" +unset MESHCORE_FORCE_LORA_OTA +apply_lora_ota_flag_order_fix \ + Station_G2_companion_radio_wifi Station_G2_companion_radio_wifi +[ "${MESHCORE_FORCE_LORA_OTA:-}" = 1 ] \ + || fail "standalone ESP32 OTA Companion did not force its OTA overlay" +[[ "$PLATFORMIO_EXTRA_SCRIPTS" == *"pre:scripts/force_lora_ota.py"* ]] \ + || fail "standalone ESP32 OTA Companion omitted the OTA flag-order fix" + +# Full capability contracts must use functional strings that survive release +# builds with optional debug logging disabled. +BUILD_PROFILE_FOR_TARGET=full +BUILD_CAPABILITIES=() +BUILD_REDUCTIONS=() +BUILD_EXPECTATIONS=() +declare_build_capability_contract \ + Station_G2_companion_radio_full ESP32_PLATFORM +expectations=" ${BUILD_EXPECTATIONS[*]} " +[[ "$expectations" == *"companion.network_terminal=USB currently owns the Full Companion terminal"* ]] \ + || fail "ESP32 Full contract uses no stable network-terminal evidence" +[[ "$expectations" == *"companion.wifi_ota_seeder=OTA seeder listening on :"* ]] \ + || fail "ESP32 Full contract omitted the WiFi seeder" +[[ "$expectations" != *"Full Companion terminal listening"* ]] \ + || fail "ESP32 Full contract still depends on optional debug logging" + +PIO_ENV_PLATFORM_BY_NAME[RAK_3401_companion_radio_full]=NRF52_PLATFORM +BUILD_CAPABILITIES=() +BUILD_REDUCTIONS=() +BUILD_EXPECTATIONS=() +declare_build_capability_contract \ + RAK_3401_companion_radio_full NRF52_PLATFORM +expectations=" ${BUILD_EXPECTATIONS[*]} " +[[ "$expectations" == *"companion.usb_mota_source=ota folder on"* ]] \ + || fail "nRF52 Full contract omitted USB folder seeding" +[[ "$expectations" != *"companion.network_terminal="* ]] \ + || fail "nRF52 Full contract promised an ESP32-only network terminal" + echo "test_build_profiles: OK" diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 86cfeefc4..e97666d9d 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -1,6 +1,12 @@ [Station_G2] extends = esp32_base board = station-g2 +; MeshCore and the Station G2's source-built dependencies do not throw C++ +; exceptions. Removing the otherwise forced exception runtime saves about +; 67 KiB, keeping the complete ordinary repeater inside the deployed 1.25 MiB +; app slot without removing sensors, LoRa OTA, the browser updater, or CLI. +build_unflags = + -fexceptions ; Preserve the original 0x10000/0x150000 dual-OTA layout so app-only updates do ; not require replacing the partition table or erasing an installed G2. board_build.partitions = default.csv @@ -11,6 +17,7 @@ board_build.flash_mode = dio build_flags = ${esp32_base.build_flags} ${sensor_base.build_flags} + -fno-exceptions -I variants/station_g2 -I src/helpers/ui -D STATION_G2 @@ -326,7 +333,9 @@ lib_deps = extends = env:Station_G2_companion_radio_wifi platform = ${esp32_s3_dual_cdc_full.platform} board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode} -build_unflags = ${esp32_s3_dual_cdc_full.build_unflags} +build_unflags = + ${Station_G2.build_unflags} + ${esp32_s3_dual_cdc_full.build_unflags} build_flags = ${env:Station_G2_companion_radio_wifi.build_flags} ${esp32_s3_dual_cdc_full.build_flags}