From 1087643ae867b9a8746f00b3ff43d574b59e2457 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Wed, 21 Oct 2026 18:51:02 -0700 Subject: [PATCH] Keep ESP32 Bluetooth handles stable and refresh paired client caches --- .../esp32_ble_radio2_sleep_2026-10-21.md | 85 ++++++ docs/test-results/index.md | 1 + src/helpers/esp32/SerialBLEInterface.cpp | 262 +++++++++++++++++- src/helpers/esp32/SerialBLEInterface.h | 48 ++++ .../esp32_ble_power/mocks/BLEDevice.h | 86 +++++- .../esp32_ble_power/test_bluedroid_power.cpp | 173 +++++++++++- test/test_esp32_ble_power.py | 8 +- test/test_esp32_native_ble_stop.py | 4 +- test/test_sensor_tracker_sleep.py | 29 +- 9 files changed, 682 insertions(+), 14 deletions(-) create mode 100644 docs/test-results/esp32_ble_radio2_sleep_2026-10-21.md diff --git a/docs/test-results/esp32_ble_radio2_sleep_2026-10-21.md b/docs/test-results/esp32_ble_radio2_sleep_2026-10-21.md new file mode 100644 index 000000000..d8475759c --- /dev/null +++ b/docs/test-results/esp32_ble_radio2_sleep_2026-10-21.md @@ -0,0 +1,85 @@ + + + + +# ESP32 Bluetooth shutdown and radio2 CPU idle, 2026-10-21 + +The ESP32 changes cover two gaps identified while qualifying nRF52 CPU sleep: +reversibly stopping the Bluetooth host/controller, and allowing bounded MCU +light-sleep during a settled secondary-radio receive visit. This report +separates host/build coverage, SDK wait measurements and physical packet tests. + +## Behavior and safeguards + +Bluetooth off disconnects clients and completes host/controller teardown in +the main loop. Bluetooth on rebuilds the service without releasing controller +memory permanently or clearing successful bonds. Address, name, PIN and saved +stealth policy remain intact. Pending or failed teardown prevents CPU sleep. +The Bluedroid characteristics have deterministic TX/RX registration order; +heap fragmentation must not change a paired client's cached handles. + +Companion, Repeater, Room Server and Sensor can idle during an eligible +radio2 visit. The decision requires a settled, validated receive profile, +working radio GPIO wake, no pending reception and physically stopped wireless +drivers. An absolute scanner deadline is checked again at sleep entry. The +sleep slice is at most 10 ms, ends at least 4 ms before the visit deadline and +requires at least 2 ms of useful time. Calibration, repair, ownership, queue, +network, USB and role deadlines retain their existing guards. The receiver +stays on. This does not enable sleep through an active Bluetooth/WiFi link. + +## Host tests and builds + +The scheduling, board wake, dispatcher and real role integration suites pass. +Coverage includes expired and wrapping deadlines, stale profile proofs, +pending interrupts, failed wake setup and SDK errors. The BLE adapter tests +exercise actual production code with ASan/UBSan, ownership differences, +partial initialization/teardown failures and repeated off/on cycles. Existing +nRF52 event/profile tests also pass; nRF52 hardware was not touched. + +Full V4.3 FEM-off Companion, V3 Companion, V4 Repeater, Room and Sensor builds +pass their capability and memory gates. ESP32-C6 BLE (Arduino 3.1.3), the +NimBLE-Arduino V4 trial and RC32 Full (Arduino 3.3.11) also compile. RC32 +exposed SDK service ownership and array-deletion defects; a version/hash-bound +build-local repair fixes those sources without editing a shared SDK install. + +Classic Heltec V2 BLE remains below its required 8,192-byte internal RAM +reserve. The baseline has 2,552 bytes free and the candidate has 2,432 bytes +free, a 120-byte regression against an existing failure. Removing an +unnecessary WiFi-driver reference recovered 1,208 bytes. The gate was retained; +this target is not reported as passing. + +## Physical V4 observations + +The test board is a Heltec V4.3 OLED using its matching FEM-off Full Companion +image. The first candidate reports source suffix `278c9ffb`; its published +equivalent after history cleanup is `61eecbd97`. An application-only write and +complete readback preserved the partition table, OTA metadata, identity, +settings and credential digests. No whole-flash backup, bootloader write or +other-device update was performed. + +With Bluetooth/WiFi off and power saving on, only this V4's USB data was +suspended. After the host-loss grace period, a 90-second diagnostic window +recorded **4,635 SDK light-sleep calls and returns**, zero SDK errors, zero +short waits, zero vetoes and zero pending-wake returns. Cumulative call time +was 35.523299 seconds; the longest call was 9,869 microseconds. The mixed +profiles were 910.525 MHz / BW62.5 / SF7 / CR5 and 916 MHz / BW125 / SF9 / CR5. +USB authorization and runtime policy were restored. Core errors remained zero. +These are SDK wait measurements, not electrical current or residency readings. + +Two initial saved-bond Bluetooth off/on sessions passed. Further testing +exposed both a host-side notification-acquisition problem and a real firmware +GATT-handle ordering defect. The host driver now uses ordinary StartNotify. +The original failures are retained; clearing a client cache is not accepted +as proof that firmware handles remain stable or an existing cache migrates. + +Encrypted nonce tests on both frequencies have not yet passed. Earlier +six-packet attempts completed physical transmissions but did not deliver all +expected nonces. An awake control then stopped before transmitting when the +peer rejected a TX-power setting. These failures remain separate from the +successful SDK wait window; that window does not qualify end-to-end radio2 +delivery. The peer is an ESP32 XIAO S3 WIO, independently identified over USB. + +Private artifacts, receipts and traces are retained under +`/home/mesh/hwtest/esp32-ble-radio2-sleep-cd4ea053-20261021/`. +See [power-saving diagnostics](../power_saving_diagnostics.md) for the controls +and the limits of interpreting wait counters as energy savings. diff --git a/docs/test-results/index.md b/docs/test-results/index.md index 316d5499c..882753c1b 100644 --- a/docs/test-results/index.md +++ b/docs/test-results/index.md @@ -70,6 +70,7 @@ Completed validation reports, experiments, and test guides are collected here. O ## Build, storage, memory, and checklists +- [ESP32 Bluetooth shutdown and radio2 CPU idle, 2026-10-21](esp32_ble_radio2_sleep_2026-10-21.md) - [RAK3401 all firmware CPU sleep checks, 2026-10-21](rak3401_all_roles_cpu_sleep_2026-10-21.md) - [Shared contact RAM savings and RAK4631 Full qualification, 2026-10-19](contact_memory_2026-10-19.md) - [Heltec V3 Full memory qualification, 2026-10-19](heltec_v3_full_memory_2026-10-19.md) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 33affb175..475abe6b7 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -4,6 +4,7 @@ #include "esp_mac.h" #include "esp_bt.h" #include +#include #if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO) #include "esp_gap_ble_api.h" #include "esp_bt_main.h" @@ -34,6 +35,216 @@ extern "C" bool bleInUse(void) { #define BLE_POWER_RETRY_MS 1000 +#if !MESH_BLE_USES_NIMBLE +// Bluedroid's BLEService registers its std::map in +// pointer order. Separate allocations can reverse TX/RX after a host restart, +// invalidating a bonded phone's cached value/CCCD handles. Members of one +// holder have a fixed address order, so retain the original TX-first layout. +struct SerialBLEInterface::OrderedGattCharacteristics { + BLECharacteristic tx; + BLECharacteristic rx; + OrderedGattCharacteristics(uint32_t tx_properties, uint32_t rx_properties) + : tx(CHARACTERISTIC_UUID_TX, tx_properties), + rx(CHARACTERISTIC_UUID_RX, rx_properties) {} +}; +#endif + +#if MESH_BLE_AUTO_CACHE_REFRESH +std::atomic SerialBLEInterface::_cacheEventOwner{nullptr}; + +// IDF's queued attribute-table request copies these pointers, not their bytes. +// Keep every referenced value alive through its asynchronous callback. +static uint16_t cachePrimaryServiceUuid = ESP_GATT_UUID_PRI_SERVICE; +static uint8_t cacheMarkerUuid[16] = { + 0x91, 0x6a, 0x63, 0x7d, 0x2d, 0x1b, 0x44, 0x96, + 0x99, 0x61, 0x53, 0x43, 0x4d, 0x43, 0x00, 0x01 +}; +static const esp_gatts_attr_db_t cacheMarkerAttributes[] = {{ + {ESP_GATT_AUTO_RSP}, + {ESP_UUID_LEN_16, reinterpret_cast(&cachePrimaryServiceUuid), + ESP_GATT_PERM_READ, sizeof(cacheMarkerUuid), sizeof(cacheMarkerUuid), + cacheMarkerUuid} +}}; + +void SerialBLEInterface::installCacheGattHandler() { + SerialBLEInterface* expected = nullptr; + if (!_cacheEventOwner.compare_exchange_strong(expected, this)) return; + _previousGattsHandler = BLEDevice::m_customGattsHandler; + BLEDevice::setCustomGattsHandler(cacheGattEvent); +} + +void SerialBLEInterface::retireCacheGattHandler() { + // The host is quiescent before this runs; no callback can still own us. + if (_cacheEventOwner.load(std::memory_order_acquire) != this) return; + if (BLEDevice::m_customGattsHandler == cacheGattEvent) + BLEDevice::setCustomGattsHandler(_previousGattsHandler); + _cacheEventOwner.store(nullptr, std::memory_order_release); + _cacheConnected.store(false, std::memory_order_release); + _cacheAuthenticated.store(false, std::memory_order_release); + _cacheConnection.store(0, std::memory_order_release); + _cacheNusVerifiedConnection.store(0, std::memory_order_release); + _cacheOperation.store(CacheOperation::None, std::memory_order_release); + _cacheEvent.store(CacheOperation::None, std::memory_order_release); + _cacheGattsIf = ESP_GATT_IF_NONE; + _cacheMarker = CacheMarker::Absent; + _cacheMarkerHandle = 0; + _cacheStalled = false; + _cacheVerifiedPeerValid = false; + _cacheAttemptConnection = _cacheEncryptedConnection = 0; + _cacheTriggeredConnection = _cacheOperationConnection = 0; + _cacheFailures = 0; +} + +void SerialBLEInterface::cacheGattEvent(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t* param) { + SerialBLEInterface* owner = _cacheEventOwner.load(std::memory_order_acquire); + if (owner == nullptr) return; + if (owner->_previousGattsHandler != nullptr + && owner->_previousGattsHandler != cacheGattEvent) + owner->_previousGattsHandler(event, gatts_if, param); + if (owner->_callbacksEnabled.load(std::memory_order_acquire) && param != nullptr) + owner->handleCacheGattEvent(event, gatts_if, param); +} + +void SerialBLEInterface::handleCacheGattEvent(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t* param) { + if (event == ESP_GATTS_CONNECT_EVT) { + if (!_cacheConnected.load(std::memory_order_acquire)) { + _cacheGattsIf = gatts_if; + for (size_t i = 0; i < sizeof(param->connect.remote_bda); ++i) + _cacheRemoteAddress[i].store(param->connect.remote_bda[i], std::memory_order_relaxed); + _cacheAuthenticated.store(false, std::memory_order_release); + _cacheConnection.fetch_add(1, std::memory_order_release); + _cacheConnected.store(true, std::memory_order_release); + } + return; + } + if (gatts_if != _cacheGattsIf) return; + if (event == ESP_GATTS_DISCONNECT_EVT) { + _cacheConnected.store(false, std::memory_order_release); + _cacheAuthenticated.store(false, std::memory_order_release); + return; + } + CacheOperation completed; + bool succeeded; + uint16_t handle = 0; + if (event == ESP_GATTS_CREAT_ATTR_TAB_EVT) { + if (param->add_attr_tab.svc_inst_id != 0 + || param->add_attr_tab.svc_uuid.len != ESP_UUID_LEN_128 + || memcmp(param->add_attr_tab.svc_uuid.uuid.uuid128, cacheMarkerUuid, + sizeof(cacheMarkerUuid)) != 0) return; + completed = CacheOperation::Create; + succeeded = param->add_attr_tab.status == ESP_GATT_OK + && param->add_attr_tab.num_handle == 1 + && param->add_attr_tab.handles != nullptr; + if (succeeded) { + handle = param->add_attr_tab.handles[0]; + succeeded = handle != 0; + } + if (!succeeded && param->add_attr_tab.status == ESP_GATT_OK) + handle = UINT16_MAX; // Unknown successful allocation: do not create another. + } else if (event == ESP_GATTS_START_EVT) { + completed = CacheOperation::Start; + handle = param->start.service_handle; + succeeded = param->start.status == ESP_GATT_OK; + } else if (event == ESP_GATTS_DELETE_EVT) { + completed = CacheOperation::Delete; + handle = param->del.service_handle; + succeeded = param->del.status == ESP_GATT_OK; + } else return; + if (_cacheOperation.load(std::memory_order_acquire) != completed) return; + if (completed != CacheOperation::Create && handle != _cacheMarkerHandle) return; + CacheOperation expected = completed; + if (!_cacheOperation.compare_exchange_strong(expected, CacheOperation::Completing)) + return; // Duplicate/unrelated callbacks must not advance the next action. + _cacheEventHandle = handle; // Copy; IDF releases the handles array on return. + _cacheEventSucceeded = succeeded; + _cacheEvent.store(completed, std::memory_order_release); +} + +void SerialBLEInterface::serviceGattCacheRefresh(uint32_t now) { + if (_cacheEventOwner.load(std::memory_order_acquire) != this) return; + const CacheOperation event = _cacheEvent.exchange( + CacheOperation::None, std::memory_order_acq_rel); + if (event != CacheOperation::None) { + if (_cacheEventSucceeded) { + if (event == CacheOperation::Create) { + _cacheMarkerHandle = _cacheEventHandle; + _cacheMarker = CacheMarker::Created; + } else { + _cacheMarker = event == CacheOperation::Start + ? CacheMarker::Started : CacheMarker::Absent; + // The SDK accepted an AUTO database change, NOT an ATT acknowledgement. + // Only real client rediscovery/NUS traffic can prove cache recovery. + _cacheTriggeredConnection = _cacheOperationConnection; + } + } else { + ++_cacheFailures; + if (event == CacheOperation::Create && _cacheEventHandle == UINT16_MAX) + _cacheStalled = true; + } + _cacheOperation.store(CacheOperation::None, std::memory_order_release); + } + if (_cacheOperation.load(std::memory_order_acquire) != CacheOperation::None) { + if (mesh::bleElapsedAtLeast(now, _cacheOperationStarted, 5000)) + _cacheStalled = true; // Never duplicate an accepted operation of unknown outcome. + return; + } + if (_cacheStalled || !_cacheConnected.load(std::memory_order_acquire)) return; + const uint32_t connection = _cacheConnection.load(std::memory_order_acquire); + if (_cacheAttemptConnection != connection) { + _cacheAttemptConnection = connection; + _cacheFailures = 0; + _cacheLastAttempt = now - BLE_POWER_RETRY_MS; + } + if (_cacheFailures >= 3 + || !mesh::bleElapsedAtLeast(now, _cacheLastAttempt, BLE_POWER_RETRY_MS)) return; + uint8_t remote[6]; + for (size_t i = 0; i < sizeof(remote); ++i) + remote[i] = _cacheRemoteAddress[i].load(std::memory_order_relaxed); + if (!_cacheConnected.load(std::memory_order_acquire) + || _cacheConnection.load(std::memory_order_acquire) != connection) return; + if (_cacheEncryptedConnection != connection) { + _cacheLastAttempt = now; + // A stale cached handle can fail before its security permissions are + // consulted. Request the same existing MITM security independently of NUS. + if (esp_ble_set_encryption(remote, ESP_BLE_SEC_ENCRYPT_MITM) == ESP_OK) + _cacheEncryptedConnection = connection; + else ++_cacheFailures; + return; + } + if (!_cacheAuthenticated.load(std::memory_order_acquire)) return; + if (_cacheNusVerifiedConnection.load(std::memory_order_acquire) == connection) { + memcpy(_cacheVerifiedPeer, remote, sizeof(remote)); + _cacheVerifiedPeerValid = true; + } + if (_cacheTriggeredConnection == connection) return; + // Actual use of both NUS handles, rather than local marker completion, + // permits skipping rediscovery for the same peer in this host session. + if (_cacheVerifiedPeerValid + && memcmp(remote, _cacheVerifiedPeer, sizeof(remote)) == 0) { + _cacheTriggeredConnection = connection; + return; + } + _cacheLastAttempt = _cacheOperationStarted = now; + _cacheOperationConnection = connection; + const CacheOperation operation = _cacheMarker == CacheMarker::Absent + ? CacheOperation::Create : (_cacheMarker == CacheMarker::Created + ? CacheOperation::Start : CacheOperation::Delete); + _cacheOperation.store(operation, std::memory_order_release); + esp_err_t result; + if (operation == CacheOperation::Create) + result = esp_ble_gatts_create_attr_tab(cacheMarkerAttributes, _cacheGattsIf, 1, 0); + else if (operation == CacheOperation::Start) + result = esp_ble_gatts_start_service(_cacheMarkerHandle); + else result = esp_ble_gatts_delete_service(_cacheMarkerHandle); + if (result != ESP_OK) { + _cacheOperation.store(CacheOperation::None, std::memory_order_release); + ++_cacheFailures; + } +} +#endif + static bool stopBluetoothController() { if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED && esp_bt_controller_disable() != ESP_OK) return false; @@ -93,11 +304,19 @@ bool SerialBLEInterface::stopBluetoothStack() { #endif #if defined(CONFIG_BLUEDROID_ENABLED) if (!stopBluetoothController()) return false; +#endif +#if MESH_BLE_AUTO_CACHE_REFRESH + retireCacheGattHandler(); #endif BLEDevice::deinit(false); delete pTxDescriptor; +#if !MESH_BLE_USES_NIMBLE + delete _orderedCharacteristics; + _orderedCharacteristics = nullptr; +#else delete pRxCharacteristic; delete pTxCharacteristic; +#endif const bool owns_server = cleanupBluetoothService(pService, 0); pTxDescriptor = NULL; pRxCharacteristic = pTxCharacteristic = NULL; @@ -600,8 +819,17 @@ bool SerialBLEInterface::startBluetoothStack(bool clear_bonds) { rx_properties |= BLECharacteristic::PROPERTY_WRITE_AUTHEN; #endif #endif +#if !MESH_BLE_USES_NIMBLE + _orderedCharacteristics = new (std::nothrow) + OrderedGattCharacteristics(tx_properties, rx_properties); + if (_orderedCharacteristics != nullptr) { + pTxCharacteristic = &_orderedCharacteristics->tx; + pService->addCharacteristic(pTxCharacteristic); + } +#else pTxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID_TX, tx_properties); +#endif if (pTxCharacteristic == NULL) { BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(TX) failed"); _isEnabled = _enableRequested = false; @@ -624,8 +852,13 @@ bool SerialBLEInterface::startBluetoothStack(bool clear_bonds) { pTxCharacteristic->addDescriptor(pTxDescriptor); #endif +#if !MESH_BLE_USES_NIMBLE + pRxCharacteristic = &_orderedCharacteristics->rx; + pService->addCharacteristic(pRxCharacteristic); +#else pRxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID_RX, rx_properties); +#endif if (pRxCharacteristic == NULL) { BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(RX) failed"); _isEnabled = _enableRequested = false; @@ -650,6 +883,9 @@ bool SerialBLEInterface::startBluetoothStack(bool clear_bonds) { configureBondedOnlyAdvertising(_bondedPeer, true); if (suppressed) _advertisingSuppressed.store(true, std::memory_order_release); } +#if MESH_BLE_AUTO_CACHE_REFRESH + installCacheGattHandler(); +#endif _callbacksEnabled.store(true, std::memory_order_release); return true; } @@ -753,6 +989,15 @@ void SerialBLEInterface::onAuthenticationComplete(ble_gap_conn_desc* desc) { #else void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) { if (!_callbacksEnabled.load(std::memory_order_acquire)) return; +#if MESH_BLE_AUTO_CACHE_REFRESH + const uint32_t connection = _cacheConnection.load(std::memory_order_acquire); + bool current_peer = _cacheConnected.load(std::memory_order_acquire); + for (size_t i = 0; i < sizeof(cmpl.bd_addr); ++i) + current_peer = current_peer && cmpl.bd_addr[i] + == _cacheRemoteAddress[i].load(std::memory_order_relaxed); + if (current_peer && connection == _cacheConnection.load(std::memory_order_acquire)) + _cacheAuthenticated.store(cmpl.success, std::memory_order_release); +#endif if (cmpl.success) { BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success"); deviceConnected = true; @@ -926,6 +1171,16 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, int len = pCharacteristic->getLength(); #endif +#if MESH_BLE_AUTO_CACHE_REFRESH + // A stale CCCD write can land in RX after an old handle-order change. + // Only real use of BOTH current NUS handles establishes a valid cache. + if (pCharacteristic == pRxCharacteristic && len > 0 && len <= MAX_FRAME_SIZE + && _cacheAuthenticated.load(std::memory_order_acquire) + && pTxDescriptor != nullptr && pTxDescriptor->getNotifications()) { + _cacheNusVerifiedConnection.store( + _cacheConnection.load(std::memory_order_acquire), std::memory_order_release); + } +#endif if (len > MAX_FRAME_SIZE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len); } else { @@ -1141,7 +1396,12 @@ void SerialBLEInterface::servicePowerState() { if (pServer != NULL) pServer->getAdvertising()->stop(); oldDeviceConnected = deviceConnected = false; } - if (!_shutdownPending) return; + if (!_shutdownPending) { +#if MESH_BLE_AUTO_CACHE_REFRESH + if (_isEnabled) serviceGattCacheRefresh(now); +#endif + return; + } if (_powerAttempted && !mesh::bleElapsedAtLeast(now, _lastPowerAttempt, BLE_POWER_RETRY_MS)) return; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 18210ae51..a0c091560 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -17,6 +17,15 @@ #else #define MESH_BLE_USES_NIMBLE 0 #endif +// Bluedroid AUTO mode rejects the manual Service Changed API. A small +// asynchronous marker service requests its supported AUTO notification. +#if defined(CONFIG_BLUEDROID_ENABLED) && !MESH_BLE_USES_NIMBLE && \ + ((defined(CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_AUTO) && CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_AUTO) || \ + (defined(CONFIG_GATTS_SEND_SERVICE_CHANGE_AUTO) && CONFIG_GATTS_SEND_SERVICE_CHANGE_AUTO)) + #define MESH_BLE_AUTO_CACHE_REFRESH 1 +#else + #define MESH_BLE_AUTO_CACHE_REFRESH 0 +#endif #include #include #include @@ -30,8 +39,47 @@ class SerialBLEInterface : public BaseSerialInterface, BLEService *pService; BLECharacteristic * pTxCharacteristic; BLECharacteristic *pRxCharacteristic; +#if !MESH_BLE_USES_NIMBLE + struct OrderedGattCharacteristics; + OrderedGattCharacteristics* _orderedCharacteristics = nullptr; +#endif #if !defined(MESH_USE_NIMBLE_ARDUINO) BLE2902 *pTxDescriptor; +#endif +#if MESH_BLE_AUTO_CACHE_REFRESH + enum class CacheOperation : uint8_t { None, Create, Start, Delete, Completing }; + enum class CacheMarker : uint8_t { Absent, Created, Started }; + static std::atomic _cacheEventOwner; + gatts_event_handler _previousGattsHandler = nullptr; + std::atomic _cacheOperation{CacheOperation::None}; + std::atomic _cacheEvent{CacheOperation::None}; + std::atomic _cacheConnection{0}; + std::atomic _cacheNusVerifiedConnection{0}; + std::atomic _cacheConnected{false}; + std::atomic _cacheAuthenticated{false}; + std::atomic _cacheGattsIf{ESP_GATT_IF_NONE}; + std::atomic _cacheRemoteAddress[6] = {}; + uint8_t _cacheVerifiedPeer[6] = {}; + bool _cacheVerifiedPeerValid = false; + CacheMarker _cacheMarker = CacheMarker::Absent; + std::atomic _cacheMarkerHandle{0}; + uint16_t _cacheEventHandle = 0; + bool _cacheEventSucceeded = false; + bool _cacheStalled = false; + uint8_t _cacheFailures = 0; + uint32_t _cacheAttemptConnection = 0; + uint32_t _cacheEncryptedConnection = 0; + uint32_t _cacheTriggeredConnection = 0; + uint32_t _cacheOperationConnection = 0; + uint32_t _cacheLastAttempt = 0; + uint32_t _cacheOperationStarted = 0; + static void cacheGattEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param); + void handleCacheGattEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param); + void installCacheGattHandler(); + void retireCacheGattHandler(); + void serviceGattCacheRefresh(uint32_t now); #endif bool deviceConnected; bool oldDeviceConnected; diff --git a/test/fixtures/esp32_ble_power/mocks/BLEDevice.h b/test/fixtures/esp32_ble_power/mocks/BLEDevice.h index b420b0583..a68a00c27 100644 --- a/test/fixtures/esp32_ble_power/mocks/BLEDevice.h +++ b/test/fixtures/esp32_ble_power/mocks/BLEDevice.h @@ -2,10 +2,23 @@ #include #include #include +#include #include #include using esp_err_t = int; constexpr esp_err_t ESP_OK = 0; +using esp_gatt_if_t = uint8_t; +constexpr esp_gatt_if_t ESP_GATT_IF_NONE = 0xff; +constexpr int ESP_GATT_OK=0, ESP_UUID_LEN_16=2, ESP_UUID_LEN_128=16; +constexpr int ESP_GATT_UUID_PRI_SERVICE=0x2800, ESP_GATT_AUTO_RSP=1, ESP_GATT_PERM_READ=1; +constexpr int ESP_BLE_SEC_ENCRYPT_MITM=3; +enum esp_gatts_cb_event_t { ESP_GATTS_CONNECT_EVT, ESP_GATTS_DISCONNECT_EVT, + ESP_GATTS_CREAT_ATTR_TAB_EVT, ESP_GATTS_START_EVT, ESP_GATTS_DELETE_EVT }; +struct esp_bt_uuid_t { uint16_t len=16; union { uint16_t uuid16; uint8_t uuid128[16]; } uuid{}; }; +struct esp_gatts_attr_db_t { + struct { uint8_t auto_rsp; } attr_control; + struct { uint16_t uuid_length; uint8_t* uuid_p; uint16_t perm,max_length,length; uint8_t* value; } att_desc; +}; enum esp_bt_controller_status_t { ESP_BT_CONTROLLER_STATUS_IDLE, ESP_BT_CONTROLLER_STATUS_INITED, ESP_BT_CONTROLLER_STATUS_ENABLED }; enum esp_bluedroid_status_t { ESP_BLUEDROID_STATUS_UNINITIALIZED, ESP_BLUEDROID_STATUS_INITIALIZED, ESP_BLUEDROID_STATUS_ENABLED }; using esp_bd_addr_t = uint8_t[6]; @@ -16,7 +29,12 @@ constexpr int ESP_LE_KEY_PID=1, ESP_BLE_ADV_FLAG_BREDR_NOT_SPT=4; constexpr int ESP_GATT_PERM_READ_ENC_MITM=1, ESP_GATT_PERM_WRITE_ENC_MITM=2; constexpr int ESP_IO_CAP_OUT=0, ESP_LE_AUTH_REQ_SC_MITM_BOND=15; struct esp_ble_auth_cmpl_t { bool success=true; int addr_type=0, fail_reason=0; uint8_t bd_addr[6]={1,2,3,4,5,6}; }; -struct esp_ble_gatts_cb_param_t { struct { uint16_t conn_id=7; } connect, mtu; }; +struct esp_ble_gatts_cb_param_t { + struct { uint16_t conn_id=7; uint8_t remote_bda[6]={1,2,3,4,5,6}; } connect, mtu; + struct { int status=0; esp_bt_uuid_t svc_uuid; uint8_t svc_inst_id=0; uint16_t num_handle=1; uint16_t* handles=nullptr; } add_attr_tab; + struct { int status=0; uint16_t service_handle=0; } start, del; +}; +using gatts_event_handler=void(*)(esp_gatts_cb_event_t,esp_gatt_if_t,esp_ble_gatts_cb_param_t*); struct esp_ble_bond_dev_t { uint8_t bd_addr[6]={1,2,3,4,5,6}; struct { unsigned key_mask=0; struct { uint8_t static_addr[6]={}; int addr_type=0; } pid_key; } bond_key; @@ -32,6 +50,12 @@ inline unsigned servers=0, services=0, characteristics=0, descriptors=0; inline unsigned bond_deletions=0, disconnects=0, stops=0, uuid_adds=0; inline uint8_t address[6]={}; inline uint32_t pin=0; +inline unsigned encryptions=0, cache_creates=0, cache_starts=0, cache_deletes=0; +inline bool in_callback=false; +inline const esp_gatts_attr_db_t* marker_attributes=nullptr; +inline esp_gatt_if_t marker_if=0; +inline uint16_t marker_handle=0x40; +inline int esp_ble_set_encryption_result=0; inline BLEServer* server=nullptr; } inline esp_bt_controller_status_t esp_bt_controller_get_status() { return fake_bt::controller; } @@ -58,6 +82,30 @@ inline int esp_ble_get_bond_device_list(int*, esp_ble_bond_dev_t* dest) { *dest= inline int esp_ble_remove_bond_device(uint8_t*) { ++fake_bt::bond_deletions; return ESP_OK; } inline int esp_ble_gap_clear_whitelist() { return ESP_OK; } inline int esp_ble_gap_update_whitelist(bool, esp_bd_addr_t, esp_ble_wl_addr_type_t) { return ESP_OK; } +inline int esp_ble_set_encryption(uint8_t*, int mode) { + assert(!fake_bt::in_callback && mode==ESP_BLE_SEC_ENCRYPT_MITM); + ++fake_bt::encryptions; + return fake_bt::failure=="encrypt" ? 1 : ESP_OK; +} +inline int esp_ble_gatts_create_attr_tab(const esp_gatts_attr_db_t* db, esp_gatt_if_t id, + uint16_t count, uint8_t instance) { + assert(!fake_bt::in_callback && count==1 && instance==0); + assert(db->attr_control.auto_rsp==ESP_GATT_AUTO_RSP); + assert(*reinterpret_cast(db->att_desc.uuid_p)==ESP_GATT_UUID_PRI_SERVICE); + assert(db->att_desc.length==16); + ++fake_bt::cache_creates; fake_bt::marker_attributes=db; fake_bt::marker_if=id; + return fake_bt::failure=="cache-create" ? 1 : ESP_OK; +} +inline int esp_ble_gatts_start_service(uint16_t handle) { + assert(!fake_bt::in_callback && handle==fake_bt::marker_handle); + ++fake_bt::cache_starts; + return fake_bt::failure=="cache-start" ? 1 : ESP_OK; +} +inline int esp_ble_gatts_delete_service(uint16_t handle) { + assert(!fake_bt::in_callback && handle==fake_bt::marker_handle); + ++fake_bt::cache_deletes; + return fake_bt::failure=="cache-delete" ? 1 : ESP_OK; +} class BLESecurityCallbacks { public: virtual ~BLESecurityCallbacks()=default; @@ -114,16 +162,37 @@ public: static constexpr uint32_t PROPERTY_READ=1, PROPERTY_NOTIFY=2, PROPERTY_WRITE=4; BLECharacteristicCallbacks* callbacks=nullptr; std::vector value; - BLECharacteristic() { ++fake_bt::characteristics; } + std::string uuid; + uint32_t properties; + uint16_t value_handle = 0, cccd_handle = 0; + BLE2902* descriptor=nullptr; + BLECharacteristic(const char* id = "", uint32_t props = 0) + : uuid(id), properties(props) { ++fake_bt::characteristics; } ~BLECharacteristic() { --fake_bt::characteristics; } void setCallbacks(BLECharacteristicCallbacks* cb) { callbacks=cb; } void setAccessPermissions(int) {} - void addDescriptor(BLE2902*) {} + void addDescriptor(BLE2902* value) { descriptor=value; } void setValue(uint8_t* data,size_t len) { value.assign(data,data+len); } uint8_t* getData() { return value.data(); } size_t getLength() { return value.size(); } void notify() { callbacks->onStatus(this,BLECharacteristicCallbacks::SUCCESS_NOTIFY,0); } }; +// Exact legacy SDK behavior: registration follows a std::map keyed by +// object address, rather than UUID or characteristic creation order. +inline void registerPointerOrderedCharacteristics( + const std::vector& characteristics) { + std::map pointer_map; + for (auto* characteristic : characteristics) pointer_map[characteristic] = true; + uint16_t next = 0x0029; + for (const auto& entry : pointer_map) { + auto* characteristic = entry.first; + characteristic->value_handle = next + 1; + next += 2; + characteristic->cccd_handle = 0; + if (characteristic->properties & BLECharacteristic::PROPERTY_NOTIFY) + characteristic->cccd_handle = next++; + } +} class BLEService { public: BLEService() { ++fake_bt::services; } @@ -137,14 +206,15 @@ public: #endif std::vector children; unsigned made=0; - BLECharacteristic* createCharacteristic(const char*,uint32_t) { + void addCharacteristic(BLECharacteristic* child) { children.push_back(child); } + BLECharacteristic* createCharacteristic(const char* id,uint32_t properties) { const char* phase = made++ ? "rx" : "tx"; if (fake_bt::failure==phase) return nullptr; - auto* child = new BLECharacteristic; - children.push_back(child); + auto* child = new BLECharacteristic(id, properties); + addCharacteristic(child); return child; } - void start() {} + void start() { registerPointerOrderedCharacteristics(children); } }; inline BLEAdvertising* fake_advertising=nullptr; class BLEServer { @@ -170,6 +240,8 @@ public: class BLEDevice { public: inline static BLESecurityCallbacks* security=nullptr; + inline static gatts_event_handler m_customGattsHandler=nullptr; + static void setCustomGattsHandler(gatts_event_handler handler) { m_customGattsHandler=handler; } static void init(const std::string& name) { fake_bt::name=name; if(fake_bt::failure=="init") return; diff --git a/test/fixtures/esp32_ble_power/test_bluedroid_power.cpp b/test/fixtures/esp32_ble_power/test_bluedroid_power.cpp index d290f681c..a32783542 100644 --- a/test/fixtures/esp32_ble_power/test_bluedroid_power.cpp +++ b/test/fixtures/esp32_ble_power/test_bluedroid_power.cpp @@ -1,8 +1,26 @@ #include #include +#include using namespace fake_bt; static const uint8_t custom[]={0xc2,1,2,3,4,5}; static unsigned suites; +void* operator new(std::size_t size, const std::nothrow_t&) noexcept { + return failure == "characteristics" ? nullptr : ::operator new(size); +} +void operator delete(void* ptr, const std::nothrow_t&) noexcept { ::operator delete(ptr); } +static void pointer_map_reproduces_cached_cccd_failure() { + // Reversed heap addresses register RX before TX even when TX was added + // first, exactly reproducing the physical2B->2D cached-handle failure. + struct Reversed { + BLECharacteristic rx{"6E400002", BLECharacteristic::PROPERTY_WRITE}; + BLECharacteristic tx{"6E400003", BLECharacteristic::PROPERTY_NOTIFY}; + } reversed; + registerPointerOrderedCharacteristics({&reversed.tx, &reversed.rx}); + assert(reversed.rx.value_handle == 0x002a); + assert(reversed.tx.cccd_handle == 0x002d); + assert(reversed.tx.cccd_handle != 0x002b); // Old cached write is invalid. + ++suites; +} static void retire_global() { delete server; server=nullptr; delete fake_advertising; fake_advertising=nullptr; @@ -25,6 +43,13 @@ static void good(SerialBLEInterface& a) { assert(servers==1 && services==1 && characteristics==2 && descriptors==1); assert(name=="MeshCore-legacy" && pin==246810); assert(memcmp(address,custom,6)==0 && fake_advertising->active); + auto* tx = server->service->children[0]; + auto* rx = server->service->children[1]; + assert(tx->uuid == "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"); + assert(rx->uuid == "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"); + assert(reinterpret_cast(tx) < reinterpret_cast(rx)); + assert(tx->value_handle == 0x002a && tx->cccd_handle == 0x002b); + assert(rx->value_handle == 0x002d); // Saved clients never chase heap order. } static void repeated_cycles() { SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom)); @@ -60,7 +85,7 @@ static void partial_stop_failures_and_late_callbacks() { ++suites; } static void startup_failures() { - for(const char* fault : {"init","server","service","tx","rx"}) { + for(const char* fault : {"init","server","service","characteristics"}) { SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom)); a.enable(); stopped(a); failure=fault; a.enable(); assert(!a.isEnabled() && !a.isConnected()); a.servicePowerState(); @@ -70,7 +95,153 @@ static void startup_failures() { } ++suites; } +#if MESH_BLE_AUTO_CACHE_REFRESH +static unsigned chained_events; +static void prior_handler(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t*) { + ++chained_events; +} +static void tick(SerialBLEInterface& a) { test_millis += 1000; a.servicePowerState(); } +static void emit(esp_gatts_cb_event_t event, esp_ble_gatts_cb_param_t& param, esp_gatt_if_t id=3) { + assert(BLEDevice::m_customGattsHandler); + in_callback=true; + BLEDevice::m_customGattsHandler(event,id,¶m); + in_callback=false; +} +static void connect_cache(SerialBLEInterface& a, uint8_t peer=6) { + server->connected=true; + esp_ble_gatts_cb_param_t param; param.connect.remote_bda[5]=peer; + server->callbacks->onConnect(server,¶m); + emit(ESP_GATTS_CONNECT_EVT,param); + tick(a); // Requests MITM without relying on a possibly stale NUS handle. +} +static void auth_cache(uint8_t peer=6) { + esp_ble_auth_cmpl_t auth; auth.bd_addr[5]=peer; + BLEDevice::security->onAuthenticationComplete(auth); +} +static void disconnect_cache() { + server->connected=false; + server->callbacks->onDisconnect(server); + esp_ble_gatts_cb_param_t param; emit(ESP_GATTS_DISCONNECT_EVT,param); +} +static void created_cache(int status=ESP_GATT_OK, esp_gatt_if_t id=3, bool correct=true, uint8_t instance=0) { + esp_ble_gatts_cb_param_t param; uint16_t handle=marker_handle; + param.add_attr_tab.status=status; + memcpy(param.add_attr_tab.svc_uuid.uuid.uuid128,marker_attributes->att_desc.value,16); + if(!correct) param.add_attr_tab.svc_uuid.uuid.uuid128[0]^=1; + param.add_attr_tab.svc_inst_id=instance; + param.add_attr_tab.handles=&handle; + emit(ESP_GATTS_CREAT_ATTR_TAB_EVT,param,id); + handle=0; // The backend must have copied the ephemeral callback array. +} +static void changed_cache(bool starting, int status=ESP_GATT_OK, uint16_t handle=0) { + esp_ble_gatts_cb_param_t param; + auto& change=starting ? param.start : param.del; + change.status=status; change.service_handle=handle ? handle : marker_handle; + emit(starting ? ESP_GATTS_START_EVT : ESP_GATTS_DELETE_EVT,param); +} +static void reset_cache_stats() { + encryptions=cache_creates=cache_starts=cache_deletes=chained_events=0; + failure.clear(); marker_attributes=nullptr; + BLEDevice::setCustomGattsHandler(prior_handler); +} +static void verify_nus(SerialBLEInterface& a) { + auto* tx=server->service->children[0]; auto* rx=server->service->children[1]; + uint8_t frame[]={22}; rx->setValue(frame,sizeof(frame)); + // A cached CCCD hitting RX is insufficient while actual TX is unsubscribed. + esp_ble_gatts_cb_param_t param; + rx->callbacks->onWrite(rx,¶m); tick(a); + tx->descriptor->setNotifications(true); + rx->callbacks->onWrite(rx,¶m); tick(a); +} +static void cache_async_migration_and_real_nus_proof() { + reset_cache_stats(); + SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom)); a.enable(); + connect_cache(a); assert(encryptions==1 && cache_creates==0); + auth_cache(7); tick(a); assert(cache_creates==0); // Stale auth from another peer. + auth_cache(); tick(a); assert(cache_creates==1 && cache_starts==0); + assert(marker_attributes->att_desc.value[0]==0x91); // Static queued data remains alive. + created_cache(0,4); created_cache(0,3,false); created_cache(0,3,true,1); + tick(a); assert(cache_starts==0); + created_cache(); created_cache(); tick(a); assert(cache_starts==1); + changed_cache(true,0,marker_handle+1); tick(a); assert(cache_deletes==0); + changed_cache(true); changed_cache(true); tick(a); + uint8_t stale_cccd[]={1,0}; + auto* rx=server->service->children[1]; rx->setValue(stale_cccd,sizeof(stale_cccd)); + esp_ble_gatts_cb_param_t stale_write; + rx->callbacks->onWrite(rx,&stale_write); tick(a); // Old CCCD2D is now RX, actual TX remains off. + disconnect_cache(); connect_cache(a); auth_cache(); tick(a); + assert(cache_deletes==1); // Local START was not proof that this peer repaired its cache. + changed_cache(false,1); tick(a); assert(cache_deletes==2); // Known failure retry. + changed_cache(false); tick(a); + verify_nus(a); + disconnect_cache(); connect_cache(a); auth_cache(); tick(a); + assert(cache_creates==1 && cache_deletes==2); // Real handle proof permits fast reconnect. + disconnect_cache(); connect_cache(a,7); auth_cache(7); tick(a); + assert(cache_creates==2); // A different peer still needs migration. + created_cache(); tick(a); changed_cache(true); tick(a); + good(a); disconnect_cache(); stopped(a); + assert(BLEDevice::m_customGattsHandler==prior_handler && bond_deletions==0); + assert(chained_events>0); retire_global(); BLEDevice::setCustomGattsHandler(nullptr); ++suites; +} +static void cache_disconnect_pending_timeout_and_teardown() { + for(const char* stage : {"create","start","delete"}) { + reset_cache_stats(); + SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom)); a.enable(); + connect_cache(a); auth_cache(); tick(a); assert(cache_creates==1); + if(std::string(stage)!="create") { + created_cache(); tick(a); assert(cache_starts==1); + if(std::string(stage)=="delete") { + // Completion while disconnected must not mark its peer cache repaired. + disconnect_cache(); changed_cache(true); tick(a); + connect_cache(a); auth_cache(); tick(a); assert(cache_deletes==1); + } + } + test_millis+=6000; a.servicePowerState(); + const unsigned before=cache_creates+cache_starts+cache_deletes; + tick(a); assert(cache_creates+cache_starts+cache_deletes==before); // No orphan retry. + disconnect_cache(); connect_cache(a,7); auth_cache(7); tick(a); + assert(cache_creates+cache_starts+cache_deletes==before); + disconnect_cache(); stopped(a); + assert(BLEDevice::m_customGattsHandler==prior_handler); + esp_ble_gatts_cb_param_t late; emit(ESP_GATTS_START_EVT,late); // Restored hook only. + a.enable(); good(a); stopped(a); retire_global(); + BLEDevice::setCustomGattsHandler(nullptr); + } + ++suites; +} +static void cache_api_and_async_failures_are_bounded() { + for(const char* fault : {"encrypt","cache-create","async-create","malformed-create","cache-start"}) { + reset_cache_stats(); + SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom)); a.enable(); + if(std::string(fault)=="encrypt") failure=fault; + connect_cache(a); auth_cache(); + if(std::string(fault)=="cache-create") failure=fault; + tick(a); + if(std::string(fault)=="async-create") { created_cache(1); tick(a); created_cache(1); tick(a); created_cache(1); } + if(std::string(fault)=="malformed-create") { + esp_ble_gatts_cb_param_t param; + memcpy(param.add_attr_tab.svc_uuid.uuid.uuid128,marker_attributes->att_desc.value,16); + param.add_attr_tab.num_handle=2; // An unknown successful allocation cannot be retried safely. + emit(ESP_GATTS_CREAT_ATTR_TAB_EVT,param); + } + if(std::string(fault)=="cache-start") { created_cache(); failure=fault; } + tick(a); tick(a); tick(a); tick(a); + const unsigned attempts=encryptions+cache_creates+cache_starts; + tick(a); assert(encryptions+cache_creates+cache_starts==attempts); + assert(encryptions<=3 && cache_creates<=3 && cache_starts<=3); + failure.clear(); disconnect_cache(); stopped(a); retire_global(); + BLEDevice::setCustomGattsHandler(nullptr); + } + ++suites; +} +#endif int main() { +#if MESH_BLE_AUTO_CACHE_REFRESH + cache_async_migration_and_real_nus_proof(); + cache_disconnect_pending_timeout_and_teardown(); + cache_api_and_async_failures_are_bounded(); +#endif + pointer_map_reproduces_cached_cccd_failure(); repeated_cycles(); partial_stop_failures_and_late_callbacks(); startup_failures(); // The legacy library deliberately retains its static server/advertiser // until replacement. Release the final objects after the host has stopped. diff --git a/test/test_esp32_ble_power.py b/test/test_esp32_ble_power.py index dbe0a2723..3f7092bfb 100644 --- a/test/test_esp32_ble_power.py +++ b/test/test_esp32_ble_power.py @@ -14,12 +14,16 @@ class Esp32BlePowerTests(unittest.TestCase): ROOT / "test/fixtures/nimble_companion/mocks") def test_bluedroid_legacy_static_ownership_and_partial_stop_failures(self): - self.run_adapter("test_bluedroid_power.cpp", ["-DCONFIG_BLUEDROID_ENABLED=1"], + self.run_adapter("test_bluedroid_power.cpp", ["-DCONFIG_BLUEDROID_ENABLED=1", "-DCONFIG_BT_GATTS_SEND_SERVICE_CHANGE_AUTO=1"], ROOT / "test/fixtures/esp32_ble_power/mocks") def test_bluedroid_new_library_ownership_and_partial_stop_failures(self): self.run_adapter("test_bluedroid_power.cpp", ["-DCONFIG_BLUEDROID_ENABLED=1", - "-DTEST_BLE_OWNED_SERVER=1"], + "-DTEST_BLE_OWNED_SERVER=1", "-DCONFIG_BT_GATTS_SEND_SERVICE_CHANGE_AUTO=1"], + ROOT / "test/fixtures/esp32_ble_power/mocks") + + def test_bluedroid_manual_mode_keeps_marker_dependency_disabled(self): + self.run_adapter("test_bluedroid_power.cpp", ["-DCONFIG_BLUEDROID_ENABLED=1"], ROOT / "test/fixtures/esp32_ble_power/mocks") def run_adapter(self, fixture, flags, mocks): diff --git a/test/test_esp32_native_ble_stop.py b/test/test_esp32_native_ble_stop.py index 2dd4ee84b..8ba2dc925 100644 --- a/test/test_esp32_native_ble_stop.py +++ b/test/test_esp32_native_ble_stop.py @@ -182,7 +182,7 @@ class NativeNimbleStopTests(unittest.TestCase): cpp.write_text(harness) built = subprocess.run([ os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", "-Werror", - "-fsanitize=address,undefined", "-DCONFIG_NIMBLE_ENABLED=1", + "-fsanitize=address,undefined", "-DCONFIG_NIMBLE_ENABLED=1", "-DMESH_BLE_USES_NIMBLE=1", str(cpp), "-o", str(exe)], text=True, capture_output=True, timeout=30) self.assertEqual(built.returncode, 0, built.stderr) result = subprocess.run([str(exe)], text=True, capture_output=True, timeout=10) @@ -193,7 +193,7 @@ class NativeNimbleStopTests(unittest.TestCase): "!_nativeHostStopped") cpp.write_text(broken) built = subprocess.run([ - os.environ.get("CXX", "c++"), "-std=c++17", "-DCONFIG_NIMBLE_ENABLED=1", + os.environ.get("CXX", "c++"), "-std=c++17", "-DCONFIG_NIMBLE_ENABLED=1", "-DMESH_BLE_USES_NIMBLE=1", str(cpp), "-o", str(exe)], text=True, capture_output=True, timeout=30) self.assertEqual(built.returncode, 0, built.stderr) result = subprocess.run([str(exe)], text=True, capture_output=True, timeout=10) diff --git a/test/test_sensor_tracker_sleep.py b/test/test_sensor_tracker_sleep.py index 88e8a3070..aa5efeb2f 100644 --- a/test/test_sensor_tracker_sleep.py +++ b/test/test_sensor_tracker_sleep.py @@ -130,6 +130,10 @@ TIMER = r''' #include #include #include +#if defined(ESP32_PLATFORM) +int64_t esp_timer_get_time(){return static_cast(millis())*1000;} +#include +#endif using TimerHandle_t=void*; using TickType_t=uint32_t; bool allocate=true,queue_ok=true,active=false; @@ -159,16 +163,21 @@ public: static SoftwareTimer tracker_wake_timer; static void trackerWakeCallback(TimerHandle_t){} @ARM@ -struct Mesh {bool tracker=true;uint32_t sleep_secs=30; +struct Mesh {bool tracker=true,dual=false,profile_ready=false;uint32_t sleep_secs=30; + uint32_t profile_ms=3;unsigned profile_queries=0; bool isTrackerModeEnabled()const{return tracker;} + bool isDualRadioActive()const{return dual;} + bool getDualRadioIdleDelay(uint32_t& out){++profile_queries;out=profile_ms;return profile_ready;} bool canIdleDualRadio()const{return true;} uint32_t getPowerSaveSleepMilliseconds(uint32_t max)const{assert(max==30000);return sleep_secs*1000;} bool getNextNoiseFloorWakeDelay(uint32_t&)const{return false;} bool millisHasNowPassed(uint32_t deadline)const{return (int32_t)(millis()-deadline)>=0;} } the_mesh; struct Board {unsigned sleeps=0,last=0; + unsigned absolute_calls=0;uint32_t deadline=0; void sleep(unsigned value){++sleeps;last=value;} void sleepMilliseconds(unsigned value){++sleeps;last=value;} + void sleepUntilMicroseconds(uint32_t value){++sleeps;++absolute_calls;deadline=value;} } board; static const unsigned long POWERSAVING_FIRST_SLEEP_SECS=120; void sleepGate(bool can_power_save){ @@ -192,6 +201,21 @@ int main(){ now_ms=119999;sleepGate(true);assert(board.sleeps==0); now_ms=120000;sleepGate(true);assert(board.sleeps==1&&board.last==30000); the_mesh.sleep_secs=0;sleepGate(true);assert(board.sleeps==1); +#if defined(ESP32_PLATFORM) + // Execute the real ESP32 profile helper through the extracted Sensor gate. + // A refused scanner slice must retain polling, without a legacy long sleep. + the_mesh.sleep_secs=30;the_mesh.dual=true;sleepGate(true); + assert(the_mesh.profile_queries==1&&board.sleeps==1&&board.absolute_calls==0); + the_mesh.profile_ready=true;the_mesh.profile_ms=1;sleepGate(true); + assert(board.sleeps==1&&board.absolute_calls==0); + the_mesh.profile_ms=3;sleepGate(true); + assert(board.sleeps==2&&board.absolute_calls==1); + assert(board.deadline==120003000); + the_mesh.profile_ms=11;sleepGate(true); + assert(board.sleeps==2&&board.absolute_calls==1); + unsigned queries=the_mesh.profile_queries;sleepGate(false); + assert(the_mesh.profile_queries==queries&&board.sleeps==2); +#endif #endif } ''' @@ -269,6 +293,9 @@ class SensorTrackerSleepTest(unittest.TestCase): with tempfile.TemporaryDirectory(prefix="meshcore-tracker-sleep-") as directory: path = Path(directory) (path / "Arduino.h").write_text(ARDUINO, encoding="ascii") + (path / "esp_timer.h").write_text( + "#pragma once\n#include \nint64_t esp_timer_get_time();\n", + encoding="ascii") cpp, binary = path / "test.cpp", path / "test" cpp.write_text(source, encoding="ascii") result = subprocess.run([