Keep ESP32 Bluetooth handles stable and refresh paired client caches
This commit is contained in:
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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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,¶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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in new issue
Block a user