diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 8431716a6..c670d0b63 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -216,6 +216,7 @@ jobs: python3 -B test/test_full_logging_resume.py python3 -B test/test_firmware_capabilities.py bash test/test_build_profiles.sh + bash test/test_xiao_qspi_combined.sh - name: Verify nRF52 UF2-reset CLI coverage run: python3 -B test/test_nrf52_uf2reset_cli.py diff --git a/build.sh b/build.sh index dfb4e8941..d0eaf6381 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 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 and room servers 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. + --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 and room servers also publish the reduced image for delta-staging headroom; XIAO nRF52 repeaters with external-QSPI staging publish one combined image. 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. @@ -353,12 +353,16 @@ for section, options in data: *_repeater|*_repeater_|*_repeatr|*_repeatr_|*_room_server|*_room_server_|*_room_svr|*_room_svr_) ;; *) continue ;; esac - # SolarXiao 30S/33S repeater and room-server roles already use matched - # external QSPI staging, so their normal full-sensor image is - # install-capable. A second lean no-external-sensors target provides no - # additional OTA capability. + # XIAO nRF52840 repeaters with raw-QSPI staging and the reserved + # bootloader scratch region keep sensors, packet logging, and LoRa OTA + # in their normal image. Do not publish a redundant reduced sibling. + if is_xiao_qspi_combined_repeater_target "$env_name"; then + continue + fi + # Leave SolarXiao room-server inventory unchanged; these roles are not + # part of the XIAO repeater consolidation. case "${env_name,,}" in - solarxiao_30s_repeater|solarxiao_33s_repeater|solarxiao_30s_room_server|solarxiao_33s_room_server) continue ;; + solarxiao_30s_room_server|solarxiao_33s_room_server) continue ;; esac ota_env="${env_name%_}_lora_ota_no_external_sensors" PIO_ENV_COMPLETE_OTA_BASE_BY_NAME["$ota_env"]="$env_name" @@ -2080,6 +2084,22 @@ is_lora_ota_only_target() { [[ "$target_lc" == *lora_ota* ]] } +is_xiao_qspi_combined_repeater_target() { + local env_name=$1 + + [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" = "NRF52_PLATFORM" ] \ + && [ "${PIO_ENV_BOARD_BY_NAME[$env_name]:-}" = "seeed-xiao-afruitnrf52-nrf52840" ] \ + && [ "${PIO_ENV_QSPI_OTA_BY_NAME[$env_name]:-0}" = "1" ] \ + && { [[ "${env_name,,}" == *_repeater ]] \ + || [[ "${env_name,,}" == *_repeater_ ]]; } +} + +is_xiao_qspi_canonical_repeater_build() { + is_xiao_qspi_combined_repeater_target "$1" \ + && [ "$BUILD_PROFILE_FOR_TARGET" = "standard" ] \ + && [ -z "$FIRMWARE_FILENAME_INFIX" ] +} + is_lora_ota_no_external_sensors_target() { local target_lc=${1,,} [[ "$target_lc" == *lora_ota_no_external_sensors ]] @@ -3138,7 +3158,8 @@ record_application_expectation() { declare_full_logging_application_contract() { local env_name=$1 BUILD_APPLICATION_EXPECTATIONS=() - if requires_full_usb_packet_logging "$env_name"; then + if requires_full_usb_packet_logging "$env_name" \ + || is_xiao_qspi_canonical_repeater_build "$env_name"; then # Inspect the packaged application, not an ELF debug/symbol string. The # settings alone can exist in debug-only builds with packet logging absent. record_application_expectation "logging.usb.packets" \ @@ -3179,7 +3200,9 @@ declare_build_capability_contract() { 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" - if is_lora_ota_build "$env_name" && ! is_companion_build "$env_name"; then + if { is_lora_ota_build "$env_name" \ + || is_xiao_qspi_canonical_repeater_build "$env_name"; } \ + && ! is_companion_build "$env_name"; then # nRF52 applies mOTA in its bootloader, so the ESP32 decoder marker is # absent. The checker also verifies the packaged app's EndF/storage layout. record_build_expectation "ota.update.lora" "invalid in-place patch geometry" @@ -3209,7 +3232,8 @@ declare_build_capability_contract() { fi fi - if is_lora_ota_build "$env_name"; then + if is_lora_ota_build "$env_name" \ + || is_xiao_qspi_canonical_repeater_build "$env_name"; then record_build_expectation "ota.cli" "OTA: status" fi @@ -4077,7 +4101,8 @@ write_build_capability_manifest() { ) local item - if requires_wireless_self_update "$env_name"; then + if requires_wireless_self_update "$env_name" \ + || is_xiao_qspi_canonical_repeater_build "$env_name"; then checker_args+=(--require-ota) fi if [ "$env_platform" = "ESP32_PLATFORM" ]; then diff --git a/test/test_xiao_qspi_combined.sh b/test/test_xiao_qspi_combined.sh new file mode 100644 index 000000000..98701eeea --- /dev/null +++ b/test/test_xiao_qspi_combined.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." +source build.sh + +fail() { echo "test_xiao_qspi_combined: $*" >&2; exit 1; } + +# Use the resolved PlatformIO recipes, not a board-name allowlist: each +# canonical XIAO repeater must retain the internal bootloader scratch bank, +# raw external-QSPI OTA staging, and packet logging in one application. +init_project_context +BUILD_PROFILE_FOR_TARGET=standard +BUILD_PROFILE_EFFECTIVE=standard +FIRMWARE_FILENAME_INFIX="" +DISABLE_DEBUG=0 +PACKET_LOGGING_OVERRIDE="" +MESHDEBUG_OVERRIDE="" +MQTT_BRIDGE_OVERRIDE="" + +for target in \ + Xiao_nrf52_repeater \ + ikoka_handheld_nrf_e22_30dbm_repeater \ + ikoka_nano_nrf_22dbm_repeater \ + ikoka_nano_nrf_30dbm_repeater \ + ikoka_nano_nrf_33dbm_repeater \ + ikoka_stick_nrf_22dbm_repeater \ + ikoka_stick_nrf_30dbm_repeater \ + ikoka_stick_nrf_33dbm_repeater \ + solarxiao_30S_repeater \ + solarxiao_33S_repeater; do + is_supported_build_env "$target" || fail "$target is missing" + is_xiao_qspi_canonical_repeater_build "$target" \ + || fail "$target lost its combined-image classification" + if is_supported_build_env "${target}_lora_ota_no_external_sensors"; then + fail "$target still publishes a redundant lean OTA sibling" + fi + if get_reduced_lora_ota_target "$target" >/dev/null; then + fail "$target still falls back to a reduced OTA sibling" + fi + pio_env_option_contains "$target" board_build.ldscript \ + nrf52840_s140_v7_xiao_bootloader_ota.ld \ + || fail "$target lost its 40 KiB internal bootloader scratch reservation" + pio_env_option_contains "$target" board_upload.maximum_size 757760 \ + || fail "$target lost its internal application size limit" + pio_env_option_contains "$target" build_flags OTA_QSPI_BOOTLOADER_UPDATE=1 \ + || fail "$target lost its OTAFIX bootloader-update layout" + pio_env_option_contains "$target" build_flags OTA_QSPI_STORE=1 \ + || fail "$target lost external-QSPI OTA staging" + pio_env_option_contains "$target" build_flags MESH_PACKET_LOGGING=1 \ + || fail "$target lost packet logging" + is_lora_ota_build "$target" || fail "$target lost LoRa OTA" + + BUILD_CAPABILITIES=() + BUILD_REDUCTIONS=() + BUILD_EXPECTATIONS=() + BUILD_APPLICATION_EXPECTATIONS=() + declare_build_capability_contract "$target" NRF52_PLATFORM + expectations=" ${BUILD_EXPECTATIONS[*]} " + application=" ${BUILD_APPLICATION_EXPECTATIONS[*]} " + [[ "$expectations" == *"ota.update.lora=invalid in-place patch geometry"* ]] \ + || fail "$target no longer proves its LoRa OTA receiver" + [[ "$expectations" == *"ota.cli=OTA: status"* ]] \ + || fail "$target no longer proves its OTA CLI" + [[ "$application" == *"logging.usb.packets=%s: %s, len=%d (type=%d, route=%s, payload_len=%d)"* ]] \ + || fail "$target no longer verifies packaged packet logging" + [[ "$application" == *"logging.usb.control=OK - USB logging %s (saved)"* ]] \ + || fail "$target no longer verifies the packaged USB logging control" +done + +# This consolidation is specific to the external-QSPI XIAO repeater layout. +is_xiao_qspi_combined_repeater_target Xiao_nrf52_room_server \ + && fail "non-QSPI room server was incorrectly consolidated" +is_xiao_qspi_combined_repeater_target RAK_4631_repeater \ + && fail "unrelated repeater was incorrectly consolidated" + +echo "test_xiao_qspi_combined: OK" diff --git a/variants/ikoka_handheld_nrf/platformio.ini b/variants/ikoka_handheld_nrf/platformio.ini index a5a6c0eb0..31150b465 100644 --- a/variants/ikoka_handheld_nrf/platformio.ini +++ b/variants/ikoka_handheld_nrf/platformio.ini @@ -99,6 +99,7 @@ build_flags = ${ikoka_handheld_nrf.build_flags} -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -D LORA_TX_POWER=20 + -D MESH_PACKET_LOGGING=1 build_src_filter = ${ikoka_handheld_nrf.build_src_filter} +<../examples/simple_repeater/*.cpp> diff --git a/variants/ikoka_nano_nrf/platformio.ini b/variants/ikoka_nano_nrf/platformio.ini index 2e2a93773..53c94e4f4 100644 --- a/variants/ikoka_nano_nrf/platformio.ini +++ b/variants/ikoka_nano_nrf/platformio.ini @@ -129,7 +129,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -; -D MESH_PACKET_LOGGING=1 + -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ikoka_nano_nrf.build_src_filter} +<../examples/simple_repeater/*.cpp> diff --git a/variants/ikoka_stick_nrf/platformio.ini b/variants/ikoka_stick_nrf/platformio.ini index a36dd01da..2a96ab60f 100644 --- a/variants/ikoka_stick_nrf/platformio.ini +++ b/variants/ikoka_stick_nrf/platformio.ini @@ -135,7 +135,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -; -D MESH_PACKET_LOGGING=1 + -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ikoka_stick_nrf.build_src_filter} + diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index dcfc2cc8d..765e6bf52 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -101,7 +101,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -; -D MESH_PACKET_LOGGING=1 + -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -231,6 +231,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D MESH_PACKET_LOGGING=1 -D WITH_RS232_BRIDGE=Serial1 -D WITH_RS232_BRIDGE_UART=1 -D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX