From 8ec38d74500c292051475e606bc692d4c989daf7 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:26:14 -0300 Subject: [PATCH 1/4] fix(ble): Fix authentication deadlock --- libraries/BLE/src/BLEDevice.cpp | 4 ++ libraries/BLE/src/BLERemoteCharacteristic.cpp | 8 +++ libraries/BLE/src/BLESecurity.cpp | 57 +++++++++++++++++++ libraries/BLE/src/BLESecurity.h | 5 ++ 4 files changed, 74 insertions(+) diff --git a/libraries/BLE/src/BLEDevice.cpp b/libraries/BLE/src/BLEDevice.cpp index d56786e8e18..6e125b07ef8 100644 --- a/libraries/BLE/src/BLEDevice.cpp +++ b/libraries/BLE/src/BLEDevice.cpp @@ -960,6 +960,10 @@ void BLEDevice::gapEventHandler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_par { log_i("ESP_GAP_BLE_AUTH_CMPL_EVT"); #ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig + // Signal that authentication has completed + // This unblocks any GATT operations waiting for pairing when bonding is enabled + BLESecurity::signalAuthenticationComplete(); + if (BLEDevice::m_securityCallbacks != nullptr) { BLEDevice::m_securityCallbacks->onAuthenticationComplete(param->ble_security.auth_cmpl); } diff --git a/libraries/BLE/src/BLERemoteCharacteristic.cpp b/libraries/BLE/src/BLERemoteCharacteristic.cpp index d26e2970a10..e964e679897 100644 --- a/libraries/BLE/src/BLERemoteCharacteristic.cpp +++ b/libraries/BLE/src/BLERemoteCharacteristic.cpp @@ -565,6 +565,10 @@ String BLERemoteCharacteristic::readValue() { return String(); } + // Wait for authentication to complete if bonding is enabled + // This prevents the read request from being made while pairing is in progress + BLESecurity::waitForAuthenticationComplete(); + m_semaphoreReadCharEvt.take("readValue"); // Ask the BLE subsystem to retrieve the value for the remote hosted characteristic. @@ -607,6 +611,10 @@ bool BLERemoteCharacteristic::writeValue(uint8_t *data, size_t length, bool resp return false; } + // Wait for authentication to complete if bonding is enabled + // This prevents the write request from being made while pairing is in progress + BLESecurity::waitForAuthenticationComplete(); + m_semaphoreWriteCharEvt.take("writeValue"); // Invoke the ESP-IDF API to perform the write. esp_err_t errRc = ::esp_ble_gattc_write_char( diff --git a/libraries/BLE/src/BLESecurity.cpp b/libraries/BLE/src/BLESecurity.cpp index 7e9269e6000..76757f52135 100644 --- a/libraries/BLE/src/BLESecurity.cpp +++ b/libraries/BLE/src/BLESecurity.cpp @@ -46,6 +46,7 @@ bool BLESecurity::m_securityStarted = false; bool BLESecurity::m_passkeySet = false; bool BLESecurity::m_staticPasskey = true; bool BLESecurity::m_regenOnConnect = false; +bool BLESecurity::m_authenticationComplete = false; uint8_t BLESecurity::m_iocap = ESP_IO_CAP_NONE; uint8_t BLESecurity::m_authReq = 0; uint8_t BLESecurity::m_initKey = 0; @@ -59,6 +60,7 @@ uint32_t BLESecurity::m_passkey = BLE_SM_DEFAULT_PASSKEY; #if defined(CONFIG_BLUEDROID_ENABLED) uint8_t BLESecurity::m_keySize = 16; esp_ble_sec_act_t BLESecurity::m_securityLevel; +FreeRTOS::Semaphore *BLESecurity::m_authCompleteSemaphore = nullptr; #endif /*************************************************************************** @@ -191,6 +193,7 @@ void BLESecurity::regenPassKeyOnConnect(bool enable) { void BLESecurity::resetSecurity() { log_d("resetSecurity: Resetting security state"); m_securityStarted = false; + m_authenticationComplete = false; } // This function sets the authentication mode with bonding, MITM, and secure connection options. @@ -263,6 +266,48 @@ bool BLESecurityCallbacks::onAuthorizationRequest(uint16_t connHandle, uint16_t return true; } +// This function waits for authentication to complete when bonding is enabled +// It prevents GATT operations from proceeding before pairing completes +void BLESecurity::waitForAuthenticationComplete(uint32_t timeoutMs) { +#if defined(CONFIG_BLUEDROID_ENABLED) + // Only wait if bonding is enabled + if ((m_authReq & ESP_LE_AUTH_BOND) == 0) { + return; + } + + // If already authenticated, no need to wait + if (m_authenticationComplete) { + return; + } + + // Semaphore should have been created in startSecurity() + if (m_authCompleteSemaphore == nullptr) { + log_e("Authentication semaphore not initialized"); + return; + } + + // Wait for authentication with timeout + bool success = m_authCompleteSemaphore->timedWait("waitForAuthenticationComplete", timeoutMs / portTICK_PERIOD_MS); + + if (!success) { + log_w("Timeout waiting for authentication to complete"); + } +#endif +} + +// This function signals that authentication has completed +// Called from ESP_GAP_BLE_AUTH_CMPL_EVT handler +void BLESecurity::signalAuthenticationComplete() { +#if defined(CONFIG_BLUEDROID_ENABLED) + m_authenticationComplete = true; + + // Signal waiting threads if semaphore exists + if (m_authCompleteSemaphore != nullptr) { + m_authCompleteSemaphore->give(); + } +#endif +} + /*************************************************************************** * Bluedroid functions * ***************************************************************************/ @@ -285,6 +330,18 @@ bool BLESecurity::startSecurity(esp_bd_addr_t bd_addr, int *rcPtr) { } if (m_securityEnabled) { + // Initialize semaphore before starting security to avoid race condition + if (m_authCompleteSemaphore == nullptr) { + m_authCompleteSemaphore = new FreeRTOS::Semaphore("AuthComplete"); + } + + // Reset authentication complete flag when starting new security negotiation + m_authenticationComplete = false; + + // Consume any pending semaphore signals from previous operations + // This ensures the next wait will block until the new auth completes + m_authCompleteSemaphore->take("startSecurity-reset"); + int rc = esp_ble_set_encryption(bd_addr, m_securityLevel); if (rc != ESP_OK) { log_e("esp_ble_set_encryption: rc=%d %s", rc, GeneralUtils::errorToString(rc)); diff --git a/libraries/BLE/src/BLESecurity.h b/libraries/BLE/src/BLESecurity.h index 2d2306231ac..fc6ff38f6ca 100644 --- a/libraries/BLE/src/BLESecurity.h +++ b/libraries/BLE/src/BLESecurity.h @@ -25,6 +25,7 @@ #include "BLEDevice.h" #include "BLEClient.h" #include "BLEServer.h" +#include "RTOS.h" /*************************************************************************** * Bluedroid includes * @@ -104,6 +105,8 @@ class BLESecurity { static uint32_t generateRandomPassKey(); static void regenPassKeyOnConnect(bool enable = false); static void resetSecurity(); + static void waitForAuthenticationComplete(uint32_t timeoutMs = 10000); + static void signalAuthenticationComplete(); /*************************************************************************** * Bluedroid public declarations * @@ -140,6 +143,7 @@ class BLESecurity { static bool m_passkeySet; static bool m_staticPasskey; static bool m_regenOnConnect; + static bool m_authenticationComplete; static uint8_t m_iocap; static uint8_t m_authReq; static uint8_t m_initKey; @@ -153,6 +157,7 @@ class BLESecurity { #if defined(CONFIG_BLUEDROID_ENABLED) static uint8_t m_keySize; static esp_ble_sec_act_t m_securityLevel; + static class FreeRTOS::Semaphore *m_authCompleteSemaphore; #endif }; // BLESecurity From a3786a2732ed8f318340735844069fa2e5800af4 Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:26:50 -0300 Subject: [PATCH 2/4] feat(irk): Add peer's IRK retrieval methods --- .../Client_secure_static_passkey.ino | 93 ++++++++- .../Server_secure_static_passkey.ino | 74 +++++++- libraries/BLE/src/BLEDevice.cpp | 176 ++++++++++++++++++ libraries/BLE/src/BLEDevice.h | 4 + 4 files changed, 338 insertions(+), 9 deletions(-) diff --git a/libraries/BLE/examples/Client_secure_static_passkey/Client_secure_static_passkey.ino b/libraries/BLE/examples/Client_secure_static_passkey/Client_secure_static_passkey.ino index ec3c457221c..a3204fcb044 100644 --- a/libraries/BLE/examples/Client_secure_static_passkey/Client_secure_static_passkey.ino +++ b/libraries/BLE/examples/Client_secure_static_passkey/Client_secure_static_passkey.ino @@ -1,10 +1,16 @@ /* - Secure client with static passkey + Secure client with static passkey and IRK retrieval This example demonstrates how to create a secure BLE client that connects to a secure BLE server using a static passkey without prompting the user. The client will automatically use the same passkey (123456) as the server. + After successful bonding, the example demonstrates how to retrieve the + server's Identity Resolving Key (IRK) in multiple formats: + - Comma-separated hex format: 0x1A,0x1B,0x1C,... + - Base64 encoded (for Home Assistant Private BLE Device service) + - Reverse hex order (for Home Assistant ESPresense) + This client is designed to work with the Server_secure_static_passkey example. Note that ESP32 uses Bluedroid by default and the other SoCs use NimBLE. @@ -12,9 +18,10 @@ This means that in NimBLE you can read the insecure characteristic without entering the passkey. This is not possible in Bluedroid. - IMPORTANT: MITM (Man-In-The-Middle protection) must be enabled for password prompts - to work. Without MITM, the BLE stack assumes no user interaction is needed and will use - "Just Works" pairing method (with encryption if secure connection is enabled). + IMPORTANT: + - MITM (Man-In-The-Middle protection) must be enabled for password prompts to work. + - Bonding must be enabled to store and retrieve the IRK. + - The server must distribute its Identity Key during pairing. Based on examples from Neil Kolban and h2zero. Created by lucasssvaz. @@ -36,10 +43,59 @@ static BLEUUID secureCharUUID("ff1d2614-e2d6-4c87-9154-6625d39ca7f8"); static boolean doConnect = false; static boolean connected = false; static boolean doScan = false; +static BLEClient *pClient = nullptr; static BLERemoteCharacteristic *pRemoteInsecureCharacteristic; static BLERemoteCharacteristic *pRemoteSecureCharacteristic; static BLEAdvertisedDevice *myDevice; +// Print an IRK buffer as hex with leading zeros and ':' separator +static void printIrkBinary(uint8_t *irk) { + for (int i = 0; i < 16; i++) { + if (irk[i] < 0x10) { + Serial.print("0"); + } + Serial.print(irk[i], HEX); + if (i < 15) { + Serial.print(":"); + } + } +} + +static void get_peer_irk(BLEAddress peerAddr) { + Serial.println("\n=== Retrieving peer IRK (Server) ===\n"); + + uint8_t irk[16]; + + // Get IRK in binary format + if (BLEDevice::getPeerIRK(peerAddr, irk)) { + Serial.println("Successfully retrieved peer IRK in binary format:"); + printIrkBinary(irk); + Serial.println("\n"); + } + + // Get IRK in different string formats + String irkString = BLEDevice::getPeerIRKString(peerAddr); + String irkBase64 = BLEDevice::getPeerIRKBase64(peerAddr); + String irkReverse = BLEDevice::getPeerIRKReverse(peerAddr); + + if (irkString.length() > 0) { + Serial.println("Successfully retrieved peer IRK in multiple formats:\n"); + Serial.print("IRK (comma-separated hex): "); + Serial.println(irkString); + Serial.print("IRK (Base64 for Home Assistant Private BLE Device): "); + Serial.println(irkBase64); + Serial.print("IRK (reverse hex for Home Assistant ESPresense): "); + Serial.println(irkReverse); + Serial.println(); + } else { + Serial.println("!!! Failed to retrieve peer IRK !!!"); + Serial.println("This is expected if bonding is disabled or the peer doesn't distribute its Identity Key."); + Serial.println("To enable bonding, change setAuthenticationMode to: pSecurity->setAuthenticationMode(true, true, true);\n"); + } + + Serial.println("=======================================\n"); +} + // Callback function to handle notifications static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) { Serial.print("Notify callback for characteristic "); @@ -62,11 +118,30 @@ class MyClientCallback : public BLEClientCallbacks { } }; +// Security callbacks to print IRKs once authentication completes +class MySecurityCallbacks : public BLESecurityCallbacks { +#if defined(CONFIG_BLUEDROID_ENABLED) + void onAuthenticationComplete(esp_ble_auth_cmpl_t desc) override { + // Print the IRK received by the peer + BLEAddress peerAddr(desc.bd_addr); + get_peer_irk(peerAddr); + } +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + void onAuthenticationComplete(ble_gap_conn_desc *desc) override { + // Print the IRK received by the peer + BLEAddress peerAddr(desc->peer_id_addr.val, desc->peer_id_addr.type); + get_peer_irk(peerAddr); + } +#endif +}; + bool connectToServer() { Serial.print("Forming a secure connection to "); Serial.println(myDevice->getAddress().toString().c_str()); - BLEClient *pClient = BLEDevice::createClient(); + pClient = BLEDevice::createClient(); Serial.println(" - Created client"); pClient->setClientCallbacks(new MyClientCallback()); @@ -192,8 +267,9 @@ void setup() { pSecurity->setPassKey(true, CLIENT_PIN); // Set authentication mode to match server requirements - // Enable secure connection and MITM (for password prompts) for this example - pSecurity->setAuthenticationMode(false, true, true); + // Enable bonding, MITM (for password prompts), and secure connection for this example + // Bonding is required to store and retrieve the IRK + pSecurity->setAuthenticationMode(true, true, true); // Set IO capability to KeyboardOnly // We need the proper IO capability for MITM authentication even @@ -201,6 +277,9 @@ void setup() { // See https://www.bluetooth.com/blog/bluetooth-pairing-part-2-key-generation-methods/ pSecurity->setCapability(ESP_IO_CAP_IN); + // Set callbacks to handle authentication completion and print IRKs + BLEDevice::setSecurityCallbacks(new MySecurityCallbacks()); + // Retrieve a Scanner and set the callback we want to use to be informed when we // have detected a new device. Specify that we want active scanning and start the // scan to run for 5 seconds. diff --git a/libraries/BLE/examples/Server_secure_static_passkey/Server_secure_static_passkey.ino b/libraries/BLE/examples/Server_secure_static_passkey/Server_secure_static_passkey.ino index 79b73695202..fef8c0bd15f 100644 --- a/libraries/BLE/examples/Server_secure_static_passkey/Server_secure_static_passkey.ino +++ b/libraries/BLE/examples/Server_secure_static_passkey/Server_secure_static_passkey.ino @@ -40,6 +40,73 @@ // This is an example passkey. You should use a different or random passkey. #define SERVER_PIN 123456 +// Print an IRK buffer as hex with leading zeros and ':' separator +static void printIrkBinary(uint8_t *irk) { + for (int i = 0; i < 16; i++) { + if (irk[i] < 0x10) { + Serial.print("0"); + } + Serial.print(irk[i], HEX); + if (i < 15) { + Serial.print(":"); + } + } +} + +static void get_peer_irk(BLEAddress peerAddr) { + Serial.println("\n=== Retrieving peer IRK (Client) ===\n"); + + uint8_t irk[16]; + + // Get IRK in binary format + if (BLEDevice::getPeerIRK(peerAddr, irk)) { + Serial.println("Successfully retrieved peer IRK in binary format:"); + printIrkBinary(irk); + Serial.println("\n"); + } + + // Get IRK in different string formats + String irkString = BLEDevice::getPeerIRKString(peerAddr); + String irkBase64 = BLEDevice::getPeerIRKBase64(peerAddr); + String irkReverse = BLEDevice::getPeerIRKReverse(peerAddr); + + if (irkString.length() > 0) { + Serial.println("Successfully retrieved peer IRK in multiple formats:\n"); + Serial.print("IRK (comma-separated hex): "); + Serial.println(irkString); + Serial.print("IRK (Base64 for Home Assistant Private BLE Device): "); + Serial.println(irkBase64); + Serial.print("IRK (reverse hex for Home Assistant ESPresense): "); + Serial.println(irkReverse); + Serial.println(); + } else { + Serial.println("!!! Failed to retrieve peer IRK !!!"); + Serial.println("This is expected if bonding is disabled or the peer doesn't distribute its Identity Key."); + Serial.println("To enable bonding, change setAuthenticationMode to: pSecurity->setAuthenticationMode(true, true, true);\n"); + } + + Serial.println("=======================================\n"); +} + +// Security callbacks to print IRKs once authentication completes +class MySecurityCallbacks : public BLESecurityCallbacks { +#if defined(CONFIG_BLUEDROID_ENABLED) + void onAuthenticationComplete(esp_ble_auth_cmpl_t desc) override { + // Print the IRK received by the peer + BLEAddress peerAddr(desc.bd_addr); + get_peer_irk(peerAddr); + } +#endif + +#if defined(CONFIG_NIMBLE_ENABLED) + void onAuthenticationComplete(ble_gap_conn_desc *desc) override { + // Print the IRK received by the peer + BLEAddress peerAddr(desc->peer_id_addr.val, desc->peer_id_addr.type); + get_peer_irk(peerAddr); + } +#endif +}; + void setup() { Serial.begin(115200); Serial.println("Starting BLE work!"); @@ -76,8 +143,11 @@ void setup() { pSecurity->setCapability(ESP_IO_CAP_OUT); // Set authentication mode - // Require secure connection and MITM (for password prompts) for this example - pSecurity->setAuthenticationMode(false, true, true); + // Enable bonding, MITM (for password prompts), and secure connection for this example + pSecurity->setAuthenticationMode(true, true, true); + + // Set callbacks to handle authentication completion and print IRKs + BLEDevice::setSecurityCallbacks(new MySecurityCallbacks()); BLEServer *pServer = BLEDevice::createServer(); pServer->advertiseOnDisconnect(true); diff --git a/libraries/BLE/src/BLEDevice.cpp b/libraries/BLE/src/BLEDevice.cpp index 6e125b07ef8..7a9df8c4cc4 100644 --- a/libraries/BLE/src/BLEDevice.cpp +++ b/libraries/BLE/src/BLEDevice.cpp @@ -38,6 +38,7 @@ #include "BLEUtils.h" #include "GeneralUtils.h" #include "BLESecurity.h" +#include "base64.h" #if defined(ARDUINO_ARCH_ESP32) #include "esp32-hal-bt.h" @@ -608,6 +609,181 @@ bool BLEDevice::getInitialized() { return initialized; } +/* + * @brief Get a peer device's Identity Resolving Key (IRK). + * @param [in] peerAddress The address of the bonded peer device. + * @param [out] irk Buffer to store the 16-byte IRK. + * @return True if successful, false otherwise. + * @note IRK is only available after bonding has occurred. + */ +bool BLEDevice::getPeerIRK(BLEAddress peerAddress, uint8_t *irk) { + log_v(">> BLEDevice::getPeerIRK()"); + + if (irk == nullptr) { + log_e("IRK buffer is null"); + return false; + } + +#if defined(CONFIG_BLUEDROID_ENABLED) + // Get the list of bonded devices + int dev_num = esp_ble_get_bond_device_num(); + if (dev_num == 0) { + log_e("No bonded devices found"); + return false; + } + + esp_ble_bond_dev_t *bond_dev = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * dev_num); + if (bond_dev == nullptr) { + log_e("Failed to allocate memory for bond device list"); + return false; + } + + esp_err_t ret = esp_ble_get_bond_device_list(&dev_num, bond_dev); + if (ret != ESP_OK) { + log_e("Failed to get bond device list: %d", ret); + free(bond_dev); + return false; + } + + // Find the bonded device that matches the peer address + bool found = false; + + for (int i = 0; i < dev_num; i++) { + BLEAddress bondAddr(bond_dev[i].bd_addr); + if (bondAddr.equals(peerAddress)) { + // Check if the PID key (which contains the IRK) is present + if (bond_dev[i].bond_key.key_mask & ESP_LE_KEY_PID) { + memcpy(irk, bond_dev[i].bond_key.pid_key.irk, 16); + found = true; + log_d("IRK found for peer: %s", peerAddress.toString().c_str()); + break; + } else { + log_w("PID key not present for peer: %s", peerAddress.toString().c_str()); + } + } + } + + free(bond_dev); + + if (!found) { + log_e("IRK not found for peer"); + return false; + } + + log_v("<< BLEDevice::getPeerIRK()"); + return true; +#endif // CONFIG_BLUEDROID_ENABLED + +#if defined(CONFIG_NIMBLE_ENABLED) + // Prepare the key structure to search for the peer's security information + struct ble_store_key_sec key_sec; + memset(&key_sec, 0, sizeof(key_sec)); + + // Convert BLEAddress to ble_addr_t + // NOTE: BLEAddress stores bytes in INVERSE order for NimBLE, + // but ble_addr_t.val expects them in normal order, so we reverse them + ble_addr_t addr; + uint8_t *peer_addr = peerAddress.getNative(); + for (int i = 0; i < 6; i++) { + addr.val[i] = peer_addr[5 - i]; + } + + // Try public address first, then random if that fails + addr.type = BLE_ADDR_PUBLIC; + memcpy(&key_sec.peer_addr, &addr, sizeof(ble_addr_t)); + + // Read the peer's security information from the store + struct ble_store_value_sec value_sec; + int rc = ble_store_read_peer_sec(&key_sec, &value_sec); + + // If public address failed, try random address type + if (rc != 0) { + addr.type = BLE_ADDR_RANDOM; + memcpy(&key_sec.peer_addr, &addr, sizeof(ble_addr_t)); + rc = ble_store_read_peer_sec(&key_sec, &value_sec); + + if (rc != 0) { + log_e("IRK not found for peer: %s", peerAddress.toString().c_str()); + return false; + } + } + + // Check if the IRK is present + if (!value_sec.irk_present) { + log_e("IRK not present for peer"); + return false; + } + + // Copy the IRK to the output buffer + memcpy(irk, value_sec.irk, 16); + + log_d("IRK found for peer: %s (type=%d)", peerAddress.toString().c_str(), addr.type); + log_v("<< BLEDevice::getPeerIRK()"); + return true; +#endif // CONFIG_NIMBLE_ENABLED +} + +/* + * @brief Get a peer device's IRK as a comma-separated hex string. + * @param [in] peerAddress The address of the bonded peer device. + * @return String in format "0xXX,0xXX,..." or empty string on failure. + */ +String BLEDevice::getPeerIRKString(BLEAddress peerAddress) { + uint8_t irk[16]; + if (!getPeerIRK(peerAddress, irk)) { + return String(); + } + + String result = ""; + for (int i = 0; i < 16; i++) { + result += "0x"; + if (irk[i] < 0x10) { + result += "0"; + } + result += String(irk[i], HEX); + if (i < 15) { + result += ","; + } + } + return result; +} + +/* + * @brief Get a peer device's IRK as a Base64 encoded string. + * @param [in] peerAddress The address of the bonded peer device. + * @return Base64 encoded string or empty string on failure. + */ +String BLEDevice::getPeerIRKBase64(BLEAddress peerAddress) { + uint8_t irk[16]; + if (!getPeerIRK(peerAddress, irk)) { + return String(); + } + + return base64::encode(irk, 16); +} + +/* + * @brief Get a peer device's IRK in reverse hex format. + * @param [in] peerAddress The address of the bonded peer device. + * @return String in reverse hex format (uppercase) or empty string on failure. + */ +String BLEDevice::getPeerIRKReverse(BLEAddress peerAddress) { + uint8_t irk[16]; + if (!getPeerIRK(peerAddress, irk)) { + return String(); + } + + String result = ""; + for (int i = 15; i >= 0; i--) { + if (irk[i] < 0x10) { + result += "0"; + } + result += String(irk[i], HEX); + } + result.toUpperCase(); + return result; +} + BLEAdvertising *BLEDevice::getAdvertising() { if (m_bleAdvertising == nullptr) { m_bleAdvertising = new BLEAdvertising(); diff --git a/libraries/BLE/src/BLEDevice.h b/libraries/BLE/src/BLEDevice.h index ba29c735afd..9d9cbf0421e 100644 --- a/libraries/BLE/src/BLEDevice.h +++ b/libraries/BLE/src/BLEDevice.h @@ -192,6 +192,10 @@ class BLEDevice { static esp_err_t setMTU(uint16_t mtu); static uint16_t getMTU(); static bool getInitialized(); + static bool getPeerIRK(BLEAddress peerAddress, uint8_t *irk); + static String getPeerIRKString(BLEAddress peerAddress); + static String getPeerIRKBase64(BLEAddress peerAddress); + static String getPeerIRKReverse(BLEAddress peerAddress); static BLEAdvertising *getAdvertising(); static void startAdvertising(); static void stopAdvertising(); From 2be60ae86897c41813bdb9cf1c9ef1f55c734f6f Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:22:54 -0300 Subject: [PATCH 3/4] fix(ble): Fix notification timing --- libraries/BLE/src/BLECharacteristic.cpp | 146 ++++++++++++++++++++++++ libraries/BLE/src/BLECharacteristic.h | 3 + 2 files changed, 149 insertions(+) diff --git a/libraries/BLE/src/BLECharacteristic.cpp b/libraries/BLE/src/BLECharacteristic.cpp index a492116b2e6..c7ed91d4669 100644 --- a/libraries/BLE/src/BLECharacteristic.cpp +++ b/libraries/BLE/src/BLECharacteristic.cpp @@ -954,8 +954,18 @@ int BLECharacteristic::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr } rc = ble_gap_conn_find(conn_handle, &desc); assert(rc == 0); + + // Set write context flag to defer notifications + pCharacteristic->m_inWriteContext = true; pCharacteristic->setValue(buf, len); pCharacteristic->m_pCallbacks->onWrite(pCharacteristic, &desc); + pCharacteristic->m_inWriteContext = false; + + // Execute any deferred notifications after write context ends + if (pCharacteristic->m_deferNotifications) { + pCharacteristic->m_deferNotifications = false; + pCharacteristic->notifyDeferred(); + } return 0; } @@ -1025,6 +1035,14 @@ void BLECharacteristic::notify(bool is_notification) { assert(getService() != nullptr); assert(getService()->getServer() != nullptr); + // If we're in a write context, defer the notification + if (m_inWriteContext) { + log_v("Deferring notification - in write context"); + m_deferNotifications = true; + m_pCallbacks->onNotify(this); // Still invoke the callback + return; + } + int rc = 0; m_pCallbacks->onNotify(this); // Invoke the notify callback. @@ -1153,6 +1171,134 @@ void BLECharacteristic::notify(bool is_notification) { log_v("<< notify"); } // Notify +/** + * @brief Execute deferred notifications. + * This function is called after a write context ends to send any notifications + * that were deferred during the write operation. + * @return N/A. + */ +void BLECharacteristic::notifyDeferred() { + log_v(">> notifyDeferred: executing deferred notifications"); + + assert(getService() != nullptr); + assert(getService()->getServer() != nullptr); + + int rc = 0; + m_pCallbacks->onNotify(this); // Invoke the notify callback. + + // GeneralUtils::hexDump() doesn't output anything if the log level is not + // "VERBOSE". Additionally, it is very CPU intensive, even when it doesn't + // output anything! So it is much better to *not* call it at all if not needed. + // In a simple program which calls BLECharacteristic::notify() every 50 ms, + // the performance gain of this little optimization is 37% in release mode + // (-O3) and 57% in debug mode. + // Of course, the "#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE" guard + // could also be put inside the GeneralUtils::hexDump() function itself. But + // it's better to put it here also, as it is clearer (indicating a verbose log + // thing) and it allows to remove the "m_value.getValue().c_str()" call, which + // is, in itself, quite CPU intensive. +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + GeneralUtils::hexDump((uint8_t *)m_value.getValue().c_str(), m_value.getValue().length()); +#endif + + if (getService()->getServer()->getConnectedCount() == 0) { + log_v("<< notifyDeferred: No connected clients."); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NO_CLIENT, 0); + return; + } + + if (m_subscribedVec.size() == 0) { + log_v("<< notifyDeferred: No clients subscribed."); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_NO_SUBSCRIBER, 0); + return; + } + + bool reqSec = (m_properties & BLE_GATT_CHR_F_READ_AUTHEN) || (m_properties & BLE_GATT_CHR_F_READ_AUTHOR) || (m_properties & BLE_GATT_CHR_F_READ_ENC); + + for (auto &myPair : m_subscribedVec) { + uint16_t _mtu = getService()->getServer()->getPeerMTU(myPair.first); + + // check if connected and subscribed + if (_mtu == 0 || myPair.second == 0) { + continue; + } + + if (reqSec) { + struct ble_gap_conn_desc desc; + rc = ble_gap_conn_find(myPair.first, &desc); + if (rc != 0 || !desc.sec_state.encrypted) { + continue; + } + } + + String value = getValue(); + size_t length = value.length(); + + if (length > _mtu - 3) { + log_w("- Truncating to %d bytes (maximum notify size)", _mtu - 3); + } + + // For deferred notifications, default to notification (not indication) + bool is_notification = true; + if (is_notification && (!(myPair.second & NIMBLE_SUB_NOTIFY))) { + log_w("Sending notification to client subscribed to indications, sending indication instead"); + is_notification = false; + } + + if (!is_notification && (!(myPair.second & NIMBLE_SUB_INDICATE))) { + log_w("Sending indication to client subscribed to notification, sending notification instead"); + is_notification = true; + } + + if (!is_notification) { // is indication + m_semaphoreConfEvt.take("indicate"); + } + + // don't create the m_buf until we are sure to send the data or else + // we could be allocating a buffer that doesn't get released. + // We also must create it in each loop iteration because it is consumed with each host call. + os_mbuf *om = ble_hs_mbuf_from_flat((uint8_t *)value.c_str(), length); + + if (!is_notification && (m_properties & BLECharacteristic::PROPERTY_INDICATE)) { + if (!BLEDevice::getServer()->setIndicateWait(myPair.first)) { + log_e("prior Indication in progress"); + os_mbuf_free_chain(om); + return; + } + + rc = ble_gatts_indicate_custom(myPair.first, m_handle, om); + if (rc != 0) { + BLEDevice::getServer()->clearIndicateWait(myPair.first); + } + } else { + rc = ble_gatts_notify_custom(myPair.first, m_handle, om); + } + + if (rc != 0) { + log_e("<< ble_gatts_%s_custom: rc=%d %s", is_notification ? "notify" : "indicate", rc, GeneralUtils::errorToString(rc)); + m_semaphoreConfEvt.give(); + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_GATT, rc); // Invoke the notify callback. + return; + } + + if (!is_notification) { // is indication + if (!m_semaphoreConfEvt.timedWait("indicate", indicationTimeout)) { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_TIMEOUT, 0); // Invoke the notify callback. + } else { + auto code = m_semaphoreConfEvt.value(); + if (code == ESP_OK) { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_INDICATE, code); // Invoke the notify callback. + } else { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::ERROR_INDICATE_FAILURE, code); + } + } + } else { + m_pCallbacks->onStatus(this, BLECharacteristicCallbacks::Status::SUCCESS_NOTIFY, 0); // Invoke the notify callback. + } + } + log_v("<< notifyDeferred"); +} // notifyDeferred + void BLECharacteristicCallbacks::onRead(BLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc) { onRead(pCharacteristic); } // onRead diff --git a/libraries/BLE/src/BLECharacteristic.h b/libraries/BLE/src/BLECharacteristic.h index b0ab8f916ea..fe00efa7bd3 100644 --- a/libraries/BLE/src/BLECharacteristic.h +++ b/libraries/BLE/src/BLECharacteristic.h @@ -246,6 +246,8 @@ class BLECharacteristic { portMUX_TYPE m_readMux; uint8_t m_removed; std::vector> m_subscribedVec; + bool m_inWriteContext = false; + bool m_deferNotifications = false; #endif /*************************************************************************** @@ -271,6 +273,7 @@ class BLECharacteristic { #if defined(CONFIG_NIMBLE_ENABLED) void setSubscribe(struct ble_gap_event *event); static int handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); + void notifyDeferred(); #endif }; // BLECharacteristic From 5a91832e9f9bc17b91a4061a1561dcf8c3cf750a Mon Sep 17 00:00:00 2001 From: Lucas Saavedra Vaz <32426024+lucasssvaz@users.noreply.github.com> Date: Fri, 24 Oct 2025 22:30:48 -0300 Subject: [PATCH 4/4] fix(ble): Fix deinit function --- libraries/BLE/src/BLEDevice.cpp | 47 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/libraries/BLE/src/BLEDevice.cpp b/libraries/BLE/src/BLEDevice.cpp index 7a9df8c4cc4..8b809abdaf3 100644 --- a/libraries/BLE/src/BLEDevice.cpp +++ b/libraries/BLE/src/BLEDevice.cpp @@ -867,13 +867,47 @@ void BLEDevice::removePeerDevice(uint16_t conn_id, bool _client) { /** * @brief de-Initialize the %BLE environment. - * @param release_memory release the internal BT stack memory + * @param release_memory release the internal BT stack memory (prevents reinitialization) */ void BLEDevice::deinit(bool release_memory) { if (!initialized) { return; } + // Stop advertising and scanning first + if (m_bleAdvertising != nullptr) { + m_bleAdvertising->stop(); + } + + if (m_pScan != nullptr) { + m_pScan->stop(); + } + + // Delete all BLE objects + if (m_bleAdvertising != nullptr) { + delete m_bleAdvertising; + m_bleAdvertising = nullptr; + } + + if (m_pScan != nullptr) { + delete m_pScan; + m_pScan = nullptr; + } + + if (m_pServer != nullptr) { + delete m_pServer; + m_pServer = nullptr; + } + + if (m_pClient != nullptr) { + delete m_pClient; + m_pClient = nullptr; + } + + // Clear the connected clients map + m_connectedClientsMap.clear(); + + // Always deinit the BLE stack #ifdef CONFIG_BLUEDROID_ENABLED esp_bluedroid_disable(); esp_bluedroid_deinit(); @@ -893,19 +927,20 @@ void BLEDevice::deinit(bool release_memory) { esp_bt_controller_deinit(); #endif -#ifdef ARDUINO_ARCH_ESP32 + // Only release memory if requested (this prevents reinitialization) if (release_memory) { +#ifdef ARDUINO_ARCH_ESP32 // Require tests because we released classic BT memory and this can cause crash (most likely not, esp-idf takes care of it) #if CONFIG_BT_CONTROLLER_ENABLED esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); #endif - } else { -#ifdef CONFIG_NIMBLE_ENABLED - m_synced = false; #endif - initialized = false; } + +#ifdef CONFIG_NIMBLE_ENABLED + m_synced = false; #endif + initialized = false; } void BLEDevice::setCustomGapHandler(gap_event_handler handler) {