BLE stability: defer disconnect processing, fix data races, harden operations

Critical fixes for NimBLE host task / BLE loop task concurrency:
- Defer all disconnect map cleanup from NimBLE callbacks to loop task via
  SPSC ring buffer, preventing iterator invalidation and use-after-free
- Defer enterErrorRecovery() from callback context to loop task
- Add WDT feed in enterErrorRecovery() host-sync polling loop

Operational hardening:
- Cache NimBLERemoteCharacteristic* pointers in write() to avoid repeated
  service/characteristic lookups per fragment
- Add isConnected() checks before GATT operations (read, enableNotifications)
- Validate peer address in notification callback to guard against handle reuse
- Skip stuck-state detector during CONNECTING/CONN_STARTING states
- Expire stale pending data entries after HANDSHAKE_TIMEOUT (30s)
- Read actual connection RSSI via ble_gap_conn_rssi() for peripheral connections
  instead of hardcoding 0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
torlando-tech
2026-03-03 00:15:24 -05:00
parent 609a3bc62b
commit d6d4eb2c9c
5 changed files with 194 additions and 89 deletions

View File

@@ -220,6 +220,14 @@ void BLEInterface::loop() {
if (resolved.size() == Limits::IDENTITY_SIZE) {
stored_id = resolved;
} else {
// Expire entries that have waited longer than HANDSHAKE_TIMEOUT.
// If a peer sends data but never completes handshake (e.g., disconnect
// during handshake), these entries would stay indefinitely.
if (now - _pending_data_pool[i].queued_at > Timing::HANDSHAKE_TIMEOUT) {
DEBUG("BLEInterface: Expiring stale pending data (no identity after " +
std::to_string((int)(now - _pending_data_pool[i].queued_at)) + "s)");
continue; // Drop this entry
}
// Still no identity — keep for next loop iteration
if (requeue_count != i) {
_pending_data_pool[requeue_count] = _pending_data_pool[i];
@@ -759,8 +767,8 @@ void BLEInterface::onCentralConnected(const ConnectionHandle& conn) {
Bytes mac = conn.peer_address.toBytes();
// Update peer manager
_peer_manager.addDiscoveredPeer(mac, 0);
// Update peer manager with connection RSSI
_peer_manager.addDiscoveredPeer(mac, conn.rssi);
_peer_manager.setPeerState(mac, PeerState::HANDSHAKING);
_peer_manager.setPeerHandle(mac, conn.handle);
@@ -1086,6 +1094,7 @@ void BLEInterface::handleIncomingData(const ConnectionHandle& conn, const Bytes&
PendingData& pending = _pending_data_pool[_pending_data_count];
pending.identity = mac; // Use MAC as temporary key
pending.data = data;
pending.queued_at = Utilities::OS::time();
_pending_data_count++;
}
return;
@@ -1099,6 +1108,7 @@ void BLEInterface::handleIncomingData(const ConnectionHandle& conn, const Bytes&
PendingData& pending = _pending_data_pool[_pending_data_count];
pending.identity = identity;
pending.data = data;
pending.queued_at = Utilities::OS::time();
_pending_data_count++;
}