Keep ESP32 Bluetooth handles stable and refresh paired client caches

This commit is contained in:
mikecarper committed 2026-10-21 18:51:02 -07:00
1 parent 669107c3db
commit 1087643ae8
9 files changed
+682 -14

No files matched your search

@@ -0,0 +1,85 @@
<!-- meshcore-hosted-doc-link:start -->
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/esp32_ble_radio2_sleep_2026-10-21/">View this page on MeshCore Docs</a>.</p>
<!-- meshcore-hosted-doc-link:end -->
# 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.
+1
View File
@@ -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)
+261 -1
View File
@@ -4,6 +4,7 @@
#include "esp_mac.h"
#include "esp_bt.h"
#include <stdlib.h>
#include <new>
#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<BLECharacteristic*, ...> 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*> 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<uint8_t*>(&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<BLEDevice>(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;
+48
View File
@@ -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 <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
@@ -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<SerialBLEInterface*> _cacheEventOwner;
gatts_event_handler _previousGattsHandler = nullptr;
std::atomic<CacheOperation> _cacheOperation{CacheOperation::None};
std::atomic<CacheOperation> _cacheEvent{CacheOperation::None};
std::atomic<uint32_t> _cacheConnection{0};
std::atomic<uint32_t> _cacheNusVerifiedConnection{0};
std::atomic<bool> _cacheConnected{false};
std::atomic<bool> _cacheAuthenticated{false};
std::atomic<esp_gatt_if_t> _cacheGattsIf{ESP_GATT_IF_NONE};
std::atomic<uint8_t> _cacheRemoteAddress[6] = {};
uint8_t _cacheVerifiedPeer[6] = {};
bool _cacheVerifiedPeerValid = false;
CacheMarker _cacheMarker = CacheMarker::Absent;
std::atomic<uint16_t> _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;
+79 -7
View File
@@ -2,10 +2,23 @@
#include <Arduino.h>
#include <string>
#include <vector>
#include <map>
#include <cstring>
#include <cassert>
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<uint16_t*>(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<uint8_t> 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<BLECharacteristic*>& characteristics) {
std::map<BLECharacteristic*, bool> 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<BLECharacteristic*> 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;
+172 -1
View File
@@ -1,8 +1,26 @@
#include <helpers/esp32/SerialBLEInterface.h>
#include <cassert>
#include <new>
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<uintptr_t>(tx) < reinterpret_cast<uintptr_t>(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,&param);
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,&param);
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,&param); tick(a);
tx->descriptor->setNotifications(true);
rx->callbacks->onWrite(rx,&param); 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.
+6 -2
View File
@@ -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):
+2 -2
View File
@@ -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)
+28 -1
View File
@@ -130,6 +130,10 @@ TIMER = r'''
#include <cassert>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/PowerManagementUtils.h>
#if defined(ESP32_PLATFORM)
int64_t esp_timer_get_time(){return static_cast<int64_t>(millis())*1000;}
#include <helpers/esp32/ProfileIdleSleep.h>
#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 <stdint.h>\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([