mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-03-30 21:55:41 +00:00
Add 30-second cooldown after NimBLE host desync recovery before allowing new connection attempts. During desync, client->connect() blocks waiting for a host-task completion event that never arrives, causing WDT crashes. The cooldown skips connection attempts while the host is desynced or recently recovered. Also adds ESP reset reason logging at boot to diagnose crash types (WDT, panic, brownout, etc.) in soak test logs. Soak test results: Run 3 (before) had 17 reboots in ~4 hours with a 12-crash-in-14-minutes loop. Run 4 (after) has 1 early reboot then 19+ hours of continuous uptime with the same desync frequency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2372 lines
83 KiB
C++
2372 lines
83 KiB
C++
/**
|
|
* @file NimBLEPlatform.cpp
|
|
* @brief NimBLE-Arduino implementation for ESP32
|
|
*/
|
|
|
|
#include "NimBLEPlatform.h"
|
|
|
|
#if defined(ESP32) && (defined(USE_NIMBLE) || defined(CONFIG_BT_NIMBLE_ENABLED))
|
|
|
|
#include "Log.h"
|
|
#include "Identity.h"
|
|
#include <algorithm>
|
|
#include <esp_mac.h>
|
|
#include <esp_task_wdt.h>
|
|
|
|
// WiFi coexistence: Check if WiFi is available and connected
|
|
// This is used to add extra delays before BLE connection attempts
|
|
#if __has_include(<WiFi.h>)
|
|
#include <WiFi.h>
|
|
#define HAS_WIFI_COEX 1
|
|
#else
|
|
#define HAS_WIFI_COEX 0
|
|
#endif
|
|
|
|
// NimBLE low-level GAP functions for checking stack state and native connections
|
|
extern "C" {
|
|
#include "nimble/nimble/host/include/host/ble_gap.h"
|
|
#include "nimble/nimble/host/include/host/ble_hs.h"
|
|
|
|
int ble_gap_adv_active(void);
|
|
int ble_gap_disc_active(void);
|
|
int ble_gap_conn_active(void);
|
|
|
|
// Host reset — enqueues a reset event that clears GAP state and resyncs
|
|
// with the BLE controller without touching heap-corrupting deinit paths.
|
|
void ble_hs_sched_reset(int reason);
|
|
}
|
|
|
|
// Defined in patched NimBLEDevice.cpp — set in onReset callback with the reason code.
|
|
// Poll from BLE loop to log via UDP (NimBLE's own logging only reaches serial UART).
|
|
extern volatile int nimble_host_reset_reason;
|
|
|
|
namespace RNS { namespace BLE {
|
|
|
|
//=============================================================================
|
|
// Static Member Initialization
|
|
//=============================================================================
|
|
|
|
// Unclean shutdown flag - persists across soft reboot on ESP32
|
|
// RTC_NOINIT_ATTR places in RTC slow memory which survives soft reset
|
|
#ifdef ESP32
|
|
RTC_NOINIT_ATTR
|
|
#endif
|
|
bool NimBLEPlatform::_unclean_shutdown = false;
|
|
|
|
//=============================================================================
|
|
// State Name Helpers (for logging)
|
|
//=============================================================================
|
|
|
|
const char* masterStateName(MasterState state) {
|
|
switch (state) {
|
|
case MasterState::IDLE: return "IDLE";
|
|
case MasterState::SCAN_STARTING: return "SCAN_STARTING";
|
|
case MasterState::SCANNING: return "SCANNING";
|
|
case MasterState::SCAN_STOPPING: return "SCAN_STOPPING";
|
|
case MasterState::CONN_STARTING: return "CONN_STARTING";
|
|
case MasterState::CONNECTING: return "CONNECTING";
|
|
case MasterState::CONN_CANCELING: return "CONN_CANCELING";
|
|
default: return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
const char* slaveStateName(SlaveState state) {
|
|
switch (state) {
|
|
case SlaveState::IDLE: return "IDLE";
|
|
case SlaveState::ADV_STARTING: return "ADV_STARTING";
|
|
case SlaveState::ADVERTISING: return "ADVERTISING";
|
|
case SlaveState::ADV_STOPPING: return "ADV_STOPPING";
|
|
default: return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
const char* gapStateName(GAPState state) {
|
|
switch (state) {
|
|
case GAPState::UNINITIALIZED: return "UNINITIALIZED";
|
|
case GAPState::INITIALIZING: return "INITIALIZING";
|
|
case GAPState::READY: return "READY";
|
|
case GAPState::MASTER_PRIORITY: return "MASTER_PRIORITY";
|
|
case GAPState::SLAVE_PRIORITY: return "SLAVE_PRIORITY";
|
|
case GAPState::TRANSITIONING: return "TRANSITIONING";
|
|
case GAPState::ERROR_RECOVERY: return "ERROR_RECOVERY";
|
|
default: return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// Constructor / Destructor
|
|
//=============================================================================
|
|
|
|
NimBLEPlatform::NimBLEPlatform() {
|
|
// Initialize connection mutex
|
|
_conn_mutex = xSemaphoreCreateMutex();
|
|
}
|
|
|
|
NimBLEPlatform::~NimBLEPlatform() {
|
|
shutdown();
|
|
if (_conn_mutex) {
|
|
vSemaphoreDelete(_conn_mutex);
|
|
_conn_mutex = nullptr;
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// Lifecycle
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::initialize(const PlatformConfig& config) {
|
|
if (_initialized) {
|
|
WARNING("NimBLEPlatform: Already initialized");
|
|
return true;
|
|
}
|
|
|
|
_config = config;
|
|
|
|
// Initialize NimBLE
|
|
NimBLEDevice::init(_config.device_name);
|
|
|
|
// Address type for ESP32-S3:
|
|
// - BLE_OWN_ADDR_PUBLIC fails with error 13 (ETIMEOUT) for client connections
|
|
// - BLE_OWN_ADDR_RPA_PUBLIC_DEFAULT also fails with error 13
|
|
// - BLE_OWN_ADDR_RANDOM works for client connections
|
|
// Using RANDOM address allows connections to work. Role negotiation is handled
|
|
// by always initiating connections and using identity-based duplicate detection.
|
|
NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM);
|
|
|
|
// Set power level (ESP32)
|
|
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
|
|
|
// Set MTU
|
|
NimBLEDevice::setMTU(_config.preferred_mtu);
|
|
|
|
// Setup server (peripheral mode)
|
|
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
|
|
if (!setupServer()) {
|
|
ERROR("NimBLEPlatform: Failed to setup server");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Setup scan (central mode)
|
|
if (_config.role == Role::CENTRAL || _config.role == Role::DUAL) {
|
|
if (!setupScan()) {
|
|
ERROR("NimBLEPlatform: Failed to setup scan");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_initialized = true;
|
|
|
|
// Set GAP state to READY
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_gap_state = GAPState::READY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
INFO("NimBLEPlatform: Initialized, role: " + std::string(roleToString(_config.role)));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NimBLEPlatform::start() {
|
|
if (!_initialized) {
|
|
ERROR("NimBLEPlatform: Not initialized");
|
|
return false;
|
|
}
|
|
|
|
if (_running) {
|
|
return true;
|
|
}
|
|
|
|
// Start advertising if peripheral mode
|
|
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
|
|
if (!startAdvertising()) {
|
|
WARNING("NimBLEPlatform: Failed to start advertising");
|
|
}
|
|
}
|
|
|
|
_running = true;
|
|
INFO("NimBLEPlatform: Started");
|
|
|
|
return true;
|
|
}
|
|
|
|
void NimBLEPlatform::stop() {
|
|
if (!_running) {
|
|
return;
|
|
}
|
|
|
|
stopScan();
|
|
stopAdvertising();
|
|
disconnectAll();
|
|
|
|
_running = false;
|
|
INFO("NimBLEPlatform: Stopped");
|
|
}
|
|
|
|
void NimBLEPlatform::loop() {
|
|
if (!_running) {
|
|
return;
|
|
}
|
|
|
|
// Process deferred disconnects from NimBLE host task callbacks.
|
|
// Must run before other loop logic to ensure stale connections are cleaned up.
|
|
processPendingDisconnects();
|
|
|
|
// Process deferred error recovery (requested from callback context)
|
|
if (_error_recovery_requested) {
|
|
_error_recovery_requested = false;
|
|
enterErrorRecovery();
|
|
}
|
|
|
|
// Check if continuous scan should stop
|
|
portENTER_CRITICAL(&_state_mux);
|
|
MasterState ms = _master_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (ms == MasterState::SCANNING && _scan_stop_time > 0 && millis() >= _scan_stop_time) {
|
|
DEBUG("NimBLEPlatform: Stopping scan after timeout");
|
|
stopScan();
|
|
|
|
if (_on_scan_complete) {
|
|
_on_scan_complete();
|
|
}
|
|
}
|
|
|
|
// Stuck-state safety net: if GAP hardware is idle but our state machine
|
|
// thinks we're busy, reset state machine. This recovers from missed callbacks
|
|
// (e.g., service discovery disconnect not properly cleaning up state).
|
|
// Skip during CONNECTING — connectNative() can legitimately take up to 10s.
|
|
static uint32_t last_stuck_check = 0;
|
|
uint32_t now_ms = millis();
|
|
if (now_ms - last_stuck_check >= 5000) { // Check every 5 seconds
|
|
last_stuck_check = now_ms;
|
|
|
|
portENTER_CRITICAL(&_state_mux);
|
|
GAPState gs = _gap_state;
|
|
MasterState ms2 = _master_state;
|
|
SlaveState ss = _slave_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
// Don't fire stuck detector while a connection attempt is in progress
|
|
if (ms2 == MasterState::CONNECTING || ms2 == MasterState::CONN_STARTING) {
|
|
// Expected — connect can take several seconds
|
|
} else {
|
|
bool gap_idle = !ble_gap_disc_active() && !ble_gap_adv_active() && !ble_gap_conn_active();
|
|
|
|
if (gap_idle && (gs != GAPState::READY || ms2 != MasterState::IDLE || ss != SlaveState::IDLE)) {
|
|
WARNING(std::string("NimBLEPlatform: Stuck state detected - GAP idle but state=") +
|
|
gapStateName(gs) + " master=" + masterStateName(ms2) +
|
|
" slave=" + slaveStateName(ss) + ". Resetting.");
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_gap_state = GAPState::READY;
|
|
_master_state = MasterState::IDLE;
|
|
_slave_state = SlaveState::IDLE;
|
|
_slave_paused_for_master = false;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
// Restart advertising in dual/peripheral mode
|
|
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
|
|
startAdvertising();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process operation queue
|
|
BLEOperationQueue::process();
|
|
}
|
|
|
|
void NimBLEPlatform::shutdown() {
|
|
// Guard re-entrant shutdown (e.g. recoverBLEStack -> shutdown -> callback -> recoverBLEStack -> shutdown)
|
|
if (_shutting_down) {
|
|
WARNING("NimBLEPlatform: Shutdown already in progress, skipping");
|
|
return;
|
|
}
|
|
|
|
INFO("NimBLEPlatform: Beginning graceful shutdown");
|
|
|
|
// Mark as shutting down FIRST to prevent:
|
|
// 1. Re-entrant shutdown calls
|
|
// 2. Callbacks from doing cleanup (onDisconnect would double-free clients)
|
|
_shutting_down = true;
|
|
|
|
// CONC-H4: Graceful shutdown timeout for active write operations
|
|
const uint32_t SHUTDOWN_TIMEOUT_MS = 10000;
|
|
uint32_t start = millis();
|
|
|
|
// Stop accepting new operations by transitioning GAP state
|
|
// This prevents new connections/operations from starting
|
|
portENTER_CRITICAL(&_state_mux);
|
|
GAPState current_gap = _gap_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_gap == GAPState::READY) {
|
|
transitionGAPState(GAPState::READY, GAPState::TRANSITIONING);
|
|
}
|
|
|
|
// Wait for active write operations to complete
|
|
while (hasActiveWriteOperations() && (millis() - start) < SHUTDOWN_TIMEOUT_MS) {
|
|
DEBUG("NimBLEPlatform: Waiting for " + std::to_string(_active_write_count.load()) +
|
|
" active write operation(s)");
|
|
// DELAY RATIONALE: Shutdown wait polling - check every 100ms for write completion
|
|
delay(100);
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
// Check if we timed out
|
|
if (hasActiveWriteOperations()) {
|
|
WARNING("NimBLEPlatform: Shutdown timeout (" +
|
|
std::to_string(SHUTDOWN_TIMEOUT_MS) + "ms) with " +
|
|
std::to_string(_active_write_count.load()) + " active writes - forcing close");
|
|
_unclean_shutdown = true;
|
|
} else {
|
|
DEBUG("NimBLEPlatform: All operations complete, proceeding with clean shutdown");
|
|
}
|
|
|
|
// Stop advertising and scanning
|
|
stop();
|
|
|
|
// Notify higher layers about all disconnections BEFORE deinit,
|
|
// so the peer manager can reset peer states properly.
|
|
// Do NOT delete clients individually — deinit(true) handles all client cleanup.
|
|
// Process any remaining deferred disconnects before shutdown cleanup
|
|
processPendingDisconnects();
|
|
|
|
if (xSemaphoreTake(_conn_mutex, pdMS_TO_TICKS(1000))) {
|
|
if (_on_disconnected) {
|
|
for (auto& kv : _connections) {
|
|
_on_disconnected(kv.second, 0x16); // 0x16 = local host terminated
|
|
}
|
|
}
|
|
_clients.clear();
|
|
_connections.clear();
|
|
_cached_rx_chars.clear();
|
|
_discovered_devices.clear();
|
|
_discovered_order.clear();
|
|
xSemaphoreGive(_conn_mutex);
|
|
} else {
|
|
WARNING("NimBLEPlatform: Could not acquire mutex for cleanup - forcing cleanup");
|
|
if (_on_disconnected) {
|
|
for (auto& kv : _connections) {
|
|
_on_disconnected(kv.second, 0x16);
|
|
}
|
|
}
|
|
_clients.clear();
|
|
_connections.clear();
|
|
_cached_rx_chars.clear();
|
|
_discovered_devices.clear();
|
|
_discovered_order.clear();
|
|
}
|
|
|
|
// Deinit NimBLE stack — deinit(true) disconnects and deletes all clients/server.
|
|
// We do NOT delete clients individually above to avoid double-free.
|
|
if (_initialized) {
|
|
NimBLEDevice::deinit(true);
|
|
_initialized = false;
|
|
}
|
|
|
|
_server = nullptr;
|
|
_service = nullptr;
|
|
_rx_char = nullptr;
|
|
_tx_char = nullptr;
|
|
_identity_char = nullptr;
|
|
_scan = nullptr;
|
|
_advertising_obj = nullptr;
|
|
|
|
_shutting_down = false;
|
|
|
|
INFO("NimBLEPlatform: Shutdown complete" +
|
|
std::string(wasCleanShutdown() ? "" : " (unclean - verify on boot)"));
|
|
}
|
|
|
|
bool NimBLEPlatform::isRunning() const {
|
|
return _running;
|
|
}
|
|
|
|
//=============================================================================
|
|
// BLE Stack Recovery
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::recoverBLEStack() {
|
|
// NimBLEDevice::deinit() frees memory that the NimBLE host task may have
|
|
// corrupted during sync failures, causing CORRUPT HEAP panics. The only
|
|
// safe recovery is a full reboot. With atomic file persistence, data
|
|
// survives reboots reliably.
|
|
ERROR("NimBLEPlatform: BLE stack stuck - persisting data and rebooting");
|
|
|
|
// Persist any dirty data before reboot
|
|
RNS::Identity::persist_data();
|
|
|
|
delay(100);
|
|
ESP.restart();
|
|
return false; // Won't reach here
|
|
}
|
|
|
|
bool NimBLEPlatform::attemptHostReset() {
|
|
INFO("NimBLEPlatform: Attempting ble_hs_sched_reset (attempt " +
|
|
std::to_string(_host_reset_attempts + 1) + ")");
|
|
|
|
ble_hs_sched_reset(BLE_HS_ETIMEOUT);
|
|
|
|
// Poll for resync up to 3s
|
|
uint32_t start = millis();
|
|
while (!ble_hs_synced() && (millis() - start) < 3000) {
|
|
delay(50);
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
if (ble_hs_synced()) {
|
|
unsigned long elapsed = millis() - start;
|
|
INFO("NimBLEPlatform: Host resync successful after " +
|
|
std::to_string(elapsed) + "ms");
|
|
return true;
|
|
}
|
|
|
|
WARNING("NimBLEPlatform: Host resync failed after 3s");
|
|
return false;
|
|
}
|
|
|
|
//=============================================================================
|
|
// State Machine Implementation
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::transitionMasterState(MasterState expected, MasterState new_state) {
|
|
bool ok = false;
|
|
portENTER_CRITICAL(&_state_mux);
|
|
if (_master_state == expected) {
|
|
_master_state = new_state;
|
|
ok = true;
|
|
}
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
if (ok) {
|
|
DEBUG("NimBLEPlatform: Master state: " + std::string(masterStateName(expected)) +
|
|
" -> " + std::string(masterStateName(new_state)));
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool NimBLEPlatform::transitionSlaveState(SlaveState expected, SlaveState new_state) {
|
|
bool ok = false;
|
|
portENTER_CRITICAL(&_state_mux);
|
|
if (_slave_state == expected) {
|
|
_slave_state = new_state;
|
|
ok = true;
|
|
}
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
if (ok) {
|
|
DEBUG("NimBLEPlatform: Slave state: " + std::string(slaveStateName(expected)) +
|
|
" -> " + std::string(slaveStateName(new_state)));
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool NimBLEPlatform::transitionGAPState(GAPState expected, GAPState new_state) {
|
|
bool ok = false;
|
|
portENTER_CRITICAL(&_state_mux);
|
|
if (_gap_state == expected) {
|
|
_gap_state = new_state;
|
|
ok = true;
|
|
}
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
if (ok) {
|
|
DEBUG("NimBLEPlatform: GAP state: " + std::string(gapStateName(expected)) +
|
|
" -> " + std::string(gapStateName(new_state)));
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool NimBLEPlatform::canStartScan() const {
|
|
bool ok = false;
|
|
portENTER_CRITICAL(&_state_mux);
|
|
ok = (_gap_state == GAPState::READY || _gap_state == GAPState::MASTER_PRIORITY)
|
|
&& _master_state == MasterState::IDLE
|
|
&& !ble_gap_disc_active()
|
|
&& !ble_gap_conn_active(); // Also check no connection in progress
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
return ok;
|
|
}
|
|
|
|
bool NimBLEPlatform::canStartAdvertising() const {
|
|
bool ok = false;
|
|
portENTER_CRITICAL(&_state_mux);
|
|
ok = (_gap_state == GAPState::READY || _gap_state == GAPState::SLAVE_PRIORITY)
|
|
&& _slave_state == SlaveState::IDLE
|
|
&& !ble_gap_adv_active();
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
return ok;
|
|
}
|
|
|
|
bool NimBLEPlatform::canConnect() const {
|
|
bool ok = false;
|
|
portENTER_CRITICAL(&_state_mux);
|
|
ok = (_gap_state == GAPState::READY || _gap_state == GAPState::MASTER_PRIORITY)
|
|
&& _master_state == MasterState::IDLE
|
|
&& !ble_gap_conn_active();
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
return ok;
|
|
}
|
|
|
|
bool NimBLEPlatform::pauseSlaveForMaster() {
|
|
// Check if slave is currently advertising
|
|
portENTER_CRITICAL(&_state_mux);
|
|
SlaveState current_slave = _slave_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_slave == SlaveState::IDLE) {
|
|
DEBUG("NimBLEPlatform: Slave already idle, no pause needed");
|
|
return true; // Already idle
|
|
}
|
|
|
|
if (current_slave == SlaveState::ADVERTISING) {
|
|
// Transition to stopping
|
|
if (!transitionSlaveState(SlaveState::ADVERTISING, SlaveState::ADV_STOPPING)) {
|
|
WARNING("NimBLEPlatform: Failed to transition slave to ADV_STOPPING");
|
|
return false;
|
|
}
|
|
|
|
// Stop advertising
|
|
if (_advertising_obj) {
|
|
_advertising_obj->stop();
|
|
}
|
|
|
|
// Also stop at low level
|
|
if (ble_gap_adv_active()) {
|
|
ble_gap_adv_stop();
|
|
}
|
|
|
|
// Wait for advertising to stop
|
|
uint32_t start = millis();
|
|
while (ble_gap_adv_active() && millis() - start < 2000) {
|
|
// DELAY RATIONALE: Advertising stop polling - check completion every NimBLE scheduler tick (~10ms)
|
|
delay(10);
|
|
esp_task_wdt_reset(); // Feed WDT during blocking wait
|
|
}
|
|
|
|
if (ble_gap_adv_active()) {
|
|
ERROR("NimBLEPlatform: Advertising didn't stop within 2s");
|
|
// Force state to IDLE anyway
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_slave_state = SlaveState::IDLE;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
return false;
|
|
}
|
|
|
|
// Transition to IDLE
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_slave_state = SlaveState::IDLE;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
_slave_paused_for_master = true;
|
|
DEBUG("NimBLEPlatform: Slave paused for master operation");
|
|
return true;
|
|
}
|
|
|
|
// In other states (ADV_STARTING, ADV_STOPPING), wait for completion
|
|
uint32_t start = millis();
|
|
while (millis() - start < 2000) {
|
|
portENTER_CRITICAL(&_state_mux);
|
|
current_slave = _slave_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_slave == SlaveState::IDLE) {
|
|
_slave_paused_for_master = true;
|
|
return true;
|
|
}
|
|
// DELAY RATIONALE: Slave state polling - check completion every NimBLE scheduler tick (~10ms)
|
|
delay(10);
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
WARNING("NimBLEPlatform: Timed out waiting for slave to become idle");
|
|
return false;
|
|
}
|
|
|
|
void NimBLEPlatform::resumeSlave() {
|
|
// Atomically check and clear the paused flag to prevent race conditions
|
|
bool should_resume = false;
|
|
portENTER_CRITICAL(&_state_mux);
|
|
if (_slave_paused_for_master) {
|
|
_slave_paused_for_master = false;
|
|
should_resume = true;
|
|
}
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (!should_resume) {
|
|
return;
|
|
}
|
|
|
|
// Only restart advertising if in peripheral/dual mode
|
|
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
|
|
DEBUG("NimBLEPlatform: Resuming slave (restarting advertising)");
|
|
startAdvertising();
|
|
}
|
|
}
|
|
|
|
void NimBLEPlatform::enterErrorRecovery() {
|
|
// Guard against recursive calls (recoverBLEStack -> start -> enterErrorRecovery)
|
|
static bool in_recovery = false;
|
|
if (in_recovery) {
|
|
WARNING("NimBLEPlatform: Already in error recovery, skipping");
|
|
return;
|
|
}
|
|
in_recovery = true;
|
|
WARNING("NimBLEPlatform: Entering error recovery");
|
|
|
|
// Reset all states atomically
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_gap_state = GAPState::ERROR_RECOVERY;
|
|
_master_state = MasterState::IDLE;
|
|
_slave_state = SlaveState::IDLE;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
// Force stop all operations at low level first
|
|
if (ble_gap_disc_active()) {
|
|
ble_gap_disc_cancel();
|
|
}
|
|
if (ble_gap_adv_active()) {
|
|
ble_gap_adv_stop();
|
|
}
|
|
|
|
// Stop high level objects
|
|
if (_scan) {
|
|
_scan->stop();
|
|
}
|
|
if (_advertising_obj) {
|
|
_advertising_obj->stop();
|
|
}
|
|
|
|
_scan_stop_time = 0;
|
|
_slave_paused_for_master = false;
|
|
|
|
// Wait for host to sync after any reset operation
|
|
// Give the host up to 5s — NimBLE typically re-syncs within 1-3s
|
|
if (!ble_hs_synced()) {
|
|
WARNING("NimBLEPlatform: Host not synced, waiting up to 5s...");
|
|
uint32_t sync_start = millis();
|
|
while (!ble_hs_synced() && (millis() - sync_start) < 5000) {
|
|
delay(50);
|
|
esp_task_wdt_reset();
|
|
}
|
|
if (ble_hs_synced()) {
|
|
INFO("NimBLEPlatform: Host sync restored after " +
|
|
std::to_string(millis() - sync_start) + "ms");
|
|
} else {
|
|
// Don't immediately reboot — track desync time and let startScan()
|
|
// handle the reboot decision based on prolonged desync (30s).
|
|
WARNING("NimBLEPlatform: Host not synced after 5s, will retry on next scan cycle");
|
|
if (_host_desync_since == 0) {
|
|
_host_desync_since = millis();
|
|
}
|
|
in_recovery = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// DELAY RATIONALE: Connect attempt recovery - ESP32-S3 settling time after host sync
|
|
delay(50);
|
|
|
|
// Re-acquire scan object to reset NimBLE internal state
|
|
// This is necessary because NimBLE scan object can get into stuck state
|
|
_scan = NimBLEDevice::getScan();
|
|
if (_scan) {
|
|
_scan->setScanCallbacks(this, false);
|
|
_scan->setActiveScan(_config.scan_mode == ScanMode::ACTIVE);
|
|
_scan->setInterval(_config.scan_interval_ms);
|
|
_scan->setWindow(_config.scan_window_ms);
|
|
_scan->setFilterPolicy(BLE_HCI_SCAN_FILT_NO_WL);
|
|
_scan->setDuplicateFilter(true);
|
|
_scan->clearResults();
|
|
}
|
|
|
|
// Verify GAP is truly idle
|
|
if (!ble_gap_disc_active() && !ble_gap_adv_active() && !ble_gap_conn_active()) {
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_gap_state = GAPState::READY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
INFO("NimBLEPlatform: Error recovery complete, GAP ready");
|
|
} else {
|
|
ERROR("NimBLEPlatform: GAP still busy after recovery attempt");
|
|
}
|
|
|
|
// Restart advertising if in peripheral/dual mode
|
|
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
|
|
DEBUG("NimBLEPlatform: Restarting advertising after recovery");
|
|
startAdvertising();
|
|
}
|
|
|
|
in_recovery = false;
|
|
}
|
|
|
|
//=============================================================================
|
|
// Deferred Disconnect Processing
|
|
//=============================================================================
|
|
|
|
void NimBLEPlatform::queueDisconnect(uint16_t conn_handle, int reason, bool is_peripheral) {
|
|
uint8_t next = (_pending_disc_write + 1) % PENDING_DISC_QUEUE_SIZE;
|
|
if (next == _pending_disc_read) {
|
|
// Queue full — more simultaneous disconnects than queue size
|
|
WARNING("NimBLEPlatform: Pending disconnect queue full, dropping handle=" +
|
|
std::to_string(conn_handle));
|
|
return;
|
|
}
|
|
_pending_disc_queue[_pending_disc_write] = {conn_handle, reason, is_peripheral};
|
|
_pending_disc_write = next;
|
|
}
|
|
|
|
void NimBLEPlatform::processPendingDisconnects() {
|
|
while (_pending_disc_read != _pending_disc_write) {
|
|
PendingDisconnect& pd = _pending_disc_queue[_pending_disc_read];
|
|
|
|
auto conn_it = _connections.find(pd.conn_handle);
|
|
if (conn_it != _connections.end()) {
|
|
ConnectionHandle conn = conn_it->second;
|
|
_connections.erase(conn_it);
|
|
|
|
INFO("NimBLEPlatform: Processing deferred disconnect for " +
|
|
conn.peer_address.toString() + " reason=" + std::to_string(pd.reason));
|
|
|
|
if (!pd.is_peripheral) {
|
|
// Central mode: clean up client object and cached char pointer
|
|
auto client_it = _clients.find(pd.conn_handle);
|
|
if (client_it != _clients.end()) {
|
|
if (client_it->second) {
|
|
NimBLEDevice::deleteClient(client_it->second);
|
|
}
|
|
_clients.erase(client_it);
|
|
}
|
|
_cached_rx_chars.erase(pd.conn_handle);
|
|
}
|
|
|
|
// Clear operation queue for this connection
|
|
clearForConnection(pd.conn_handle);
|
|
|
|
// Notify higher layers
|
|
if (pd.is_peripheral) {
|
|
if (_on_central_disconnected) {
|
|
_on_central_disconnected(conn);
|
|
}
|
|
} else {
|
|
if (_on_disconnected) {
|
|
_on_disconnected(conn, static_cast<uint8_t>(pd.reason));
|
|
}
|
|
}
|
|
|
|
// Restart advertising if in peripheral/dual mode
|
|
if ((_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) &&
|
|
!isAdvertising()) {
|
|
startAdvertising();
|
|
}
|
|
}
|
|
|
|
_pending_disc_read = (_pending_disc_read + 1) % PENDING_DISC_QUEUE_SIZE;
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// Central Mode - Scanning
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::startScan(uint16_t duration_ms) {
|
|
if (!_scan) {
|
|
ERROR("NimBLEPlatform: Scan not initialized");
|
|
return false;
|
|
}
|
|
|
|
// Check current master state
|
|
portENTER_CRITICAL(&_state_mux);
|
|
MasterState current_master = _master_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_master == MasterState::SCANNING) {
|
|
_scan_fail_count = 0; // Reset on successful state
|
|
return true;
|
|
}
|
|
|
|
// Wait for host sync before trying to scan (host may be resetting after connection failure).
|
|
// NimBLE host self-recovers from most desyncs within 1-5s. Only reboot after prolonged desync.
|
|
if (!ble_hs_synced()) {
|
|
// Track when desync started
|
|
if (_host_desync_since == 0) {
|
|
_host_desync_since = millis();
|
|
}
|
|
|
|
DEBUG("NimBLEPlatform: Host not synced, waiting before scan...");
|
|
uint32_t sync_wait = millis();
|
|
while (!ble_hs_synced() && (millis() - sync_wait) < 3000) {
|
|
delay(50);
|
|
}
|
|
if (!ble_hs_synced()) {
|
|
unsigned long desync_duration = millis() - _host_desync_since;
|
|
_scan_fail_count++;
|
|
// Capture NimBLE's internal reset reason (set in patched onReset callback)
|
|
int reset_reason = nimble_host_reset_reason;
|
|
if (reset_reason != 0) {
|
|
nimble_host_reset_reason = 0;
|
|
}
|
|
WARNING("NimBLEPlatform: Host not synced, desync " +
|
|
std::to_string(desync_duration / 1000) + "s (fail " +
|
|
std::to_string(_scan_fail_count) + "/" +
|
|
std::to_string(SCAN_FAIL_RECOVERY_THRESHOLD) +
|
|
", resets=" + std::to_string(_host_reset_attempts) +
|
|
(reset_reason != 0 ? ", nimble_reason=" + std::to_string(reset_reason) : "") +
|
|
")");
|
|
|
|
// Tiered recovery:
|
|
// 0-10s: Wait for natural self-recovery
|
|
// 10s: Try ble_hs_sched_reset() (first attempt)
|
|
// 30s: Try ble_hs_sched_reset() (second attempt)
|
|
// 60s+: Reboot (last resort)
|
|
if (desync_duration >= 10000 && _host_reset_attempts == 0) {
|
|
_host_reset_attempts++;
|
|
if (attemptHostReset()) {
|
|
_host_desync_since = 0;
|
|
_host_reset_attempts = 0;
|
|
_scan_fail_count = 0;
|
|
return false; // Synced — will succeed on next scan cycle
|
|
}
|
|
} else if (desync_duration >= 30000 && _host_reset_attempts == 1) {
|
|
_host_reset_attempts++;
|
|
if (attemptHostReset()) {
|
|
_host_desync_since = 0;
|
|
_host_reset_attempts = 0;
|
|
_scan_fail_count = 0;
|
|
return false;
|
|
}
|
|
} else if (desync_duration >= HOST_DESYNC_REBOOT_MS) {
|
|
ERROR("NimBLEPlatform: Host desynced for " +
|
|
std::to_string(desync_duration / 1000) + "s (conns=" +
|
|
std::to_string(getConnectionCount()) +
|
|
", resets=" + std::to_string(_host_reset_attempts) +
|
|
"), rebooting");
|
|
_scan_fail_count = 0;
|
|
_host_desync_since = 0;
|
|
_host_reset_attempts = 0;
|
|
recoverBLEStack();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Host is synced — clear desync tracking
|
|
if (_host_desync_since != 0) {
|
|
unsigned long recovery_time = millis() - _host_desync_since;
|
|
// Capture any remaining reset reason from NimBLE
|
|
int reset_reason = nimble_host_reset_reason;
|
|
if (reset_reason != 0) {
|
|
nimble_host_reset_reason = 0;
|
|
}
|
|
INFO("NimBLEPlatform: Host re-synced after " + std::to_string(recovery_time) + "ms" +
|
|
(reset_reason != 0 ? " (nimble_reason=" + std::to_string(reset_reason) + ")" : ""));
|
|
_host_desync_since = 0;
|
|
_host_reset_attempts = 0;
|
|
_last_desync_recovery = millis(); // Start cooldown before allowing connections
|
|
}
|
|
|
|
// Log GAP hardware state before checking
|
|
DEBUG("NimBLEPlatform: Pre-scan GAP state: disc=" + std::to_string(ble_gap_disc_active()) +
|
|
" adv=" + std::to_string(ble_gap_adv_active()) +
|
|
" conn=" + std::to_string(ble_gap_conn_active()));
|
|
|
|
// Verify we can start scan
|
|
if (!canStartScan()) {
|
|
DEBUG("NimBLEPlatform: Cannot start scan - state check failed" +
|
|
std::string(" master=") + masterStateName(current_master) +
|
|
" gap_disc=" + std::to_string(ble_gap_disc_active()) +
|
|
" gap_conn=" + std::to_string(ble_gap_conn_active()));
|
|
return false;
|
|
}
|
|
|
|
// Pause slave (advertising) for master operation
|
|
if (!pauseSlaveForMaster()) {
|
|
WARNING("NimBLEPlatform: Failed to pause slave for scan");
|
|
// Try to restart advertising in case it was stopped but flag wasn't set
|
|
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
|
|
startAdvertising();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// DELAY RATIONALE: MTU negotiation settling - allow stack to stabilize before scan start
|
|
delay(20);
|
|
|
|
// Transition to SCAN_STARTING
|
|
if (!transitionMasterState(MasterState::IDLE, MasterState::SCAN_STARTING)) {
|
|
WARNING("NimBLEPlatform: Failed to transition to SCAN_STARTING");
|
|
resumeSlave();
|
|
return false;
|
|
}
|
|
|
|
// Set GAP to master priority
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_gap_state = GAPState::MASTER_PRIORITY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
uint32_t duration_sec = (duration_ms == 0) ? 0 : (duration_ms / 1000);
|
|
if (duration_sec < 1) duration_sec = 1; // Minimum 1 second
|
|
|
|
// Clear results and reconfigure scan before starting
|
|
_scan->clearResults();
|
|
_scan->setActiveScan(_config.scan_mode == ScanMode::ACTIVE);
|
|
_scan->setInterval(_config.scan_interval_ms);
|
|
_scan->setWindow(_config.scan_window_ms);
|
|
|
|
DEBUG("NimBLEPlatform: Starting scan with duration=" + std::to_string(duration_sec) + "s");
|
|
|
|
// NimBLE 2.x: use 0 for continuous scanning (we'll stop it manually in loop())
|
|
bool started = _scan->start(0, false);
|
|
|
|
if (started) {
|
|
// Transition to SCANNING
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::SCANNING;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
_scan_fail_count = 0;
|
|
_lightweight_reset_fails = 0;
|
|
_scan_stop_time = millis() + duration_ms;
|
|
INFO("BLE SCAN: Started, duration=" + std::to_string(duration_ms) + "ms");
|
|
return true;
|
|
}
|
|
|
|
// Scan failed
|
|
ERROR("NimBLEPlatform: Failed to start scan");
|
|
|
|
// Reset state
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::IDLE;
|
|
_gap_state = GAPState::READY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
_scan_fail_count++;
|
|
if (_scan_fail_count >= SCAN_FAIL_RECOVERY_THRESHOLD) {
|
|
_scan_fail_count = 0; // Reset so we don't immediately re-enter after recovery
|
|
_lightweight_reset_fails++;
|
|
|
|
if (_lightweight_reset_fails >= LIGHTWEIGHT_RESET_MAX_FAILS) {
|
|
WARNING("NimBLEPlatform: " + std::to_string(_lightweight_reset_fails) +
|
|
" error recoveries failed to restore scan, escalating to full stack recovery");
|
|
_lightweight_reset_fails = 0;
|
|
recoverBLEStack();
|
|
} else {
|
|
WARNING("NimBLEPlatform: Too many scan failures, entering error recovery (" +
|
|
std::to_string(_lightweight_reset_fails) + "/" +
|
|
std::to_string(LIGHTWEIGHT_RESET_MAX_FAILS) + ")");
|
|
enterErrorRecovery();
|
|
}
|
|
}
|
|
|
|
resumeSlave();
|
|
return false;
|
|
}
|
|
|
|
void NimBLEPlatform::stopScan() {
|
|
portENTER_CRITICAL(&_state_mux);
|
|
MasterState current_master = _master_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_master != MasterState::SCANNING && current_master != MasterState::SCAN_STARTING) {
|
|
return;
|
|
}
|
|
|
|
// Transition to SCAN_STOPPING
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::SCAN_STOPPING;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
DEBUG("NimBLEPlatform: stopScan() called");
|
|
|
|
if (_scan) {
|
|
_scan->stop();
|
|
}
|
|
|
|
// Wait for scan to actually stop
|
|
uint32_t start = millis();
|
|
while (ble_gap_disc_active() && millis() - start < 1000) {
|
|
// DELAY RATIONALE: Scan stop polling - check completion every NimBLE scheduler tick (~10ms)
|
|
delay(10);
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
// Transition to IDLE
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::IDLE;
|
|
_gap_state = GAPState::READY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
_scan_stop_time = 0;
|
|
DEBUG("NimBLEPlatform: Scan stopped");
|
|
|
|
// Resume slave if it was paused
|
|
resumeSlave();
|
|
}
|
|
|
|
bool NimBLEPlatform::isScanning() const {
|
|
portENTER_CRITICAL(&_state_mux);
|
|
bool scanning = (_master_state == MasterState::SCANNING ||
|
|
_master_state == MasterState::SCAN_STARTING);
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
return scanning;
|
|
}
|
|
|
|
//=============================================================================
|
|
// Central Mode - Connections
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::connect(const BLEAddress& address, uint16_t timeout_ms) {
|
|
NimBLEAddress nimAddr = toNimBLE(address);
|
|
|
|
// Skip connections during desync cooldown — connecting while the NimBLE
|
|
// stack is recovering from a desync can hang client->connect() (the host
|
|
// task can't process the completion event), leading to WDT crashes.
|
|
if (_host_desync_since != 0 || (_last_desync_recovery > 0 && millis() - _last_desync_recovery < DESYNC_CONNECT_COOLDOWN_MS)) {
|
|
DEBUG("NimBLEPlatform: Skipping connect during desync cooldown");
|
|
return false;
|
|
}
|
|
|
|
// Rate limit connections to avoid overwhelming the BLE stack
|
|
// Non-blocking: return false if too soon, caller can retry later
|
|
static unsigned long last_connect_time = 0;
|
|
unsigned long now = millis();
|
|
if (now - last_connect_time < 300) { // Reduced from 500ms
|
|
DEBUG("NimBLEPlatform: Connection rate limited, try again later");
|
|
return false; // Non-blocking: fail fast instead of delay
|
|
}
|
|
last_connect_time = millis();
|
|
|
|
// Check if already connected
|
|
if (isConnectedTo(address)) {
|
|
WARNING("NimBLEPlatform: Already connected to " + address.toString());
|
|
return false;
|
|
}
|
|
|
|
// Check connection limit
|
|
if (getConnectionCount() >= _config.max_connections) {
|
|
WARNING("NimBLEPlatform: Connection limit reached");
|
|
return false;
|
|
}
|
|
|
|
// Verify we can connect using state machine
|
|
if (!canConnect()) {
|
|
portENTER_CRITICAL(&_state_mux);
|
|
MasterState ms = _master_state;
|
|
GAPState gs = _gap_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
WARNING("NimBLEPlatform: Cannot connect - state check failed" +
|
|
std::string(" master=") + masterStateName(ms) +
|
|
" gap=" + gapStateName(gs));
|
|
return false;
|
|
}
|
|
|
|
// Stop scanning if active
|
|
portENTER_CRITICAL(&_state_mux);
|
|
MasterState current_master = _master_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_master == MasterState::SCANNING) {
|
|
DEBUG("NimBLEPlatform: Stopping scan before connect");
|
|
stopScan();
|
|
}
|
|
|
|
// Pause slave (advertising) for master operation
|
|
if (!pauseSlaveForMaster()) {
|
|
WARNING("NimBLEPlatform: Failed to pause slave for connect");
|
|
// Try to restart advertising in case it was stopped but flag wasn't set
|
|
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
|
|
startAdvertising();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Transition to CONN_STARTING
|
|
if (!transitionMasterState(MasterState::IDLE, MasterState::CONN_STARTING)) {
|
|
WARNING("NimBLEPlatform: Failed to transition to CONN_STARTING");
|
|
resumeSlave();
|
|
return false;
|
|
}
|
|
|
|
// Set GAP to master priority
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_gap_state = GAPState::MASTER_PRIORITY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
// DELAY RATIONALE: Service discovery settling - allow stack to finalize after advertising stop
|
|
delay(20);
|
|
|
|
// Verify GAP is truly idle
|
|
if (ble_gap_disc_active() || ble_gap_adv_active()) {
|
|
ERROR("NimBLEPlatform: GAP not idle before connect, entering error recovery");
|
|
enterErrorRecovery();
|
|
resumeSlave();
|
|
return false;
|
|
}
|
|
|
|
// Check if there's still a pending connection
|
|
if (ble_gap_conn_active()) {
|
|
WARNING("NimBLEPlatform: Connection still pending in GAP, waiting...");
|
|
uint32_t start = millis();
|
|
while (ble_gap_conn_active() && millis() - start < 1000) {
|
|
// DELAY RATIONALE: Service discovery polling - check completion per scheduler tick
|
|
delay(10);
|
|
esp_task_wdt_reset();
|
|
}
|
|
if (ble_gap_conn_active()) {
|
|
ERROR("NimBLEPlatform: GAP connection still active after timeout");
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::IDLE;
|
|
_gap_state = GAPState::READY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
resumeSlave();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Delete any existing clients for this address to ensure clean state
|
|
NimBLEClient* existingClient = NimBLEDevice::getClientByPeerAddress(nimAddr);
|
|
while (existingClient) {
|
|
DEBUG("NimBLEPlatform: Deleting existing client for " + address.toString());
|
|
if (existingClient->isConnected()) {
|
|
existingClient->disconnect();
|
|
}
|
|
NimBLEDevice::deleteClient(existingClient);
|
|
existingClient = NimBLEDevice::getClientByPeerAddress(nimAddr);
|
|
}
|
|
|
|
DEBUG("NimBLEPlatform: Connecting to " + address.toString() +
|
|
" timeout=" + std::to_string(timeout_ms / 1000) + "s");
|
|
|
|
// Transition to CONNECTING
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::CONNECTING;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
// Use native NimBLE connection
|
|
bool connected = connectNative(address, timeout_ms);
|
|
|
|
if (!connected) {
|
|
ERROR("NimBLEPlatform: Native connection failed to " + address.toString());
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::IDLE;
|
|
_gap_state = GAPState::READY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
resumeSlave();
|
|
return false;
|
|
}
|
|
|
|
// Connection succeeded - transition states
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_master_state = MasterState::IDLE;
|
|
_gap_state = GAPState::READY;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
// Remove from discovered devices cache
|
|
std::string addrKey = nimAddr.toString().c_str();
|
|
if (xSemaphoreTake(_conn_mutex, pdMS_TO_TICKS(100))) {
|
|
auto cachedIt = _discovered_devices.find(addrKey);
|
|
if (cachedIt != _discovered_devices.end()) {
|
|
// Also remove from order tracking
|
|
auto orderIt = std::find(_discovered_order.begin(),
|
|
_discovered_order.end(), addrKey);
|
|
if (orderIt != _discovered_order.end()) {
|
|
_discovered_order.erase(orderIt);
|
|
}
|
|
_discovered_devices.erase(cachedIt);
|
|
}
|
|
xSemaphoreGive(_conn_mutex);
|
|
} else {
|
|
// CONC-M5: Log timeout failures
|
|
WARNING("NimBLEPlatform: conn_mutex timeout (100ms) during cache update");
|
|
}
|
|
|
|
DEBUG("NimBLEPlatform: Connection established successfully");
|
|
|
|
// Resume slave operations
|
|
resumeSlave();
|
|
|
|
return true;
|
|
}
|
|
|
|
//=============================================================================
|
|
// Native NimBLE Connection (bypasses NimBLE-Arduino wrapper)
|
|
//=============================================================================
|
|
|
|
int NimBLEPlatform::nativeGapEventHandler(struct ble_gap_event* event, void* arg) {
|
|
NimBLEPlatform* platform = static_cast<NimBLEPlatform*>(arg);
|
|
|
|
switch (event->type) {
|
|
case BLE_GAP_EVENT_CONNECT:
|
|
DEBUG("NimBLEPlatform::nativeGapEventHandler: BLE_GAP_EVENT_CONNECT status=" +
|
|
std::to_string(event->connect.status) +
|
|
" handle=" + std::to_string(event->connect.conn_handle));
|
|
|
|
platform->_native_connect_result = event->connect.status;
|
|
if (event->connect.status == 0) {
|
|
platform->_native_connect_success = true;
|
|
platform->_native_connect_handle = event->connect.conn_handle;
|
|
// Reset failure counters on successful connection
|
|
platform->_conn_establish_fail_count = 0;
|
|
} else {
|
|
platform->_native_connect_success = false;
|
|
}
|
|
platform->_native_connect_pending = false;
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_DISCONNECT: {
|
|
uint16_t disc_handle = event->disconnect.conn.conn_handle;
|
|
int disc_reason = event->disconnect.reason;
|
|
|
|
DEBUG("NimBLEPlatform::nativeGapEventHandler: BLE_GAP_EVENT_DISCONNECT reason=" +
|
|
std::to_string(disc_reason) +
|
|
" handle=" + std::to_string(disc_handle));
|
|
|
|
// If we were still waiting for connection, this is a failure
|
|
if (platform->_native_connect_pending) {
|
|
platform->_native_connect_result = disc_reason;
|
|
platform->_native_connect_success = false;
|
|
platform->_native_connect_pending = false;
|
|
|
|
// Track connection establishment failures (574 = BLE_ERR_CONN_ESTABLISHMENT).
|
|
// These commonly cause brief host desyncs that self-recover.
|
|
// Don't escalate to enterErrorRecovery here — let the time-based
|
|
// desync tracking in startScan() handle reboot decisions.
|
|
if (disc_reason == 574) {
|
|
platform->_conn_establish_fail_count++;
|
|
WARNING("NimBLEPlatform: Connection establishment failed (574), count=" +
|
|
std::to_string(platform->_conn_establish_fail_count));
|
|
}
|
|
}
|
|
|
|
// During shutdown, skip cleanup — shutdown() handles it
|
|
if (platform->_shutting_down) {
|
|
break;
|
|
}
|
|
|
|
// Defer map cleanup to BLE loop task to avoid data race.
|
|
// This callback runs in the NimBLE host task while the BLE loop task
|
|
// may be iterating _connections/_clients concurrently.
|
|
platform->queueDisconnect(disc_handle, disc_reason, false);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
DEBUG("NimBLEPlatform::nativeGapEventHandler: event type=" + std::to_string(event->type));
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool NimBLEPlatform::connectNative(const BLEAddress& address, uint16_t timeout_ms) {
|
|
INFO("NimBLEPlatform: Connecting to " + address.toString() + " type=" + std::to_string(address.type));
|
|
|
|
// Verify host-controller sync — don't trigger recovery here,
|
|
// just return false and let the host recover naturally. A single
|
|
// connection failure (574) can cause a temporary host reset that
|
|
// resolves on its own. Triggering recoverBLEStack() here would
|
|
// kill all existing connections unnecessarily.
|
|
if (!ble_hs_synced()) {
|
|
WARNING("NimBLEPlatform: Host not synced before connect, skipping");
|
|
return false;
|
|
}
|
|
|
|
if (address.type > 3) {
|
|
ERROR("NimBLEPlatform: Invalid address type " + std::to_string(address.type));
|
|
return false;
|
|
}
|
|
|
|
// Convert to NimBLE address
|
|
NimBLEAddress nimAddr = toNimBLE(address);
|
|
|
|
// Use NimBLEClient for connection — this properly manages the GAP event handler,
|
|
// connection handle tracking, and service discovery. Raw ble_gap_connect() bypasses
|
|
// NimBLE's internal client management, causing service discovery to fail.
|
|
NimBLEClient* client = NimBLEDevice::createClient(nimAddr);
|
|
if (!client) {
|
|
ERROR("NimBLEPlatform: Failed to create NimBLE client");
|
|
return false;
|
|
}
|
|
|
|
client->setClientCallbacks(this, false);
|
|
client->setConnectionParams(24, 48, 0, 400); // 30-60ms interval, 4.0s supervision timeout
|
|
client->setConnectTimeout(timeout_ms); // milliseconds
|
|
|
|
// Suppress _on_connected in onConnect callback — we'll fire it from here
|
|
// after connect() returns. The onConnect callback runs in the NimBLE host
|
|
// task, and _on_connected triggers blocking GATT operations (service
|
|
// discovery) that would deadlock the host task.
|
|
_native_connect_pending = true;
|
|
|
|
// Feed WDT before blocking connect — NimBLE connect can take several seconds
|
|
// with WiFi coexistence, and the BLE task is subscribed to the 10s WDT
|
|
esp_task_wdt_reset();
|
|
|
|
// Connect (blocking) — NimBLE handles GAP event management internally
|
|
bool connected = client->connect(nimAddr, false); // deleteAttributes=false
|
|
|
|
esp_task_wdt_reset(); // Feed WDT after connect returns
|
|
_native_connect_pending = false;
|
|
|
|
if (!connected) {
|
|
INFO("NimBLEPlatform: Connection failed to " + address.toString());
|
|
NimBLEDevice::deleteClient(client);
|
|
return false;
|
|
}
|
|
|
|
// onConnect callback already stored in _connections/_clients.
|
|
// Update MTU (exchange happens after onConnect fires).
|
|
uint16_t conn_handle = client->getConnHandle();
|
|
uint16_t negotiated_mtu = client->getMTU() - MTU::ATT_OVERHEAD;
|
|
|
|
auto conn_it = _connections.find(conn_handle);
|
|
if (conn_it != _connections.end()) {
|
|
conn_it->second.mtu = negotiated_mtu;
|
|
}
|
|
|
|
INFO("NimBLEPlatform: Connected to " + address.toString() +
|
|
" handle=" + std::to_string(conn_handle) +
|
|
" MTU=" + std::to_string(negotiated_mtu));
|
|
|
|
// Fire _on_connected from THIS task (BLEInterface loop), not the host task.
|
|
// This allows the callback to safely do blocking GATT operations.
|
|
if (_on_connected) {
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
_on_connected(conn);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NimBLEPlatform::disconnect(uint16_t conn_handle) {
|
|
auto conn_it = _connections.find(conn_handle);
|
|
if (conn_it == _connections.end()) {
|
|
return false;
|
|
}
|
|
|
|
ConnectionHandle& conn = conn_it->second;
|
|
|
|
if (conn.local_role == Role::CENTRAL) {
|
|
// We are central - disconnect client
|
|
auto client_it = _clients.find(conn_handle);
|
|
if (client_it != _clients.end() && client_it->second) {
|
|
client_it->second->disconnect();
|
|
return true;
|
|
}
|
|
} else {
|
|
// We are peripheral - disconnect via server
|
|
if (_server) {
|
|
_server->disconnect(conn_handle);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void NimBLEPlatform::disconnectAll() {
|
|
// Disconnect all clients (central mode)
|
|
for (auto& kv : _clients) {
|
|
if (kv.second && kv.second->isConnected()) {
|
|
kv.second->disconnect();
|
|
}
|
|
}
|
|
|
|
// Disconnect all server connections (peripheral mode)
|
|
if (_server) {
|
|
std::vector<uint16_t> handles;
|
|
for (const auto& kv : _connections) {
|
|
if (kv.second.local_role == Role::PERIPHERAL) {
|
|
handles.push_back(kv.first);
|
|
}
|
|
}
|
|
for (uint16_t handle : handles) {
|
|
_server->disconnect(handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NimBLEPlatform::requestMTU(uint16_t conn_handle, uint16_t mtu) {
|
|
auto client_it = _clients.find(conn_handle);
|
|
if (client_it == _clients.end() || !client_it->second) {
|
|
return false;
|
|
}
|
|
|
|
// NimBLE handles MTU exchange automatically, but we can try to update
|
|
// The MTU change callback will be invoked
|
|
return true;
|
|
}
|
|
|
|
bool NimBLEPlatform::discoverServices(uint16_t conn_handle) {
|
|
auto client_it = _clients.find(conn_handle);
|
|
if (client_it == _clients.end() || !client_it->second) {
|
|
return false;
|
|
}
|
|
|
|
NimBLEClient* client = client_it->second;
|
|
|
|
// Get our service
|
|
NimBLERemoteService* service = client->getService(UUID::SERVICE);
|
|
if (!service) {
|
|
ERROR("NimBLEPlatform: Service not found");
|
|
if (_on_services_discovered) {
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
_on_services_discovered(conn, false);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Get characteristics
|
|
NimBLERemoteCharacteristic* rxChar = service->getCharacteristic(UUID::RX_CHAR);
|
|
NimBLERemoteCharacteristic* txChar = service->getCharacteristic(UUID::TX_CHAR);
|
|
NimBLERemoteCharacteristic* idChar = service->getCharacteristic(UUID::IDENTITY_CHAR);
|
|
|
|
if (!rxChar || !txChar) {
|
|
ERROR("NimBLEPlatform: Required characteristics not found");
|
|
if (_on_services_discovered) {
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
_on_services_discovered(conn, false);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Update connection with characteristic handles
|
|
auto conn_it = _connections.find(conn_handle);
|
|
if (conn_it != _connections.end()) {
|
|
conn_it->second.rx_char_handle = rxChar->getHandle();
|
|
conn_it->second.tx_char_handle = txChar->getHandle();
|
|
if (idChar) {
|
|
conn_it->second.identity_handle = idChar->getHandle();
|
|
}
|
|
conn_it->second.state = ConnectionState::READY;
|
|
}
|
|
|
|
DEBUG("NimBLEPlatform: Services discovered for " + std::to_string(conn_handle));
|
|
|
|
if (_on_services_discovered) {
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
_on_services_discovered(conn, true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//=============================================================================
|
|
// Peripheral Mode
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::startAdvertising() {
|
|
if (!_advertising_obj) {
|
|
if (!setupAdvertising()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check current slave state
|
|
portENTER_CRITICAL(&_state_mux);
|
|
SlaveState current_slave = _slave_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_slave == SlaveState::ADVERTISING) {
|
|
return true;
|
|
}
|
|
|
|
// Wait for host sync before advertising (host may be resetting)
|
|
if (!ble_hs_synced()) {
|
|
uint32_t sync_wait = millis();
|
|
while (!ble_hs_synced() && (millis() - sync_wait) < 1000) {
|
|
delay(50);
|
|
esp_task_wdt_reset();
|
|
}
|
|
if (!ble_hs_synced()) {
|
|
DEBUG("NimBLEPlatform: Host not synced, cannot start advertising");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if we can start advertising
|
|
if (!canStartAdvertising()) {
|
|
DEBUG("NimBLEPlatform: Cannot start advertising - state check failed" +
|
|
std::string(" slave=") + slaveStateName(current_slave) +
|
|
" gap_adv=" + std::to_string(ble_gap_adv_active()));
|
|
return false;
|
|
}
|
|
|
|
// Transition to ADV_STARTING
|
|
if (!transitionSlaveState(SlaveState::IDLE, SlaveState::ADV_STARTING)) {
|
|
WARNING("NimBLEPlatform: Failed to transition to ADV_STARTING");
|
|
return false;
|
|
}
|
|
|
|
if (_advertising_obj->start()) {
|
|
// Transition to ADVERTISING
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_slave_state = SlaveState::ADVERTISING;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
DEBUG("NimBLEPlatform: Advertising started");
|
|
return true;
|
|
}
|
|
|
|
// Failed to start
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_slave_state = SlaveState::IDLE;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
ERROR("NimBLEPlatform: Failed to start advertising");
|
|
return false;
|
|
}
|
|
|
|
void NimBLEPlatform::stopAdvertising() {
|
|
portENTER_CRITICAL(&_state_mux);
|
|
SlaveState current_slave = _slave_state;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
if (current_slave != SlaveState::ADVERTISING && current_slave != SlaveState::ADV_STARTING) {
|
|
return;
|
|
}
|
|
|
|
// Transition to ADV_STOPPING
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_slave_state = SlaveState::ADV_STOPPING;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
DEBUG("NimBLEPlatform: stopAdvertising() called");
|
|
|
|
if (_advertising_obj) {
|
|
_advertising_obj->stop();
|
|
}
|
|
|
|
// Also stop at low level
|
|
if (ble_gap_adv_active()) {
|
|
ble_gap_adv_stop();
|
|
}
|
|
|
|
// Wait for advertising to actually stop
|
|
uint32_t start = millis();
|
|
while (ble_gap_adv_active() && millis() - start < 1000) {
|
|
// DELAY RATIONALE: Loop iteration throttle - prevent tight loop CPU consumption
|
|
delay(10);
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
// Transition to IDLE
|
|
portENTER_CRITICAL(&_state_mux);
|
|
_slave_state = SlaveState::IDLE;
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
DEBUG("NimBLEPlatform: Advertising stopped");
|
|
}
|
|
|
|
bool NimBLEPlatform::isAdvertising() const {
|
|
portENTER_CRITICAL(&_state_mux);
|
|
bool advertising = (_slave_state == SlaveState::ADVERTISING ||
|
|
_slave_state == SlaveState::ADV_STARTING);
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
return advertising;
|
|
}
|
|
|
|
bool NimBLEPlatform::setAdvertisingData(const Bytes& data) {
|
|
// Custom advertising data not directly supported by high-level API
|
|
// Use the service UUID instead
|
|
return true;
|
|
}
|
|
|
|
void NimBLEPlatform::setIdentityData(const Bytes& identity) {
|
|
_identity_data = identity;
|
|
|
|
if (_identity_char && identity.size() > 0) {
|
|
_identity_char->setValue(identity.data(), identity.size());
|
|
DEBUG("NimBLEPlatform: Identity data set");
|
|
}
|
|
|
|
// Update device name to include identity prefix (Protocol v2.2)
|
|
// Format: "RNS-" + first 3 bytes of identity as hex (6 chars)
|
|
// This allows peers to recognize us across MAC rotations
|
|
if (identity.size() >= 3 && _advertising_obj) {
|
|
char name[11]; // "RNS-" (4) + 6 hex chars + null
|
|
snprintf(name, sizeof(name), "RNS-%02x%02x%02x",
|
|
identity.data()[0], identity.data()[1], identity.data()[2]);
|
|
|
|
_advertising_obj->setName(name);
|
|
DEBUG("NimBLEPlatform: Updated advertised name to " + std::string(name));
|
|
|
|
// Restart advertising if currently active to apply new name
|
|
if (isAdvertising()) {
|
|
stopAdvertising();
|
|
startAdvertising();
|
|
}
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// GATT Operations
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::write(uint16_t conn_handle, const Bytes& data, bool response) {
|
|
auto conn_it = _connections.find(conn_handle);
|
|
if (conn_it == _connections.end()) {
|
|
DEBUG("NimBLEPlatform::write: no connection for handle " + std::to_string(conn_handle));
|
|
return false;
|
|
}
|
|
|
|
ConnectionHandle& conn = conn_it->second;
|
|
|
|
if (conn.local_role == Role::CENTRAL) {
|
|
// We are central - write to peripheral's RX characteristic
|
|
auto client_it = _clients.find(conn_handle);
|
|
if (client_it == _clients.end() || !client_it->second) {
|
|
WARNING("NimBLEPlatform::write: no client for handle " + std::to_string(conn_handle));
|
|
return false;
|
|
}
|
|
|
|
NimBLEClient* client = client_it->second;
|
|
if (!client->isConnected()) {
|
|
WARNING("NimBLEPlatform::write: client not connected for handle " + std::to_string(conn_handle));
|
|
return false;
|
|
}
|
|
|
|
// Use cached RX characteristic pointer to avoid repeated service/char lookups
|
|
NimBLERemoteCharacteristic* rxChar = nullptr;
|
|
auto cached_it = _cached_rx_chars.find(conn_handle);
|
|
if (cached_it != _cached_rx_chars.end()) {
|
|
rxChar = cached_it->second;
|
|
} else {
|
|
NimBLERemoteService* service = client->getService(UUID::SERVICE);
|
|
if (!service) {
|
|
WARNING("NimBLEPlatform::write: service not found for handle " + std::to_string(conn_handle));
|
|
return false;
|
|
}
|
|
rxChar = service->getCharacteristic(UUID::RX_CHAR);
|
|
if (rxChar) {
|
|
_cached_rx_chars[conn_handle] = rxChar;
|
|
}
|
|
}
|
|
if (!rxChar) {
|
|
WARNING("NimBLEPlatform::write: RX char not found for handle " + std::to_string(conn_handle));
|
|
return false;
|
|
}
|
|
|
|
// CONC-H4: Track active write for graceful shutdown
|
|
beginWriteOperation();
|
|
bool result = rxChar->writeValue(data.data(), data.size(), response);
|
|
endWriteOperation();
|
|
if (!result) {
|
|
WARNING("NimBLEPlatform::write: writeValue failed for handle " + std::to_string(conn_handle));
|
|
}
|
|
return result;
|
|
} else {
|
|
// We are peripheral - this shouldn't be used, use notify instead
|
|
WARNING("NimBLEPlatform: write() called in peripheral mode, use notify()");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool NimBLEPlatform::writeCharacteristic(uint16_t conn_handle, uint16_t char_handle,
|
|
const Bytes& data, bool response) {
|
|
auto client_it = _clients.find(conn_handle);
|
|
if (client_it == _clients.end() || !client_it->second) {
|
|
return false;
|
|
}
|
|
|
|
NimBLEClient* client = client_it->second;
|
|
if (!client->isConnected()) return false;
|
|
|
|
NimBLERemoteService* service = client->getService(UUID::SERVICE);
|
|
if (!service) return false;
|
|
|
|
// Find characteristic by handle
|
|
NimBLERemoteCharacteristic* chr = nullptr;
|
|
auto conn_it = _connections.find(conn_handle);
|
|
if (conn_it != _connections.end() && char_handle == conn_it->second.identity_handle) {
|
|
chr = service->getCharacteristic(UUID::IDENTITY_CHAR);
|
|
}
|
|
// Fall through to RX_CHAR if not identity
|
|
if (!chr) {
|
|
chr = service->getCharacteristic(UUID::RX_CHAR);
|
|
}
|
|
if (!chr) return false;
|
|
|
|
return chr->writeValue(data.data(), data.size(), response);
|
|
}
|
|
|
|
bool NimBLEPlatform::read(uint16_t conn_handle, uint16_t char_handle,
|
|
std::function<void(OperationResult, const Bytes&)> callback) {
|
|
auto client_it = _clients.find(conn_handle);
|
|
if (client_it == _clients.end() || !client_it->second) {
|
|
if (callback) callback(OperationResult::NOT_FOUND, Bytes());
|
|
return false;
|
|
}
|
|
|
|
NimBLEClient* client = client_it->second;
|
|
if (!client->isConnected()) {
|
|
if (callback) callback(OperationResult::DISCONNECTED, Bytes());
|
|
return false;
|
|
}
|
|
|
|
NimBLERemoteService* service = client->getService(UUID::SERVICE);
|
|
if (!service) {
|
|
if (callback) callback(OperationResult::NOT_FOUND, Bytes());
|
|
return false;
|
|
}
|
|
|
|
// Find characteristic by handle
|
|
NimBLERemoteCharacteristic* chr = nullptr;
|
|
if (char_handle == _connections[conn_handle].identity_handle) {
|
|
chr = service->getCharacteristic(UUID::IDENTITY_CHAR);
|
|
}
|
|
|
|
if (!chr) {
|
|
if (callback) callback(OperationResult::NOT_FOUND, Bytes());
|
|
return false;
|
|
}
|
|
|
|
NimBLEAttValue value = chr->readValue();
|
|
if (callback) {
|
|
Bytes result(value.data(), value.size());
|
|
callback(OperationResult::SUCCESS, result);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NimBLEPlatform::enableNotifications(uint16_t conn_handle, bool enable) {
|
|
auto client_it = _clients.find(conn_handle);
|
|
if (client_it == _clients.end() || !client_it->second) {
|
|
return false;
|
|
}
|
|
|
|
NimBLEClient* client = client_it->second;
|
|
if (!client->isConnected()) return false;
|
|
|
|
NimBLERemoteService* service = client->getService(UUID::SERVICE);
|
|
if (!service) return false;
|
|
|
|
NimBLERemoteCharacteristic* txChar = service->getCharacteristic(UUID::TX_CHAR);
|
|
if (!txChar) return false;
|
|
|
|
if (enable) {
|
|
// Subscribe to notifications.
|
|
// Capture peer_address to guard against conn_handle reuse: if peer A disconnects
|
|
// (handle=1) and peer B connects (handle=1), we must not deliver B's data as A's.
|
|
BLEAddress expected_peer = getConnection(conn_handle).peer_address;
|
|
auto notifyCb = [this, conn_handle, expected_peer](NimBLERemoteCharacteristic* pChar,
|
|
uint8_t* pData, size_t length, bool isNotify) {
|
|
if (_on_data_received) {
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
if (!conn.isValid() || conn.peer_address != expected_peer) {
|
|
return; // Stale handle — peer changed
|
|
}
|
|
Bytes data(pData, length);
|
|
_on_data_received(conn, data);
|
|
}
|
|
};
|
|
|
|
return txChar->subscribe(true, notifyCb);
|
|
} else {
|
|
return txChar->unsubscribe();
|
|
}
|
|
}
|
|
|
|
bool NimBLEPlatform::notify(uint16_t conn_handle, const Bytes& data) {
|
|
if (!_tx_char) {
|
|
return false;
|
|
}
|
|
|
|
_tx_char->setValue(data.data(), data.size());
|
|
return _tx_char->notify(true);
|
|
}
|
|
|
|
bool NimBLEPlatform::notifyAll(const Bytes& data) {
|
|
if (!_tx_char) {
|
|
return false;
|
|
}
|
|
|
|
_tx_char->setValue(data.data(), data.size());
|
|
return _tx_char->notify(true); // Notifies all subscribed clients
|
|
}
|
|
|
|
//=============================================================================
|
|
// Connection Management
|
|
//=============================================================================
|
|
|
|
std::vector<ConnectionHandle> NimBLEPlatform::getConnections() const {
|
|
std::vector<ConnectionHandle> result;
|
|
for (const auto& kv : _connections) {
|
|
result.push_back(kv.second);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ConnectionHandle NimBLEPlatform::getConnection(uint16_t handle) const {
|
|
auto it = _connections.find(handle);
|
|
if (it != _connections.end()) {
|
|
return it->second;
|
|
}
|
|
return ConnectionHandle();
|
|
}
|
|
|
|
size_t NimBLEPlatform::getConnectionCount() const {
|
|
return _connections.size();
|
|
}
|
|
|
|
bool NimBLEPlatform::isConnectedTo(const BLEAddress& address) const {
|
|
for (const auto& kv : _connections) {
|
|
if (kv.second.peer_address == address) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool NimBLEPlatform::isDeviceConnected(const std::string& addrKey) const {
|
|
for (const auto& kv : _connections) {
|
|
if (kv.second.peer_address.toString() == addrKey) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//=============================================================================
|
|
// Callback Registration
|
|
//=============================================================================
|
|
|
|
void NimBLEPlatform::setOnScanResult(Callbacks::OnScanResult callback) {
|
|
_on_scan_result = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnScanComplete(Callbacks::OnScanComplete callback) {
|
|
_on_scan_complete = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnConnected(Callbacks::OnConnected callback) {
|
|
_on_connected = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnDisconnected(Callbacks::OnDisconnected callback) {
|
|
_on_disconnected = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnMTUChanged(Callbacks::OnMTUChanged callback) {
|
|
_on_mtu_changed = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnServicesDiscovered(Callbacks::OnServicesDiscovered callback) {
|
|
_on_services_discovered = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnDataReceived(Callbacks::OnDataReceived callback) {
|
|
_on_data_received = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnNotifyEnabled(Callbacks::OnNotifyEnabled callback) {
|
|
_on_notify_enabled = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnCentralConnected(Callbacks::OnCentralConnected callback) {
|
|
_on_central_connected = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnCentralDisconnected(Callbacks::OnCentralDisconnected callback) {
|
|
_on_central_disconnected = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnWriteReceived(Callbacks::OnWriteReceived callback) {
|
|
_on_write_received = callback;
|
|
}
|
|
|
|
void NimBLEPlatform::setOnReadRequested(Callbacks::OnReadRequested callback) {
|
|
_on_read_requested = callback;
|
|
}
|
|
|
|
BLEAddress NimBLEPlatform::getLocalAddress() const {
|
|
// Try NimBLE's address first (uses configured own_addr_type)
|
|
BLEAddress addr = fromNimBLE(NimBLEDevice::getAddress());
|
|
if (!addr.isZero()) {
|
|
return addr;
|
|
}
|
|
|
|
// Fallback: try ble_hs_id_copy_addr directly with RANDOM type
|
|
uint8_t nimble_addr[6] = {};
|
|
int rc = ble_hs_id_copy_addr(BLE_OWN_ADDR_RANDOM, nimble_addr, nullptr);
|
|
if (rc == 0) {
|
|
// NimBLE stores in little-endian: val[0]=LSB, val[5]=MSB
|
|
BLEAddress result;
|
|
for (int i = 0; i < 6; i++) {
|
|
result.addr[i] = nimble_addr[5 - i];
|
|
}
|
|
if (!result.isZero()) return result;
|
|
}
|
|
|
|
// Fallback: read BT MAC directly from ESP-IDF efuse
|
|
uint8_t mac[6] = {};
|
|
esp_err_t err = esp_read_mac(mac, ESP_MAC_BT);
|
|
if (err == ESP_OK) {
|
|
// esp_read_mac returns in standard order: mac[0]=MSB (OUI), mac[5]=LSB
|
|
// Our BLEAddress also stores MSB first, so direct copy
|
|
BLEAddress result;
|
|
memcpy(result.addr, mac, 6);
|
|
if (!result.isZero()) return result;
|
|
}
|
|
|
|
WARNING(std::string("NimBLEPlatform::getLocalAddress: all methods failed") +
|
|
" nimble_addr=" + addr.toString() +
|
|
" ble_hs_id_copy_addr_rc=" + std::to_string(rc) +
|
|
" esp_read_mac_rc=" + std::to_string(static_cast<int>(err)));
|
|
return addr;
|
|
}
|
|
|
|
//=============================================================================
|
|
// NimBLE Server Callbacks (Peripheral mode)
|
|
//=============================================================================
|
|
|
|
void NimBLEPlatform::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
|
|
uint16_t conn_handle = connInfo.getConnHandle();
|
|
|
|
ConnectionHandle conn;
|
|
conn.handle = conn_handle;
|
|
conn.peer_address = fromNimBLE(connInfo.getAddress());
|
|
conn.local_role = Role::PERIPHERAL; // We are peripheral, they are central
|
|
conn.state = ConnectionState::CONNECTED;
|
|
conn.mtu = MTU::MINIMUM - MTU::ATT_OVERHEAD;
|
|
|
|
// Read connection RSSI
|
|
int8_t rssi_val = 0;
|
|
if (ble_gap_conn_rssi(conn_handle, &rssi_val) == 0) {
|
|
conn.rssi = rssi_val;
|
|
}
|
|
|
|
_connections[conn_handle] = conn;
|
|
|
|
DEBUG("NimBLEPlatform: Central connected: " + conn.peer_address.toString() +
|
|
" rssi=" + std::to_string(conn.rssi));
|
|
|
|
if (_on_central_connected) {
|
|
_on_central_connected(conn);
|
|
}
|
|
|
|
// Continue advertising to accept more connections
|
|
if (_config.role == Role::DUAL && getConnectionCount() < _config.max_connections) {
|
|
startAdvertising();
|
|
}
|
|
}
|
|
|
|
void NimBLEPlatform::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) {
|
|
if (_shutting_down) return; // shutdown() handles cleanup
|
|
|
|
uint16_t conn_handle = connInfo.getConnHandle();
|
|
|
|
DEBUG("NimBLEPlatform: Central disconnect event for handle=" + std::to_string(conn_handle) +
|
|
" reason=" + std::to_string(reason));
|
|
|
|
// Defer map cleanup to BLE loop task to avoid data race.
|
|
// This callback runs in the NimBLE host task while the BLE loop task
|
|
// may be iterating _connections concurrently.
|
|
queueDisconnect(conn_handle, reason, true);
|
|
}
|
|
|
|
void NimBLEPlatform::onMTUChange(uint16_t MTU, NimBLEConnInfo& connInfo) {
|
|
uint16_t conn_handle = connInfo.getConnHandle();
|
|
updateConnectionMTU(conn_handle, MTU);
|
|
|
|
DEBUG("NimBLEPlatform: MTU changed to " + std::to_string(MTU) +
|
|
" for connection " + std::to_string(conn_handle));
|
|
|
|
if (_on_mtu_changed) {
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
_on_mtu_changed(conn, MTU);
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// NimBLE Characteristic Callbacks
|
|
//=============================================================================
|
|
|
|
void NimBLEPlatform::onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) {
|
|
uint16_t conn_handle = connInfo.getConnHandle();
|
|
|
|
NimBLEAttValue value = pCharacteristic->getValue();
|
|
Bytes data(value.data(), value.size());
|
|
|
|
DEBUG("NimBLEPlatform::onWrite: Received " + std::to_string(data.size()) + " bytes from conn " + std::to_string(conn_handle));
|
|
|
|
if (_on_write_received) {
|
|
DEBUG("NimBLEPlatform::onWrite: Getting connection handle");
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
DEBUG("NimBLEPlatform::onWrite: Calling callback, peer=" + conn.peer_address.toString());
|
|
_on_write_received(conn, data);
|
|
DEBUG("NimBLEPlatform::onWrite: Callback returned");
|
|
} else {
|
|
DEBUG("NimBLEPlatform::onWrite: No callback registered");
|
|
}
|
|
}
|
|
|
|
void NimBLEPlatform::onRead(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) {
|
|
// Identity characteristic read - return stored identity
|
|
if (pCharacteristic == _identity_char && _identity_data.size() > 0) {
|
|
pCharacteristic->setValue(_identity_data.data(), _identity_data.size());
|
|
}
|
|
}
|
|
|
|
void NimBLEPlatform::onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo,
|
|
uint16_t subValue) {
|
|
uint16_t conn_handle = connInfo.getConnHandle();
|
|
bool enabled = (subValue > 0);
|
|
|
|
DEBUG("NimBLEPlatform: Notifications " + std::string(enabled ? "enabled" : "disabled") +
|
|
" for connection " + std::to_string(conn_handle));
|
|
|
|
if (_on_notify_enabled) {
|
|
ConnectionHandle conn = getConnection(conn_handle);
|
|
_on_notify_enabled(conn, enabled);
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// NimBLE Client Callbacks (Central mode)
|
|
//=============================================================================
|
|
|
|
void NimBLEPlatform::onConnect(NimBLEClient* pClient) {
|
|
uint16_t conn_handle = pClient->getConnHandle();
|
|
BLEAddress peer_addr = fromNimBLE(pClient->getPeerAddress());
|
|
|
|
ConnectionHandle conn;
|
|
conn.handle = conn_handle;
|
|
conn.peer_address = peer_addr;
|
|
conn.local_role = Role::CENTRAL; // We are central
|
|
conn.state = ConnectionState::CONNECTED;
|
|
conn.mtu = pClient->getMTU() - MTU::ATT_OVERHEAD;
|
|
|
|
_connections[conn_handle] = conn;
|
|
_clients[conn_handle] = pClient;
|
|
|
|
DEBUG("NimBLEPlatform: Connected to peripheral: " + peer_addr.toString() +
|
|
" handle=" + std::to_string(conn_handle) + " mtu=" + std::to_string(conn.mtu));
|
|
|
|
// Signal async connect completion
|
|
_async_connect_pending = false;
|
|
_async_connect_failed = false;
|
|
|
|
// When _native_connect_pending is true, connectNative() is doing a blocking
|
|
// connect and will fire _on_connected itself from the calling task.
|
|
// Firing it here (in the NimBLE host task) would deadlock because _on_connected
|
|
// triggers blocking GATT operations that require the host task to be free.
|
|
if (!_native_connect_pending && _on_connected) {
|
|
_on_connected(conn);
|
|
}
|
|
}
|
|
|
|
void NimBLEPlatform::onConnectFail(NimBLEClient* pClient, int reason) {
|
|
BLEAddress peer_addr = fromNimBLE(pClient->getPeerAddress());
|
|
ERROR("NimBLEPlatform: onConnectFail to " + peer_addr.toString() +
|
|
" reason=" + std::to_string(reason));
|
|
|
|
// Signal async connect failure
|
|
_async_connect_pending = false;
|
|
_async_connect_failed = true;
|
|
_async_connect_error = reason;
|
|
}
|
|
|
|
void NimBLEPlatform::onDisconnect(NimBLEClient* pClient, int reason) {
|
|
uint16_t conn_handle = pClient->getConnHandle();
|
|
|
|
// During shutdown, cleanup is handled by shutdown() itself.
|
|
// Calling deleteClient here would double-free.
|
|
if (_shutting_down) {
|
|
DEBUG("NimBLEPlatform: onDisconnect during shutdown, skipping cleanup for handle " +
|
|
std::to_string(conn_handle));
|
|
return;
|
|
}
|
|
|
|
DEBUG("NimBLEPlatform: Client disconnect event for handle=" + std::to_string(conn_handle) +
|
|
" reason=" + std::to_string(reason));
|
|
|
|
// Defer map cleanup to BLE loop task to avoid data race.
|
|
// This callback runs in the NimBLE host task while the BLE loop task
|
|
// may be iterating _connections/_clients concurrently.
|
|
// Note: NimBLEDevice::deleteClient() for this client will be called
|
|
// in processPendingDisconnects() from the loop task context.
|
|
queueDisconnect(conn_handle, reason, false);
|
|
}
|
|
|
|
//=============================================================================
|
|
// NimBLE Scan Callbacks
|
|
//=============================================================================
|
|
|
|
void NimBLEPlatform::onResult(const NimBLEAdvertisedDevice* advertisedDevice) {
|
|
// Check if device has our service UUID
|
|
bool hasService = advertisedDevice->isAdvertisingService(BLEUUID(UUID::SERVICE));
|
|
|
|
// Debug: log RNS device scan results with address type
|
|
if (hasService) {
|
|
INFO("BLE SCAN: RNS device found: " + std::string(advertisedDevice->getAddress().toString().c_str()) +
|
|
" type=" + std::to_string(advertisedDevice->getAddress().getType()) +
|
|
" RSSI=" + std::to_string(advertisedDevice->getRSSI()) +
|
|
" name=" + advertisedDevice->getName());
|
|
|
|
// Cache the full device info for later connection
|
|
// Using string key since NimBLEAdvertisedDevice stores all connection metadata
|
|
std::string addrKey = advertisedDevice->getAddress().toString().c_str();
|
|
|
|
// Bounded cache with connected device protection (CONC-M6)
|
|
static constexpr size_t MAX_DISCOVERED_DEVICES = 16;
|
|
while (_discovered_devices.size() >= MAX_DISCOVERED_DEVICES) {
|
|
bool evicted = false;
|
|
// Find oldest non-connected device using insertion order
|
|
for (auto it = _discovered_order.begin(); it != _discovered_order.end(); ++it) {
|
|
if (!isDeviceConnected(*it)) {
|
|
_discovered_devices.erase(*it);
|
|
_discovered_order.erase(it);
|
|
evicted = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!evicted) {
|
|
// All cached devices are connected - don't cache new one
|
|
WARNING("NimBLEPlatform: Cannot cache device - all slots hold connected devices");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Track insertion order for new devices
|
|
auto existing = _discovered_devices.find(addrKey);
|
|
if (existing == _discovered_devices.end()) {
|
|
// New device - add to order tracking
|
|
_discovered_order.push_back(addrKey);
|
|
}
|
|
_discovered_devices[addrKey] = *advertisedDevice;
|
|
TRACE("NimBLEPlatform: Cached device for connection: " + addrKey +
|
|
" (cache size: " + std::to_string(_discovered_devices.size()) + ")");
|
|
}
|
|
|
|
if (hasService && _on_scan_result) {
|
|
ScanResult result;
|
|
result.address = fromNimBLE(advertisedDevice->getAddress());
|
|
result.name = advertisedDevice->getName();
|
|
result.rssi = advertisedDevice->getRSSI();
|
|
result.connectable = advertisedDevice->isConnectable();
|
|
result.has_reticulum_service = true;
|
|
|
|
// Extract identity prefix from device name (Protocol v2.2)
|
|
// Format: "RNS-xxxxxx" where xxxxxx is 6 hex chars (3 bytes of identity)
|
|
std::string name = advertisedDevice->getName();
|
|
if (name.size() >= 10 && name.substr(0, 4) == "RNS-") {
|
|
std::string hexPart = name.substr(4, 6);
|
|
if (hexPart.size() == 6) {
|
|
// Parse hex to bytes
|
|
uint8_t prefix[3];
|
|
bool valid = true;
|
|
for (int i = 0; i < 3 && valid; i++) {
|
|
unsigned int val;
|
|
if (sscanf(hexPart.c_str() + i*2, "%02x", &val) == 1) {
|
|
prefix[i] = static_cast<uint8_t>(val);
|
|
} else {
|
|
valid = false;
|
|
}
|
|
}
|
|
if (valid) {
|
|
result.identity_prefix = Bytes(prefix, 3);
|
|
DEBUG("NimBLEPlatform: Extracted identity prefix from name: " + hexPart);
|
|
}
|
|
}
|
|
}
|
|
|
|
_on_scan_result(result);
|
|
}
|
|
}
|
|
|
|
void NimBLEPlatform::onScanEnd(const NimBLEScanResults& results, int reason) {
|
|
// Check if we were actively scanning
|
|
portENTER_CRITICAL(&_state_mux);
|
|
MasterState prev_master = _master_state;
|
|
bool was_scanning = (prev_master == MasterState::SCANNING ||
|
|
prev_master == MasterState::SCAN_STARTING ||
|
|
prev_master == MasterState::SCAN_STOPPING);
|
|
// Transition to IDLE
|
|
if (was_scanning) {
|
|
_master_state = MasterState::IDLE;
|
|
_gap_state = GAPState::READY;
|
|
}
|
|
portEXIT_CRITICAL(&_state_mux);
|
|
|
|
_scan_stop_time = 0;
|
|
|
|
INFO("BLE SCAN: Ended, reason=" + std::to_string(reason) +
|
|
" found=" + std::to_string(results.getCount()) + " devices");
|
|
|
|
// Only process if we were actively scanning (not a spurious callback)
|
|
if (!was_scanning) {
|
|
return;
|
|
}
|
|
|
|
// Resume slave if it was paused for this scan
|
|
resumeSlave();
|
|
|
|
if (_on_scan_complete) {
|
|
_on_scan_complete();
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// BLEOperationQueue Implementation
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::executeOperation(const GATTOperation& op) {
|
|
// Most operations are executed directly in NimBLE
|
|
// This is a placeholder for more complex queued operations
|
|
return true;
|
|
}
|
|
|
|
//=============================================================================
|
|
// Private Methods
|
|
//=============================================================================
|
|
|
|
bool NimBLEPlatform::setupServer() {
|
|
_server = NimBLEDevice::createServer();
|
|
if (!_server) {
|
|
ERROR("NimBLEPlatform: Failed to create server");
|
|
return false;
|
|
}
|
|
|
|
_server->setCallbacks(this);
|
|
|
|
// Create Reticulum service
|
|
_service = _server->createService(UUID::SERVICE);
|
|
if (!_service) {
|
|
ERROR("NimBLEPlatform: Failed to create service");
|
|
return false;
|
|
}
|
|
|
|
// Create RX characteristic (write from central)
|
|
_rx_char = _service->createCharacteristic(
|
|
UUID::RX_CHAR,
|
|
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
|
|
);
|
|
_rx_char->setValue((uint8_t*)"\x00", 1); // Initialize to 0x00
|
|
_rx_char->setCallbacks(this);
|
|
|
|
// Create TX characteristic (notify/indicate to central)
|
|
// Note: indicate property required for compatibility with ble-reticulum/Columba
|
|
_tx_char = _service->createCharacteristic(
|
|
UUID::TX_CHAR,
|
|
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::INDICATE
|
|
);
|
|
_tx_char->setValue((uint8_t*)"\x00", 1); // Initialize to 0x00 (matches Columba)
|
|
_tx_char->setCallbacks(this);
|
|
|
|
// Create Identity characteristic (read only)
|
|
_identity_char = _service->createCharacteristic(
|
|
UUID::IDENTITY_CHAR,
|
|
NIMBLE_PROPERTY::READ
|
|
);
|
|
_identity_char->setCallbacks(this);
|
|
|
|
// Start service
|
|
_service->start();
|
|
|
|
return setupAdvertising();
|
|
}
|
|
|
|
bool NimBLEPlatform::setupAdvertising() {
|
|
_advertising_obj = NimBLEDevice::getAdvertising();
|
|
if (!_advertising_obj) {
|
|
ERROR("NimBLEPlatform: Failed to get advertising");
|
|
return false;
|
|
}
|
|
|
|
// CRITICAL: Reset advertising state before configuring
|
|
// Without this, the advertising data may not be properly updated on ESP32-S3
|
|
_advertising_obj->reset();
|
|
|
|
_advertising_obj->setMinInterval(_config.adv_interval_min_ms * 1000 / 625); // Convert to 0.625ms units
|
|
_advertising_obj->setMaxInterval(_config.adv_interval_max_ms * 1000 / 625);
|
|
|
|
// NimBLE 2.x: Use addServiceUUID to include service in advertising packet
|
|
// The name goes in scan response automatically when enableScanResponse is used
|
|
_advertising_obj->addServiceUUID(NimBLEUUID(UUID::SERVICE));
|
|
_advertising_obj->setName(_config.device_name);
|
|
|
|
DEBUG("NimBLEPlatform: Advertising configured with service UUID: " + std::string(UUID::SERVICE));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NimBLEPlatform::setupScan() {
|
|
_scan = NimBLEDevice::getScan();
|
|
if (!_scan) {
|
|
ERROR("NimBLEPlatform: Failed to get scan");
|
|
return false;
|
|
}
|
|
|
|
_scan->setScanCallbacks(this, false);
|
|
_scan->setActiveScan(_config.scan_mode == ScanMode::ACTIVE);
|
|
_scan->setInterval(_config.scan_interval_ms);
|
|
_scan->setWindow(_config.scan_window_ms);
|
|
_scan->setFilterPolicy(BLE_HCI_SCAN_FILT_NO_WL);
|
|
_scan->setDuplicateFilter(true); // Filter duplicates within a scan window
|
|
// Don't call setMaxResults - let NimBLE use defaults
|
|
|
|
DEBUG("NimBLEPlatform: Scan configured - interval=" + std::to_string(_config.scan_interval_ms) +
|
|
" window=" + std::to_string(_config.scan_window_ms));
|
|
|
|
return true;
|
|
}
|
|
|
|
BLEAddress NimBLEPlatform::fromNimBLE(const NimBLEAddress& addr) {
|
|
BLEAddress result;
|
|
const ble_addr_t* base = addr.getBase();
|
|
if (base) {
|
|
// NimBLE stores addresses in little-endian: val[0]=LSB, val[5]=MSB
|
|
// Our BLEAddress stores in big-endian display order: addr[0]=MSB, addr[5]=LSB
|
|
// Need to reverse the byte order
|
|
for (int i = 0; i < 6; i++) {
|
|
result.addr[i] = base->val[5 - i];
|
|
}
|
|
}
|
|
result.type = addr.getType();
|
|
return result;
|
|
}
|
|
|
|
NimBLEAddress NimBLEPlatform::toNimBLE(const BLEAddress& addr) {
|
|
// Use NimBLEAddress string constructor - it parses "XX:XX:XX:XX:XX:XX" format
|
|
// and handles the byte order internally
|
|
std::string addrStr = addr.toString();
|
|
NimBLEAddress nimAddr(addrStr.c_str(), addr.type);
|
|
DEBUG("NimBLEPlatform::toNimBLE: input=" + addrStr +
|
|
" type=" + std::to_string(addr.type) +
|
|
" -> nimAddr=" + std::string(nimAddr.toString().c_str()) +
|
|
" nimType=" + std::to_string(nimAddr.getType()));
|
|
return nimAddr;
|
|
}
|
|
|
|
NimBLEClient* NimBLEPlatform::findClient(uint16_t conn_handle) {
|
|
auto it = _clients.find(conn_handle);
|
|
return (it != _clients.end()) ? it->second : nullptr;
|
|
}
|
|
|
|
NimBLEClient* NimBLEPlatform::findClient(const BLEAddress& address) {
|
|
for (const auto& kv : _clients) {
|
|
if (kv.second && fromNimBLE(kv.second->getPeerAddress()) == address) {
|
|
return kv.second;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
uint16_t NimBLEPlatform::allocateConnHandle() {
|
|
return _next_conn_handle++;
|
|
}
|
|
|
|
void NimBLEPlatform::freeConnHandle(uint16_t handle) {
|
|
// No-op for simple allocator
|
|
}
|
|
|
|
void NimBLEPlatform::updateConnectionMTU(uint16_t conn_handle, uint16_t mtu) {
|
|
auto it = _connections.find(conn_handle);
|
|
if (it != _connections.end()) {
|
|
it->second.mtu = mtu - MTU::ATT_OVERHEAD;
|
|
}
|
|
}
|
|
|
|
}} // namespace RNS::BLE
|
|
|
|
#endif // ESP32 && USE_NIMBLE
|