Description
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!