Skip to content

Bonding Fails After Removing Bond in nRF Connect Until ESP32-S3 Reboot #338

Open
@x-radio

Description

@x-radio

Hello,

I'm encountering an issue with the H2Zero NimBLE library on an ESP32-S3. After removing bond information in the nRF Connect app, I cannot establish a new bond until I reboot the ESP32-S3. Interestingly, nRF Connect on a second phone works fine in this scenario.

To debug, I monitored the number of stored bonds using NimBLEDevice::getNumBonds(). Here's what I observed:

After a successful bond, the number of bonds increases by 1, and this persists after rebooting the ESP32-S3, as expected.
If bond information exists in the NimBLE stack and I remove it on the phone (via nRF Connect), the following happens:
During an attempt to create a new bond (e.g., when reading an encrypted characteristic), the number of stored bonds in the stack decreases to 0 (which seems correct, as the old bond is invalidated).
However, the new bonding attempt fails with an error (GATT AUTH FAIL (137, 0x89)).
A new bond can only be established after rebooting the ESP32-S3.
I suspect the NimBLE stack retains some internal state that prevents successful re-bonding until a full reset. Rebooting the ESP32-S3 seems to clear this state, allowing a new bond to form. How can I reproduce this reset behavior without rebooting the controller? For example, is there a way to fully reset the BLE stack or clear connection state to initiate a fresh bonding process?

nRF Connect Log:

nRF Connect, 2025-05-28
A7-3030F9745C2C (30:30:F9:74:5C:2E)
D 15:05:36.485 gatt.close()
D 15:05:36.487 wait(200)
V 15:05:36.689 Connecting to 30:30:F9:74:5C:2E...
D 15:05:36.689 gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE, preferred PHY = LE 1M)
D 15:05:36.927 [Broadcast] Action received: android.bluetooth.device.action.ACL_CONNECTED
D 15:05:36.927 [Callback] Connection state changed with status: 0 and new state: CONNECTED (2)
I 15:05:36.927 Connected to 30:30:F9:74:5C:2E
V 15:05:36.928 Requesting new MTU...
D 15:05:36.928 gatt.requestMtu(517)
I 15:05:37.416 Connection parameters updated (interval: 7.5ms, latency: 0, timeout: 5000ms)
I 15:05:37.627 MTU changed to: 512
V 15:05:37.631 Discovering services...
D 15:05:37.631 gatt.discoverServices()
D 15:05:37.637 [Callback] Services discovered with status: 0
I 15:05:37.637 Services discovered
V 15:05:37.648 Generic Access (0x1800)
- Device Name [R] (0x2A00)
- Appearance [R] (0x2A01)
Generic Attribute (0x1801)
- Service Changed [I] (0x2A05)
   Client Characteristic Configuration (0x2902)
- Server Supported Features [R] (0x2B3A)
- Client Supported Features [R W] (0x2B29)
Unknown Service (deadbeef-feed-baad-babe-facec0de0000)
- Unknown Characteristic [R] (deadbeef-feed-baad-babe-facec0de0001)
- Unknown Characteristic [R] (deadbeef-feed-baad-babe-facec0de0002)
- Unknown Characteristic [R] (deadbeef-feed-baad-babe-facec0de0003)
- Unknown Characteristic [W] (deadbeef-feed-baad-babe-facec0de0004)
- Unknown Characteristic [W] (deadbeef-feed-baad-babe-facec0de0005)
- Unknown Characteristic [W] (deadbeef-feed-baad-babe-facec0de0006)
D 15:05:37.648 gatt.setCharacteristicNotification(00002a05-0000-1000-8000-00805f9b34fb, true)
I 15:05:37.716 Connection parameters updated (interval: 45.0ms, latency: 0, timeout: 5000ms)
V 15:05:40.933 Reading characteristic deadbeef-feed-baad-babe-facec0de0001
D 15:05:40.933 gatt.readCharacteristic(deadbeef-feed-baad-babe-facec0de0001)
D 15:05:41.414 [Broadcast] Action received: android.bluetooth.device.action.BOND_STATE_CHANGED, bond state changed to: BONDING (11)
D 15:05:41.442 [Broadcast] Action received: android.bluetooth.device.extra.PAIRING_VARIANT, pairing variant: CONSENT
E 15:05:44.015 Error 137 (0x89): GATT AUTH FAIL
D 15:05:44.019 [Callback] Connection state changed with status: 19 and new state: DISCONNECTED (0)
W 15:05:44.019 Connection terminated by peer (status 19)
I 15:05:44.019 Disconnected
D 15:05:44.038 [Broadcast] Action received: android.bluetooth.device.action.BOND_STATE_CHANGED, bond state changed to: NONE (10), reason: REMOVED (9)
I 15:05:44.038 Bonding failed, reason: REMOVED (9)
D 15:05:44.071 [Broadcast] Action received: android.bluetooth.device.action.ACL_DISCONNECTED

Relevant Code:

Below are the key parts of my ble.cpp code, showing the BLE setup, server callbacks, and bond monitoring logic.

