@@ -26,6 +26,7 @@ TimerHandle_t gConnTuneTimer = nullptr;
2626TimerHandle_t gPairingTimer = nullptr ;
2727TimerHandle_t gAdvertisingWatchdogTimer = nullptr ;
2828bool pairingModeActive = false ;
29+ bool pairingModeTransitionActive = false ;
2930
3031// Store the active connection handle for conn param updates
3132uint16_t activeConnHandle = 0 ;
@@ -36,18 +37,24 @@ void stopPairingModeTimer() {
3637 }
3738}
3839
39- void clearWhiteList () {
40- while (NimBLEDevice::getWhiteListCount () > 0 ) {
41- NimBLEDevice::whiteListRemove (NimBLEDevice::getWhiteListAddress (0 ));
42- }
43- }
44-
4540size_t syncWhiteListFromBonds () {
46- clearWhiteList ();
41+ // Reconcile the whitelist to the current bond store. Advertising must be
42+ // stopped before calling this — the BLE controller rejects whitelist changes
43+ // while advertising with a filter (rc=524 / BLE_HS_EBUSY). Prune stale
44+ // entries first, then add any missing bonded addresses.
45+ for (size_t i = NimBLEDevice::getWhiteListCount (); i > 0 ; --i) {
46+ const NimBLEAddress addr = NimBLEDevice::getWhiteListAddress (i - 1 );
47+ if (!NimBLEDevice::isBonded (addr)) {
48+ NimBLEDevice::whiteListRemove (addr);
49+ }
50+ }
4751
4852 const int bondCount = NimBLEDevice::getNumBonds ();
4953 for (int i = 0 ; i < bondCount; ++i) {
50- NimBLEDevice::whiteListAdd (NimBLEDevice::getBondedAddress (i));
54+ const NimBLEAddress addr = NimBLEDevice::getBondedAddress (i);
55+ if (!NimBLEDevice::onWhiteList (addr)) {
56+ NimBLEDevice::whiteListAdd (addr);
57+ }
5158 }
5259
5360 return NimBLEDevice::getWhiteListCount ();
@@ -77,21 +84,26 @@ void applyPreferredLinkParams(TimerHandle_t timer) {
7784}
7885
7986bool shouldAdvertiseWhilePowered () {
80- return pairingModeActive || NimBLEDevice::getNumBonds () > 0 ;
87+ return !pairingModeTransitionActive &&
88+ (pairingModeActive || NimBLEDevice::getNumBonds () > 0 );
8189}
8290
8391bool startAdvertising (NimBLEServer *server) {
84- if (server == nullptr ) {
92+ if (server == nullptr || pairingModeTransitionActive ) {
8593 return false ;
8694 }
8795
8896 const size_t bondCount = static_cast <size_t >(NimBLEDevice::getNumBonds ());
8997 const bool allowOpenAdvertising = pairingModeActive;
98+
99+ // Stop advertising BEFORE modifying the whitelist — the BLE controller
100+ // rejects whitelist changes while advertising with a filter (BLE_HS_EBUSY).
101+ auto *advertising = server->getAdvertising ();
102+ advertising->stop ();
103+
90104 const size_t whiteListCount = syncWhiteListFromBonds ();
91105
92106 if (!allowOpenAdvertising && bondCount == 0 ) {
93- auto *advertising = server->getAdvertising ();
94- advertising->stop ();
95107 USBSerial.println (
96108 " [BLE] No bonds present and pairing mode inactive; advertising stopped" );
97109 return false ;
@@ -132,8 +144,6 @@ bool startAdvertising(NimBLEServer *server) {
132144 static_cast <uint8_t >(allowOpenAdvertising ? 0x01 : 0x00 )};
133145 scanRsp.setManufacturerData (mfrData, sizeof (mfrData));
134146
135- auto *advertising = server->getAdvertising ();
136- advertising->stop ();
137147 advertising->removeAll ();
138148 const bool configured = advertising->setInstanceData (kExtAdvInstance , adv);
139149 const bool scanRspConfigured =
@@ -148,11 +158,6 @@ bool startAdvertising(NimBLEServer *server) {
148158 static_cast <unsigned >(bondCount), static_cast <unsigned >(whiteListCount));
149159 return started;
150160#else
151- auto *advertising = server->getAdvertising ();
152-
153- // Stop before reconfiguring (safe even if not running)
154- advertising->stop ();
155-
156161 // Configure payload once — NimBLE accumulates addServiceUUID calls
157162 static bool payloadConfigured = false ;
158163 if (!payloadConfigured) {
@@ -252,9 +257,11 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks {
252257
253258 USBSerial.printf (" Device disconnected reason=%d\n " , reason);
254259
255- // Restart immediately, then let the watchdog keep retrying forever if this
256- // first post-disconnect start does not stick.
257- startAdvertising (server);
260+ // Suppress the immediate advertising restart during a pairing transition —
261+ // enterBLEPairingMode() issues its own startAdvertising after clearing bonds.
262+ if (!pairingModeTransitionActive) {
263+ startAdvertising (server);
264+ }
258265 }
259266
260267 void onAuthenticationComplete (NimBLEConnInfo &connInfo) override {
@@ -380,7 +387,43 @@ void restartBLEAdvertising() {
380387}
381388
382389void enterBLEPairingMode () {
390+ // Block advertising restarts (e.g. from onDisconnect) during this transition.
391+ pairingModeTransitionActive = true ;
392+
393+ // Single-bond model: disconnect the current peer so we can safely clear bonds.
394+ if (deviceConnected && pServer != nullptr &&
395+ connectedHandle != BLE_HS_CONN_HANDLE_NONE) {
396+ pServer->disconnect (connectedHandle);
397+ vTaskDelay (pdMS_TO_TICKS (100 ));
398+ }
399+
400+ // Stop advertising before modifying bonds — NimBLE rejects bond deletion
401+ // while the controller is advertising (BLE_HS_EBUSY).
402+ if (pServer != nullptr ) {
403+ auto *adv = pServer->getAdvertising ();
404+ if (adv != nullptr ) {
405+ adv->stop ();
406+ }
407+ }
408+ vTaskDelay (pdMS_TO_TICKS (50 ));
409+
410+ const int bondCount = NimBLEDevice::getNumBonds ();
411+ bool cleared = false ;
412+ if (bondCount > 0 ) {
413+ cleared = NimBLEDevice::deleteAllBonds ();
414+ if (!cleared) {
415+ for (int i = NimBLEDevice::getNumBonds () - 1 ; i >= 0 ; --i) {
416+ NimBLEDevice::deleteBond (NimBLEDevice::getBondedAddress (i));
417+ }
418+ cleared = NimBLEDevice::getNumBonds () == 0 ;
419+ }
420+ }
421+ USBSerial.printf (" [BLE] Cleared bonds: %s (was %d, now %d)\n " ,
422+ cleared ? " OK" : (bondCount == 0 ? " NONE" : " FAILED" ),
423+ bondCount, NimBLEDevice::getNumBonds ());
424+
383425 pairingModeActive = true ;
426+ pairingModeTransitionActive = false ;
384427
385428 if (gPairingTimer == nullptr ) {
386429 gPairingTimer = xTimerCreate (" blePair" , kPairingTimeoutTicks , pdFALSE,
0 commit comments