From 3282b7898e4e1f81c0b7d0f6573b2e26986c5e97 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 22 Sep 2026 05:54:46 -0700 Subject: [PATCH] fix(nrf52): stabilize full companion bluetooth --- build.sh | 38 +++++++- examples/companion_radio/main.cpp | 44 ++++++++- src/helpers/UsbLogging.h | 2 +- src/helpers/nrf52/SerialBLEInterface.cpp | 119 +++++++++++++++-------- src/helpers/nrf52/SerialBLEInterface.h | 38 ++++++++ test/test_build_profiles.sh | 38 +++++++- test/test_nrf52_ble_startup.py | 41 +++++++- tools/mota/test_mota.py | 10 +- variants/t1000-e/target.cpp | 8 ++ variants/t1000-e/target.h | 4 + 10 files changed, 283 insertions(+), 59 deletions(-) diff --git a/build.sh b/build.sh index d0eaf6381..2b5539a25 100755 --- a/build.sh +++ b/build.sh @@ -2736,7 +2736,11 @@ requires_full_usb_packet_logging() { # Full Companion and the combined USB+WiFi infrastructure artifact always # carry the logger behind runtime controls. Other Full profiles retain the # ordinary explicit logging-off/disable-debug behavior. - is_companion_radio_full_target "$env_name" && return 0 + # T1000-E Full has a single, stability-critical USB CDC endpoint. It keeps + # that endpoint for Companion data and the terminal, not optional packet + # logging, so its packaged contract must not promise a logger. + [ "$env_name" != "t1000e_companion_radio_full" ] \ + && is_companion_radio_full_target "$env_name" && return 0 [ "$ESP32_FULL_BUILD" = "1" ] || return 1 is_companion_build "$env_name" && return 1 [ "${PACKET_LOGGING_OVERRIDE,,}" = "on" ] && return 0 @@ -3287,11 +3291,14 @@ declare_build_capability_contract() { record_build_expectation "companion.usb" "+++MESHCORE-TERM-START" record_build_expectation "companion.bluetooth" \ "Companion: starting Bluetooth" - record_build_expectation "companion.usb_logging" "get usb.logging" + if [ "$env_name" != "t1000e_companion_radio_full" ]; then + record_build_expectation "companion.usb_logging" "get usb.logging" + fi record_build_expectation "companion.usb_mota_source" "ota folder on" record_build_expectation "companion.mota_sender" \ "_ZN4mesh3ota16SerialMotaSource4read" - if is_nrf52_companion_radio_full_target "$env_name"; then + if is_nrf52_companion_radio_full_target "$env_name" \ + && [ "$env_name" != "t1000e_companion_radio_full" ]; then record_build_expectation "companion.dedicated_usb_logging" \ "get usb.logging" fi @@ -3845,9 +3852,30 @@ apply_companion_radio_full_profile() { # an exclusive host-folder mode with its existing `ota folder on` preamble. # CDC 1 is a write-only plaintext packet/debug logging stream; BLE remains # an independent Companion link. ESP32 intentionally does not use this - # dual-CDC capability. + # dual-CDC capability. The T1000-E's nRF52840 USB hardware resets when + # its persisted logging preference asks TinyUSB to activate CDC 1, so it + # intentionally keeps logging on the primary USB interface instead. append_platformio_build_unflags "-UOTA_FOLDER_SERIAL" - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DOTA_FOLDER_SERIAL=1 -DCOMPANION_FEATURE_USB_MOTA_SOURCE=1 -DCOMPANION_FEATURE_BLE_MOTA_SOURCE=1 -DCOMPANION_FEATURE_DEDICATED_USB_LOGGING=1 -DCFG_TUD_CDC=2 -DMESH_DUAL_CDC_LOGGING=1 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1" + # The nRF52 core's production-safe Bluefruit bootstrap must retain its + # early SoftDevice diagnostics. Without CFG_DEBUG, a few nRF52840 USB + # layouts can finish the radio setup but leave the BLE stack unavailable. + # Apply the proven bootstrap to every nRF52 Full Companion rather than + # making a board-specific Bluetooth reliability promise. + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DOTA_FOLDER_SERIAL=1 -DCOMPANION_FEATURE_USB_MOTA_SOURCE=1 -DCOMPANION_FEATURE_BLE_MOTA_SOURCE=1 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1 -DCFG_DEBUG=1 -DRECOVERABLE_EXTERNAL_RADIO=1" + if [ "$env_name" = "t1000e_companion_radio_full" ]; then + # The T1000-E's primary CDC transport is the only USB interface. Its + # optional packet/debug logger can make the device reset while a host is + # enumerating, so keep Full Companion USB strictly data/terminal-only. + # This also ignores a stale persisted `usb.logging on` preference from + # an older dual-CDC image, allowing the device to recover automatically. + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMESH_USB_LOGGING_DISABLED=1" + # MESH_USB_LOGGING_DISABLED still silences MeshCore's optional packet + # logger on the shared CDC endpoint. The shared nRF52 Full profile above + # retains the validated BLE bootstrap and radio recovery behavior. + append_platformio_build_unflags "-UMESH_DEBUG -UMESH_PACKET_LOGGING" + else + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCOMPANION_FEATURE_DEDICATED_USB_LOGGING=1 -DCFG_TUD_CDC=2 -DMESH_DUAL_CDC_LOGGING=1" + fi # Every nRF52 Full Companion lends the upper half of its offline queue # to the cold mOTA context. Keep 256 slots for everyday use and reserve diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 9dc8ac36d..b9ddb1612 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2095,8 +2095,16 @@ void halt() { #if defined(BLE_PIN_CODE) static bool companion_bluetooth_initialized = false; + static char companion_bluetooth_start_failure[96] = + "Bluetooth has not started"; static uint32_t companion_bluetooth_start_at = 0; static constexpr uint32_t COMPANION_BLUETOOTH_RETRY_MS = 5000UL; +#ifndef COMPANION_BLUETOOTH_BOOT_DELAY_MS + // Most boards can start Bluefruit as soon as the Companion store and radio + // are ready. A board profile may defer only its first start while a shared + // USB/radio power domain settles; retries continue to use the normal delay. + #define COMPANION_BLUETOOTH_BOOT_DELAY_MS 0UL +#endif static uint8_t companion_bluetooth_session_address[ mesh::companion::BLUETOOTH_MAC_BYTES] = {}; static bool companion_bluetooth_session_address_ready = false; @@ -2192,6 +2200,11 @@ void halt() { mesh::usbLoggingPort().println("Companion: starting Bluetooth"); CompanionNodePrefs* prefs = the_mesh.getNodePrefs(); if (!prepareCompanionBluetoothIdentity(prefs)) { + strncpy(companion_bluetooth_start_failure, + "Bluetooth identity preparation could not save", + sizeof(companion_bluetooth_start_failure) - 1); + companion_bluetooth_start_failure[ + sizeof(companion_bluetooth_start_failure) - 1] = 0; mesh::usbLoggingPort().println( "Companion: Bluetooth identity preparation failed; retrying"); scheduleCompanionBluetoothRetry(); @@ -2199,6 +2212,11 @@ void halt() { } if (!interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface)) { + strncpy(companion_bluetooth_start_failure, + "no free Companion interface slot", + sizeof(companion_bluetooth_start_failure) - 1); + companion_bluetooth_start_failure[ + sizeof(companion_bluetooth_start_failure) - 1] = 0; mesh::usbLoggingPort().println( "Companion: no interface slot available for Bluetooth; retrying"); scheduleCompanionBluetoothRetry(); @@ -2231,6 +2249,11 @@ void halt() { the_mesh.getBLEPin(), bluetooth_address, clear_bonds, stealth_pair_once, bonded_only_peer_ptr)) { + strncpy(companion_bluetooth_start_failure, + bluetooth_interface.beginFailure(), + sizeof(companion_bluetooth_start_failure) - 1); + companion_bluetooth_start_failure[ + sizeof(companion_bluetooth_start_failure) - 1] = 0; interface_manager.removeInterface(&bluetooth_interface); #if defined(NRF52_PLATFORM) // A partly initialized Bluefruit stack cannot be started again safely. @@ -2250,6 +2273,7 @@ void halt() { the_mesh.getNodePrefs()->powersaving_enabled != 0, false); #endif companion_bluetooth_initialized = true; + companion_bluetooth_start_failure[0] = 0; if (interface_manager.isEnabled()) bluetooth_interface.enable(); } @@ -2447,7 +2471,9 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply, } #if defined(BLE_PIN_CODE) if (!companion_bluetooth_initialized) { - snprintf(reply, reply_size, "Error: Bluetooth unavailable in this boot; check transport mode/startup"); + snprintf(reply, reply_size, + "Error: Bluetooth unavailable in this boot: %s", + companion_bluetooth_start_failure); return true; } if (action == CompanionBluetoothCommand::Get) { @@ -2658,9 +2684,13 @@ bool handleCompanionWirelessCommand(const char* command, char* reply, size_t siz } void setup() { + // On nRF52 TinyUSB, bring the primary CDC transport up before any board or + // wireless preparation. Bluefruit preserves USB power state while it + // enables the SoftDevice; a late first Serial.begin() can leave that enable + // path in an invalid state on native-USB boards such as the T1000-E. + Serial.begin(115200); mesh::wireless::control().begin(companion_wireless); mesh::prepareUsbLoggingPort(); - Serial.begin(115200); #if MESH_PACKET_LOGGING mesh::serialLogBegin(); #endif @@ -2962,7 +2992,15 @@ void setup() { // not be fragmented by WiFi. Other BLE companions start here. #if defined(BLE_PIN_CODE) #if !(defined(ESP32) && defined(WIFI_SSID)) - startCompanionBluetooth(); + #if COMPANION_BLUETOOTH_BOOT_DELAY_MS > 0 + companion_bluetooth_start_at = + millis() + COMPANION_BLUETOOTH_BOOT_DELAY_MS; + if (companion_bluetooth_start_at == 0) { + companion_bluetooth_start_at = 1; + } + #else + startCompanionBluetooth(); + #endif #endif #endif diff --git a/src/helpers/UsbLogging.h b/src/helpers/UsbLogging.h index 634f71f2f..fa4a41b5a 100644 --- a/src/helpers/UsbLogging.h +++ b/src/helpers/UsbLogging.h @@ -3,7 +3,7 @@ // Canonical USB-capable images compile at least one of these two diagnostics. // One runtime gate covers every diagnostic category that writes to the USB // Serial stream, so a separate logging firmware is unnecessary. -#if defined(ARDUINO) && \ +#if !defined(MESH_USB_LOGGING_DISABLED) && defined(ARDUINO) && \ ((defined(MESH_DEBUG) && MESH_DEBUG) || \ (defined(MESH_PACKET_LOGGING) && MESH_PACKET_LOGGING)) #define MESH_USB_LOGGING_AVAILABLE 1 diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 3c20dba6d..57a38e1c8 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -407,6 +407,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, // or configuration and a reboot. if (_begin_attempted) return _begin_ready; _begin_attempted = true; + _begin_failure[0] = 0; instance = this; _successfulConnectionPending.store(false, std::memory_order_release); _successfulConnectionStarted.store(0, std::memory_order_relaxed); @@ -422,16 +423,34 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, // If we want to control BLE LED ourselves, uncomment this: // Bluefruit.autoConnLed(false); - Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + // Configure individual connection values rather than the all-or-nothing + // Bluefruit preset. Defaults are identical to BANDWIDTH_MAX; constrained + // board profiles can lower event/buffer reservations without shrinking the + // Companion's negotiated 247-byte MTU. + Bluefruit.configPrphConn(COMPANION_BLE_PRPH_MTU, + COMPANION_BLE_PRPH_EVENT_LENGTH, + COMPANION_BLE_PRPH_HVN_QUEUE, + COMPANION_BLE_PRPH_WRCMD_QUEUE); // The pinned nRF52 core is built with LTO. Its internal xTaskCreate() calls // are resolved before the application's linker wrappers, so a wrapper-based // task observation incorrectly reports that the BLE/SOC workers never // started. Bluefruit.begin() is the reliable startup result available to // this application. if (!Bluefruit.begin()) { + uint8_t softdevice_enabled = 0; + const uint32_t softdevice_status = + sd_softdevice_is_enabled(&softdevice_enabled); + ble_gap_addr_t address = {}; + const uint32_t gap_status = softdevice_enabled + ? sd_ble_gap_addr_get(&address) : NRF_ERROR_INVALID_STATE; + snprintf(_begin_failure, sizeof(_begin_failure), + "Bluefruit begin failed (SD %lu/%u, GAP %lu)", + (unsigned long)softdevice_status, + (unsigned)softdevice_enabled, + (unsigned long)gap_status); instance = nullptr; mesh::usbLoggingPort().println( - "Bluetooth startup failed; reboot required"); + _begin_failure); return false; } @@ -439,6 +458,9 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, if (custom_address != nullptr) { if (!mesh::companion::isValidBluetoothMac(custom_address)) { + strncpy(_begin_failure, "custom Bluetooth MAC is invalid", + sizeof(_begin_failure) - 1); + _begin_failure[sizeof(_begin_failure) - 1] = 0; instance = nullptr; BLE_DEBUG_PRINTLN("Custom Bluetooth MAC is invalid"); return false; @@ -452,6 +474,9 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, } const uint32_t address_error = sd_ble_gap_addr_set(&address); if (address_error != NRF_SUCCESS) { + snprintf(_begin_failure, sizeof(_begin_failure), + "custom Bluetooth MAC rejected (%lu)", + (unsigned long)address_error); instance = nullptr; BLE_DEBUG_PRINTLN("Custom Bluetooth MAC failed: %lu", address_error); return false; @@ -474,6 +499,9 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, const int dev_name_len = snprintf(dev_name, sizeof(dev_name), "%s%s", prefix, suffix); if (dev_name_len < 0 || dev_name_len >= (int)sizeof(dev_name)) { + strncpy(_begin_failure, "Bluetooth device name is too long", + sizeof(_begin_failure) - 1); + _begin_failure[sizeof(_begin_failure) - 1] = 0; instance = nullptr; BLE_DEBUG_PRINTLN("Bluetooth device name is too long"); return false; @@ -514,57 +542,61 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); if (bleuart.begin() != ERROR_NONE) { + strncpy(_begin_failure, "Bluetooth UART service registration failed", + sizeof(_begin_failure) - 1); + _begin_failure[sizeof(_begin_failure) - 1] = 0; instance = nullptr; BLE_DEBUG_PRINTLN("Bluetooth UART service begin failed"); return false; } bleuart.setRxCallback(onBleUartRX); -#if COMPANION_FEATURE_BLE_MOTA_SOURCE - _mota_stream.setSender(sendMotaRequest, this); - _mota_stream.setActive(false); - - _mota_service.setPermission(SECMODE_ENC_WITH_MITM, - SECMODE_ENC_WITH_MITM); - if (_mota_service.begin() != ERROR_NONE) { - instance = nullptr; - BLE_DEBUG_PRINTLN("Bluetooth mOTA service begin failed"); - return false; - } - - _mota_request.setProperties(CHR_PROPS_NOTIFY); - _mota_request.setPermission(SECMODE_ENC_WITH_MITM, - SECMODE_NO_ACCESS); - _mota_request.setMaxLen(mesh::ota::BLE_MOTA_REQUEST_MAX); - _mota_request.setUserDescriptor("mOTA device request"); - if (_mota_request.begin() != ERROR_NONE) { - instance = nullptr; - BLE_DEBUG_PRINTLN("Bluetooth mOTA request characteristic begin failed"); - return false; - } - - _mota_response.setProperties(CHR_PROPS_WRITE); - _mota_response.setPermission(SECMODE_NO_ACCESS, - SECMODE_ENC_WITH_MITM); - _mota_response.setMaxLen(mesh::ota::BLE_MOTA_RESPONSE_MAX); - _mota_response.setUserDescriptor("mOTA host response"); - _mota_response.setWriteCallback(onMotaResponse); - if (_mota_response.begin() != ERROR_NONE) { - instance = nullptr; - BLE_DEBUG_PRINTLN("Bluetooth mOTA response characteristic begin failed"); - return false; + // Register the legacy DFU service before the optional mOTA service. A + // fielded SoftDevice has a fixed GATT table: reserve the ordinary Companion + // and firmware-recovery transports first, then use whatever remains for + // the larger mOTA extension. Do not attempt a new service after an mOTA + // registration failure because Bluefruit cannot remove attributes that did + // fit from its active table. +#if COMPANION_FEATURE_BLE_DFU + bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); + if (bledfu.begin() != ERROR_NONE) { + mesh::usbLoggingPort().println( + "Bluetooth DFU service unavailable; normal Companion Bluetooth continues"); } #endif +#if COMPANION_FEATURE_BLE_MOTA_SOURCE + _mota_stream.setSender(sendMotaRequest, this); + _mota_stream.setActive(false); + _mota_available = false; - // Register DFU on the main BLE stack so paired clients can discover it - // without switching the device into a separate OTA-only BLE mode first. - bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); - if (bledfu.begin() != ERROR_NONE) { - instance = nullptr; - BLE_DEBUG_PRINTLN("Bluetooth DFU service begin failed"); - return false; + _mota_service.setPermission(SECMODE_ENC_WITH_MITM, + SECMODE_ENC_WITH_MITM); + if (_mota_service.begin() == ERROR_NONE) { + _mota_request.setProperties(CHR_PROPS_NOTIFY); + _mota_request.setPermission(SECMODE_ENC_WITH_MITM, + SECMODE_NO_ACCESS); + _mota_request.setMaxLen(mesh::ota::BLE_MOTA_REQUEST_MAX); + _mota_request.setUserDescriptor("mOTA device request"); + if (_mota_request.begin() == ERROR_NONE) { + _mota_response.setProperties(CHR_PROPS_WRITE); + _mota_response.setPermission(SECMODE_NO_ACCESS, + SECMODE_ENC_WITH_MITM); + _mota_response.setMaxLen(mesh::ota::BLE_MOTA_RESPONSE_MAX); + _mota_response.setUserDescriptor("mOTA host response"); + _mota_response.setWriteCallback(onMotaResponse); + _mota_available = _mota_response.begin() == ERROR_NONE; + } } + if (!_mota_available) { + // GATT attributes cannot be removed after Bluefruit starts, so do not + // retry here. Continuing with the already-registered UART service keeps + // the phone and USB recovery paths available; a corrected image can add + // the optional mOTA service on the next reboot. + mesh::usbLoggingPort().println( + "Bluetooth mOTA unavailable; normal Companion Bluetooth continues"); + } +#endif Bluefruit.Advertising.setType( BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED); @@ -985,7 +1017,8 @@ bool SerialBLEInterface::isConnected() const { #if COMPANION_FEATURE_BLE_MOTA_SOURCE bool SerialBLEInterface::isMotaChannelReady() { - return isConnected() && _conn_handle != BLE_CONN_HANDLE_INVALID + return _mota_available && isConnected() + && _conn_handle != BLE_CONN_HANDLE_INVALID && _mota_request.notifyEnabled(_conn_handle); } #endif diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index 3d256eccb..e38c18bb1 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -16,8 +16,38 @@ #define BLE_TX_POWER 4 #endif +// Bluefruit's BANDWIDTH_MAX preset reserves a 100-unit connection event and +// three notification buffers. That is useful on boards with roomy RAM, but +// it exceeds the usable SoftDevice reservation of some bootloader layouts. +// Keep the existing preset as the default while allowing constrained boards +// to retain the 247-byte MTU with a smaller event/buffer reservation. +#ifndef COMPANION_BLE_PRPH_MTU +#define COMPANION_BLE_PRPH_MTU 247 +#endif + +#ifndef COMPANION_BLE_PRPH_EVENT_LENGTH +#define COMPANION_BLE_PRPH_EVENT_LENGTH 100 +#endif + +#ifndef COMPANION_BLE_PRPH_HVN_QUEUE +#define COMPANION_BLE_PRPH_HVN_QUEUE 3 +#endif + +#ifndef COMPANION_BLE_PRPH_WRCMD_QUEUE +#define COMPANION_BLE_PRPH_WRCMD_QUEUE 1 +#endif + +// Nordic UART is the required Companion service. The legacy Nordic DFU +// service is an optional extension that some constrained field combinations +// cannot register alongside the Companion GATT table. +#ifndef COMPANION_FEATURE_BLE_DFU +#define COMPANION_FEATURE_BLE_DFU 1 +#endif + class SerialBLEInterface : public BaseSerialInterface { +#if COMPANION_FEATURE_BLE_DFU BLEDfu bledfu; +#endif BLEUart bleuart; #if COMPANION_FEATURE_BLE_MOTA_SOURCE BLEService _mota_service = BLEService(mesh::ota::BLE_MOTA_SERVICE_UUID); @@ -26,10 +56,15 @@ class SerialBLEInterface : public BaseSerialInterface { BLECharacteristic _mota_response = BLECharacteristic(mesh::ota::BLE_MOTA_RESPONSE_UUID); mesh::ota::BleMotaStream _mota_stream; + // The normal Nordic UART service is the Companion transport. mOTA is an + // optional Full-Companion extension and must never take that transport down + // if a fielded SoftDevice rejects one of its extra GATT attributes. + bool _mota_available = false; #endif bool _isEnabled; bool _begin_attempted = false; bool _begin_ready = false; + char _begin_failure[96] = {}; bool _isDeviceConnected; uint16_t _conn_handle; unsigned long _last_health_check; @@ -129,6 +164,9 @@ public: bool clear_bonds = false, bool stealth_pair_once = false, const mesh::companion::BluetoothPeerIdentity* bonded_only_peer = nullptr); + // Kept after a failed begin so a normal Companion terminal can report the + // cause even when early USB logging was not available. + const char* beginFailure() const { return _begin_failure; } void disconnect(); void enable() override; diff --git a/test/test_build_profiles.sh b/test/test_build_profiles.sh index 5552b3195..bdbc36928 100644 --- a/test/test_build_profiles.sh +++ b/test/test_build_profiles.sh @@ -581,8 +581,41 @@ for full_env in "${SUPPORTED_PIO_ENVS[@]}"; do || fail "$full_env Full Companion omitted LoRa OTA seeding" [[ "$PLATFORMIO_BUILD_FLAGS" == *"OTA_FOLDER_SERIAL=1"* ]] \ || fail "$full_env Full Companion omitted USB folder seeding" - [[ "$PLATFORMIO_BUILD_FLAGS" == *"COMPANION_FEATURE_BLE_MOTA_SOURCE=1"* ]] \ - || fail "$full_env Full Companion omitted BLE folder seeding" + [[ "$PLATFORMIO_BUILD_FLAGS" == *"CFG_DEBUG=1"* ]] \ + || fail "$full_env Full Companion omitted the validated nRF52 BLE bootstrap" + [[ "$PLATFORMIO_BUILD_FLAGS" == *"RECOVERABLE_EXTERNAL_RADIO=1"* ]] \ + || fail "$full_env Full Companion must keep Companion transports available while radio recovery runs" + if [ "$full_env" = "t1000e_companion_radio_full" ]; then + [[ "$PLATFORMIO_BUILD_FLAGS" == *"COMPANION_FEATURE_BLE_MOTA_SOURCE=1"* ]] \ + || fail "$full_env Full Companion omitted BLE folder seeding" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"COMPANION_FEATURE_BLE_MOTA_SOURCE=0"* ]] \ + || fail "$full_env must not disable its BLE mOTA extension" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"COMPANION_FEATURE_BLE_DFU=0"* ]] \ + || fail "$full_env must not disable its BLE DFU extension" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"COMPANION_FEATURE_DEDICATED_USB_LOGGING=1"* ]] \ + || fail "$full_env must not enable its unstable second USB CDC interface" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"CFG_TUD_CDC=2"* ]] \ + || fail "$full_env must keep a single USB CDC interface" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"MESH_DUAL_CDC_LOGGING=1"* ]] \ + || fail "$full_env must keep USB logging on its primary interface" + [[ "$PLATFORMIO_BUILD_FLAGS" == *"MESH_USB_LOGGING_DISABLED=1"* ]] \ + || fail "$full_env must disable its unstable primary USB logger" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"COMPANION_BLE_PRPH_MTU="* ]] \ + || fail "$full_env must retain the normal Full-Companion BLE bandwidth" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"COMPANION_BLUETOOTH_BOOT_DELAY_MS="* ]] \ + || fail "$full_env must start Bluetooth promptly" + else + [[ "$PLATFORMIO_BUILD_FLAGS" == *"COMPANION_FEATURE_BLE_MOTA_SOURCE=1"* ]] \ + || fail "$full_env Full Companion omitted BLE folder seeding" + [[ "$PLATFORMIO_BUILD_FLAGS" == *"COMPANION_FEATURE_DEDICATED_USB_LOGGING=1"* ]] \ + || fail "$full_env Full Companion omitted dedicated USB logging" + [[ "$PLATFORMIO_BUILD_FLAGS" == *"CFG_TUD_CDC=2"* ]] \ + || fail "$full_env Full Companion omitted its second USB CDC interface" + [[ "$PLATFORMIO_BUILD_FLAGS" == *"MESH_DUAL_CDC_LOGGING=1"* ]] \ + || fail "$full_env Full Companion omitted dedicated USB logging routing" + [[ "$PLATFORMIO_BUILD_FLAGS" != *"MESH_USB_LOGGING_DISABLED=1"* ]] \ + || fail "$full_env unexpectedly disabled USB logging" + fi if ! pio_env_option_contains "$pio_env" build_src_filter "helpers/ota/*.cpp" \ && [[ "$PLATFORMIO_BUILD_SRC_FILTER" != *"helpers/ota/*.cpp"* ]]; then fail "$full_env Full Companion omitted the OTA C++ implementation" @@ -761,6 +794,7 @@ done for logging_target in Station_G2_companion_radio_full RAK_4631_companion_radio_full; do verify_full_logging_contract "$logging_target" 0 off 1 yes no done +verify_full_logging_contract t1000e_companion_radio_full 0 off 1 no no # Synthetic inventory: ESP32 and nRF52 repeater/room-server targets with the # same lean OTA policy. The sensor deliberately has no lean sibling. diff --git a/test/test_nrf52_ble_startup.py b/test/test_nrf52_ble_startup.py index 42159f1f3..26bb0948d 100644 --- a/test/test_nrf52_ble_startup.py +++ b/test/test_nrf52_ble_startup.py @@ -32,8 +32,24 @@ HARNESS = r''' #ifndef COMPANION_FEATURE_BLE_MOTA_SOURCE #define COMPANION_FEATURE_BLE_MOTA_SOURCE 1 #endif +#ifndef COMPANION_FEATURE_BLE_DFU +#define COMPANION_FEATURE_BLE_DFU 1 +#endif +#ifndef COMPANION_BLE_PRPH_MTU +#define COMPANION_BLE_PRPH_MTU 247 +#endif +#ifndef COMPANION_BLE_PRPH_EVENT_LENGTH +#define COMPANION_BLE_PRPH_EVENT_LENGTH 100 +#endif +#ifndef COMPANION_BLE_PRPH_HVN_QUEUE +#define COMPANION_BLE_PRPH_HVN_QUEUE 3 +#endif +#ifndef COMPANION_BLE_PRPH_WRCMD_QUEUE +#define COMPANION_BLE_PRPH_WRCMD_QUEUE 1 +#endif #define BLE_DEBUG_PRINTLN(...) do {} while (0) constexpr int BANDWIDTH_MAX = 3, NRF_SUCCESS = 0, ERROR_NONE = 0; +constexpr int NRF_ERROR_INVALID_STATE = 8; constexpr int BLE_GAP_ADDR_TYPE_RANDOM_STATIC = 1, BLE_TX_POWER = 4; constexpr int BLE_MIN_CONN_INTERVAL = 12, BLE_MAX_CONN_INTERVAL = 24; constexpr int BLE_SLAVE_LATENCY = 4, BLE_CONN_SUP_TIMEOUT = 200; @@ -46,6 +62,7 @@ struct ble_gap_addr_t { int addr_type; uint8_t addr[6]; }; struct ble_gap_conn_params_t { unsigned min_conn_interval, max_conn_interval, slave_latency, conn_sup_timeout; }; int sd_ble_gap_addr_set(ble_gap_addr_t*) { return NRF_SUCCESS; } int sd_ble_gap_addr_get(ble_gap_addr_t* a) { memset(a, 0, sizeof(*a)); return NRF_SUCCESS; } +int sd_softdevice_is_enabled(uint8_t* enabled) { *enabled = 0; return NRF_SUCCESS; } int sd_ble_gap_ppcp_set(ble_gap_conn_params_t*) { return NRF_SUCCESS; } namespace mesh { struct Logging { void println(const char*) {} }; @@ -85,7 +102,16 @@ struct Settings { struct BluefruitStub { Settings Security, Periph, Advertising, ScanResponse; int starts = 0; - void configPrphBandwidth(int) {} + uint16_t mtu = 0, event_length = 0; + uint8_t hvn_queue = 0, wrcmd_queue = 0; + void configPrphConn(uint16_t configured_mtu, uint16_t configured_event_length, + uint8_t configured_hvn_queue, + uint8_t configured_wrcmd_queue) { + mtu = configured_mtu; + event_length = configured_event_length; + hvn_queue = configured_hvn_queue; + wrcmd_queue = configured_wrcmd_queue; + } bool begin() { ++starts; if (fail_softdevice) return false; @@ -154,8 +180,15 @@ int main() { fail_service = fault >= 2 ? fault - 1 : 0; SerialBLEInterface port; const int starts = Bluefruit.starts; - const bool expected = fault == 0; + // UART is the required Companion transport. The mOTA and Nordic DFU + // services are extensions: registration failure must leave UART BLE + // available for pairing, diagnostics, and recovery. + const bool expected = fault != 1 && fault != 2; assert(port.begin("MC-", "T096", 123456) == expected); + assert(Bluefruit.mtu == COMPANION_BLE_PRPH_MTU); + assert(Bluefruit.event_length == COMPANION_BLE_PRPH_EVENT_LENGTH); + assert(Bluefruit.hvn_queue == COMPANION_BLE_PRPH_HVN_QUEUE); + assert(Bluefruit.wrcmd_queue == COMPANION_BLE_PRPH_WRCMD_QUEUE); assert(Bluefruit.starts == starts + 1); assert((instance == &port) == expected); // Retrying a failed partial init must never allocate another stack/FIFO. @@ -175,7 +208,9 @@ class Nrf52BleStartupTest(unittest.TestCase): header = (ROOT / "src/helpers/nrf52/SerialBLEInterface.h").read_text() # Use the real in-class startup initializers, too. fields = "\n".join(line for line in header.splitlines() - if "bool _begin_" in line) + if "bool _begin_" in line + or "bool _mota_available" in line + or "char _begin_failure" in line) self.assertTrue(fields) source = HARNESS.replace("@BEGIN@", begin).replace("@STARTUP_FIELDS@", fields) with tempfile.TemporaryDirectory(prefix="meshcore-ble-start-") as temp: diff --git a/tools/mota/test_mota.py b/tools/mota/test_mota.py index 9742c12c6..544dc802c 100644 --- a/tools/mota/test_mota.py +++ b/tools/mota/test_mota.py @@ -338,6 +338,13 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases(): )[1].split("return 0", 1)[0] assert "-DCFG_TUD_CDC=2" in nrf52_full assert "-DMESH_DUAL_CDC_LOGGING=1" in nrf52_full + # Every nRF52 Full image keeps the proven Bluefruit bootstrap and radio + # recovery profile. T1000-E alone disables its optional packet logger + # because its sole CDC endpoint is reserved for Companion data/terminal. + assert "-DCFG_DEBUG=1" in nrf52_full + assert "-DRECOVERABLE_EXTERNAL_RADIO=1" in nrf52_full + assert 'if [ "$env_name" = "t1000e_companion_radio_full" ]; then' in nrf52_full + assert "-DMESH_USB_LOGGING_DISABLED=1" in nrf52_full esp32_full = full.split("return 0\n fi", 1)[1] assert "-DCFG_TUD_CDC=2" not in esp32_full assert "-DMESH_DUAL_CDC_LOGGING=1" not in esp32_full @@ -410,8 +417,7 @@ def test_canonical_bulk_matrix_omits_runtime_and_transport_aliases(): assert "enterUsbLoggingTerminalMode();" in companion_main assert "usb_logging_terminal_mode" in companion_main assert "_prefs.usb_logging_enabled = 0" in companion - assert 'strcmp(value, "on reboot") == 0' in companion - assert 'strcmp(value, "off reboot") == 0' in companion + assert "mesh::cli::parseLoggingToggle(value, enabled, reboot_if_needed)" in companion assert "reboot required to change USB interfaces" in companion assert "rebooting to change USB interfaces" in companion assert "mesh::saveUsbLoggingBootPreference(enabled)" in companion diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index 192d7564d..bc61eaf76 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -80,6 +80,14 @@ bool radio_init() { return true; // success } +uint32_t radio_fallback_rng_seed() { + uint32_t seed = micros() ^ NRF_FICR->DEVICEID[0] ^ NRF_FICR->DEVICEID[1]; +#ifdef USE_CC310_HW_CRYPTO + mesh::mixCC310Random(reinterpret_cast(&seed), sizeof(seed)); +#endif + return seed; +} + mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity diff --git a/variants/t1000-e/target.h b/variants/t1000-e/target.h index ba6cf0cae..d0421e1b5 100644 --- a/variants/t1000-e/target.h +++ b/variants/t1000-e/target.h @@ -47,4 +47,8 @@ extern VolatileRTCClock rtc_clock; extern T1000SensorManager sensors; bool radio_init(); +// A Companion must remain serviceable over USB/Bluetooth even if the LR1110 +// is temporarily unavailable. This seed keeps the local crypto RNG usable +// until the radio recovery path can collect radio noise again. +uint32_t radio_fallback_rng_seed(); mesh::LocalIdentity radio_new_identity();