class ServerCallbacks : public NimBLEServerCallbacks {
    void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override
    {
        logi("Client connected: %s", connInfo.getAddress().toString().c_str());
        pServer->updateConnParams(connInfo.getConnHandle(), 200, 400, 10, 200);
    }
    //-----------------------
    void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override
    {
        logi("Client disconnected, reason: %d", reason);
    }
    //-----------------------
    void onMTUChange(uint16_t MTU, NimBLEConnInfo& connInfo) override
    {
        logi("MTU updated: %u for connection ID: %u", MTU, connInfo.getConnHandle());
    }
    //-----------------------
    void onAuthenticationComplete(NimBLEConnInfo& connInfo) override {
        if (connInfo.isEncrypted()) {
            logi("Secured connection to: %s\n", connInfo.getAddress().toString().c_str());
        } else {
            logi("Encryption failed for %s - deleting bond and disconnecting\n", connInfo.getAddress().toString().c_str());

            pServer->disconnect(connInfo.getConnHandle());
        }
    }
} serverCallbacks;
//---------------------------
class CharacteristicCallbacks : public NimBLECharacteristicCallbacks {
    void onRead(NimBLECharacteristic* pCharact, NimBLEConnInfo& connInfo) override
    {
        if (pCharact == pLastEventsCh) {
            pCharact->setValue("lastEventsCh");
        }
        //-------------------
        else if (pCharact == pCurStateCh){
            pCharact->setValue("curStateCh");
        }
        //-------------------
        else if (pCharact == pWifiListCh) {
            pCharact->setValue("wifiListCh");
        }
    }
//---------------------------
    void onWrite(NimBLECharacteristic* pCharact, NimBLEConnInfo& connInfo) override
    {
        string value = pCharact->getValue();
        logi("onWrite %s, value: %s", pCharact->getUUID().toString().c_str(), value.c_str());
    }
} chrCallbacks;
//---------------------------
void initServer()
{
    NimBLEDevice::init(DEV_GetIdString());
    NimBLEDevice::setMTU(512);
    NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_SC | BLE_SM_PAIR_AUTHREQ_BOND);
    pServer = NimBLEDevice::createServer();
    pServer->setCallbacks(&serverCallbacks);

    pService = pServer->createService(SERVICE_UUID);

    pCurStateCh = pService->createCharacteristic(
        CHAR_CUR_STATE_UUID,
        NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC
    );
    pCurStateCh->setCallbacks(&chrCallbacks);

    pLastEventsCh = pService->createCharacteristic(
        CHAR_LAST_EVENTS_UUID,
        NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC
    );
    pLastEventsCh->setCallbacks(&chrCallbacks);

    pWifiListCh = pService->createCharacteristic(
        CHAR_WIFI_LIST_UUID,
        NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC
    );
    pWifiListCh->setCallbacks(&chrCallbacks);

    pSetPointsCh = pService->createCharacteristic(
        CHAR_SET_POINTS_UUID,
        NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC
    );
    pSetPointsCh->setCallbacks(&chrCallbacks);

    pSetWifiCredCh = pService->createCharacteristic(
        CHAR_SET_WIFI_CRED_UUID,
        NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC
    );
    pSetWifiCredCh->setCallbacks(&chrCallbacks);

    pAuthTokenCh = pService->createCharacteristic(
        CHAR_AUTH_TOKEN_UUID,
        NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC
    );
    pAuthTokenCh->setCallbacks(&chrCallbacks);

    pService->start();
    logi("Server Started");
}
//---------------------------
void initAdvertising()
{
    pAdvertising = NimBLEDevice::getAdvertising();
    pAdvertising->setName(DEV_GetIdString());
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setAdvertisingInterval(1000);
    pAdvertising->enableScanResponse(true);
    pAdvertising->start();
    logi("Advertising Started");
}
//---------------------------
...
    //a byte of code - here i check one time per second:
    case 1:
        if(pAdvertising->isAdvertising()) {logd("is advertizing"); logd("num bonds - %d", NimBLEDevice::getNumBonds()); return;}
        if(activeClients >= MAX_CLIENTS) {logd("clients - %u", activeClients); return;}
        pAdvertising->start();
        logi("restart adv");
        return;

Environment:

Hardware: ESP32-S3
ESP-IDF Version: v5.4.1
NimBLE Library: H2Zero NimBLE (latest version as of May 2025)
nRF Connect Version: Latest (as of May 2025)
Phone: Android (specific model/version TBD)
Bonding Mode: Just Works (BLE_SM_PAIR_AUTHREQ_SC | BLE_SM_PAIR_AUTHREQ_BOND)
Steps to Reproduce:

Bond the ESP32-S3 with nRF Connect (successful, getNumBonds() == 1).
Remove the bond in nRF Connect (Settings > Bluetooth > Forget Device).
Reconnect to the ESP32-S3 with nRF Connect.
Attempt to read an encrypted characteristic (e.g., deadbeef-feed-baad-babe-facec0de0001).
Observe GATT AUTH FAIL (137) and disconnection. Note that getNumBonds() drops to 0 during the bonding attempt.
Reboot the ESP32-S3 and reconnect; bonding succeeds.
Expected Behavior:
After removing the bond in nRF Connect, a new bonding attempt should succeed without requiring an ESP32-S3 reboot.

Actual Behavior:
New bonding fails with GATT AUTH FAIL (137) until the ESP32-S3 is rebooted.

Additional Notes:

I tried calling NimBLEDevice::deleteBond(connInfo.getAddress()) in onAuthenticationComplete when bonding fails, but it returns rc=5 (No record found), as the bond is already removed by the stack.
A second phone running nRF Connect can bond successfully, suggesting the issue is specific to the state of the first phone's connection.
Is there a way to reset the NimBLE stack or clear connection state programmatically to mimic the effect of a reboot?
Please let me know if you need the full ble.cpp or additional logs. Thanks for your help!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions