diff --git a/examples/NimBLE_Async_Client/CMakeLists.txt b/examples/NimBLE_Async_Client/CMakeLists.txt new file mode 100644 index 0000000..dcfcd4b --- /dev/null +++ b/examples/NimBLE_Async_Client/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(NimBLE_Async_Client) diff --git a/examples/NimBLE_Async_Client/Makefile b/examples/NimBLE_Async_Client/Makefile new file mode 100644 index 0000000..7b4b799 --- /dev/null +++ b/examples/NimBLE_Async_Client/Makefile @@ -0,0 +1,3 @@ +PROJECT_NAME := NimBLE_Async_Client + +include $(IDF_PATH)/make/project.mk diff --git a/examples/NimBLE_Async_Client/main/CMakeLists.txt b/examples/NimBLE_Async_Client/main/CMakeLists.txt new file mode 100644 index 0000000..9be9075 --- /dev/null +++ b/examples/NimBLE_Async_Client/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(COMPONENT_SRCS "main.cpp") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +register_component() \ No newline at end of file diff --git a/examples/NimBLE_Async_Client/main/component.mk b/examples/NimBLE_Async_Client/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/examples/NimBLE_Async_Client/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/examples/NimBLE_Async_Client/main/main.cpp b/examples/NimBLE_Async_Client/main/main.cpp new file mode 100644 index 0000000..50a7ed3 --- /dev/null +++ b/examples/NimBLE_Async_Client/main/main.cpp @@ -0,0 +1,83 @@ + +/** + * NimBLE_Async_client Demo: + * + * Demonstrates asynchronous client operations. + * + * Created: on November 4, 2024 + * Author: H2zero + * + */ + +#include + +static constexpr uint32_t scanTimeMs = 5 * 1000; + +class ClientCallbacks : public NimBLEClientCallbacks { + void onConnect(NimBLEClient* pClient) { + printf("Connected to: %s\n", pClient->getPeerAddress().toString().c_str()); + } + + void onDisconnect(NimBLEClient* pClient, int reason) { + printf("%s Disconnected, reason = %d - Starting scan\n", pClient->getPeerAddress().toString().c_str(), reason); + NimBLEDevice::getScan()->start(scanTimeMs); + } +} clientCB; + +class scanCallbacks : public NimBLEScanCallbacks { + void onResult(NimBLEAdvertisedDevice* advertisedDevice) { + printf("Advertised Device found: %s\n", advertisedDevice->toString().c_str()); + if (advertisedDevice->haveName() && advertisedDevice->getName() == "NimBLE-Server") { + printf("Found Our Device\n"); + + auto pClient = NimBLEDevice::getDisconnectedClient(); + if (!pClient) { + pClient = NimBLEDevice::createClient(advertisedDevice->getAddress()); + if (!pClient) { + printf("Failed to create client\n"); + return; + } + } + + pClient->setClientCallbacks(&clientCB, false); + if (!pClient->connect(true, true, false)) { // delete attributes, async connect, no MTU exchange + NimBLEDevice::deleteClient(pClient); + printf("Failed to connect\n"); + return; + } + } + } + + void onScanEnd(NimBLEScanResults results) { + printf("Scan Ended\n"); + NimBLEDevice::getScan()->start(scanTimeMs); + } +}; + +extern "C" void app_main(void) { + printf("Starting NimBLE Async Client\n"); + NimBLEDevice::init(""); + NimBLEDevice::setPower(3); /** +3db */ + + NimBLEScan* pScan = NimBLEDevice::getScan(); + pScan->setScanCallbacks(new scanCallbacks()); + pScan->setInterval(45); + pScan->setWindow(15); + pScan->setActiveScan(true); + pScan->start(scanTimeMs); + + for (;;) { + vTaskDelay(pdMS_TO_TICKS(1000)); + auto pClients = NimBLEDevice::getConnectedClients(); + if (!pClients.size()) { + continue; + } + + for (auto& pClient : pClients) { + printf("%s\n", pClient->toString().c_str()); + NimBLEDevice::deleteClient(pClient); + } + + NimBLEDevice::getScan()->start(scanTimeMs); + } +} diff --git a/examples/NimBLE_Async_Client/sdkconfig.defaults b/examples/NimBLE_Async_Client/sdkconfig.defaults new file mode 100644 index 0000000..c829fc5 --- /dev/null +++ b/examples/NimBLE_Async_Client/sdkconfig.defaults @@ -0,0 +1,12 @@ +# Override some defaults so BT stack is enabled +# in this example + +# +# BT config +# +CONFIG_BT_ENABLED=y +CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y +CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n +CONFIG_BTDM_CTRL_MODE_BTDM=n +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y diff --git a/src/NimBLEClient.cpp b/src/NimBLEClient.cpp index 1de4a63..86a6bb6 100644 --- a/src/NimBLEClient.cpp +++ b/src/NimBLEClient.cpp @@ -65,6 +65,8 @@ NimBLEClient::NimBLEClient(const NimBLEAddress& peerAddress) m_terminateFailCount{0}, m_deleteCallbacks{false}, m_connEstablished{false}, + m_asyncConnect{false}, + m_exchangeMTU{true}, # if CONFIG_BT_NIMBLE_EXT_ADV m_phyMask{BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK | BLE_GAP_LE_PHY_CODED_MASK}, # endif @@ -123,35 +125,48 @@ size_t NimBLEClient::deleteService(const NimBLEUUID& uuid) { } // deleteServices /** - * @brief Connect to the BLE Server. + * @brief Connect to the BLE Server using the address of the last connected device, or the address\n + * passed to the constructor. * @param [in] deleteAttributes If true this will delete any attribute objects this client may already\n - * have created and clears the vectors after successful connection. - * @return True on success. + * have created when last connected. + * @param [in] asyncConnect If true, the connection will be made asynchronously and this function will return immediately.\n + * If false, this function will block until the connection is established or the connection attempt times out. + * @param [in] exchangeMTU If true, the client will attempt to exchange MTU with the server after connection.\n + * If false, the client will use the default MTU size and the application will need to call exchangeMTU() later. + * @return true on success. */ -bool NimBLEClient::connect(bool deleteAttributes) { - return connect(m_peerAddress, deleteAttributes); +bool NimBLEClient::connect(bool deleteAttributes, bool asyncConnect, bool exchangeMTU) { + return connect(m_peerAddress, deleteAttributes, asyncConnect, exchangeMTU); } /** * @brief Connect to an advertising device. * @param [in] device The device to connect to. * @param [in] deleteAttributes If true this will delete any attribute objects this client may already\n - * have created and clears the vectors after successful connection. - * @return True on success. + * have created when last connected. + * @param [in] asyncConnect If true, the connection will be made asynchronously and this function will return immediately.\n + * If false, this function will block until the connection is established or the connection attempt times out. + * @param [in] exchangeMTU If true, the client will attempt to exchange MTU with the server after connection.\n + * If false, the client will use the default MTU size and the application will need to call exchangeMTU() later. + * @return true on success. */ -bool NimBLEClient::connect(NimBLEAdvertisedDevice* device, bool deleteAttributes) { +bool NimBLEClient::connect(NimBLEAdvertisedDevice* device, bool deleteAttributes, bool asyncConnect, bool exchangeMTU) { NimBLEAddress address(device->getAddress()); - return connect(address, deleteAttributes); + return connect(address, deleteAttributes, asyncConnect, exchangeMTU); } /** * @brief Connect to a BLE Server by address. * @param [in] address The address of the server. * @param [in] deleteAttributes If true this will delete any attribute objects this client may already\n - * have created and clears the vectors after successful connection. - * @return True on success. + * have created when last connected. + * @param [in] asyncConnect If true, the connection will be made asynchronously and this function will return immediately.\n + * If false, this function will block until the connection is established or the connection attempt times out. + * @param [in] exchangeMTU If true, the client will attempt to exchange MTU with the server after connection.\n + * If false, the client will use the default MTU size and the application will need to call exchangeMTU() later. + * @return true on success. */ -bool NimBLEClient::connect(const NimBLEAddress& address, bool deleteAttributes) { +bool NimBLEClient::connect(const NimBLEAddress& address, bool deleteAttributes, bool asyncConnect, bool exchangeMTU) { NIMBLE_LOGD(LOG_TAG, ">> connect(%s)", address.toString().c_str()); if (!NimBLEDevice::m_synced) { @@ -159,8 +174,13 @@ bool NimBLEClient::connect(const NimBLEAddress& address, bool deleteAttributes) return false; } - if (isConnected() || m_connEstablished || m_pTaskData != nullptr) { - NIMBLE_LOGE(LOG_TAG, "Client busy, connected to %s, handle=%d", std::string(m_peerAddress).c_str(), getConnHandle()); + if (isConnected() || m_connEstablished) { + NIMBLE_LOGE(LOG_TAG, "Client already connected"); + return false; + } + + if (NimBLEDevice::isConnectionInProgress()) { + NIMBLE_LOGE(LOG_TAG, "Connection already in progress"); return false; } @@ -171,16 +191,24 @@ bool NimBLEClient::connect(const NimBLEAddress& address, bool deleteAttributes) } if (address.isNull()) { - NIMBLE_LOGE(LOG_TAG, "Invalid peer address;(NULL)"); + NIMBLE_LOGE(LOG_TAG, "Invalid peer address; (NULL)"); return false; } else { m_peerAddress = address; } - TaskHandle_t cur_task = xTaskGetCurrentTaskHandle(); - BleTaskData taskData = {this, cur_task, 0, nullptr}; - m_pTaskData = &taskData; + if (deleteAttributes) { + deleteServices(); + } + int rc = 0; + m_asyncConnect = asyncConnect; + m_exchangeMTU = exchangeMTU; + TaskHandle_t curTask = xTaskGetCurrentTaskHandle(); + BleTaskData taskData = {this, curTask, 0, nullptr}; + if (!asyncConnect) { + m_pTaskData = &taskData; + } // Set the connection in progress flag to prevent a scan from starting while connecting. NimBLEDevice::setConnectionInProgress(true); @@ -222,10 +250,7 @@ bool NimBLEClient::connect(const NimBLEAddress& address, bool deleteAttributes) break; case BLE_HS_EALREADY: - // Already attempting to connect to this device, cancel the previous - // attempt and report failure here so we don't get 2 connections. - NIMBLE_LOGE(LOG_TAG, "Already attempting to connect to %s - cancelling", std::string(m_peerAddress).c_str()); - ble_gap_conn_cancel(); + NIMBLE_LOGE(LOG_TAG, "Already attempting to connect"); break; default: @@ -239,16 +264,21 @@ bool NimBLEClient::connect(const NimBLEAddress& address, bool deleteAttributes) } while (rc == BLE_HS_EBUSY); - NimBLEDevice::setConnectionInProgress(false); m_lastErr = rc; if (rc != 0) { + m_lastErr = rc; m_pTaskData = nullptr; + NimBLEDevice::setConnectionInProgress(false); return false; } + if (m_asyncConnect) { + return true; + } + # ifdef ulTaskNotifyValueClear // Clear the task notification value to ensure we block - ulTaskNotifyValueClear(cur_task, ULONG_MAX); + ulTaskNotifyValueClear(curTask, ULONG_MAX); # endif // Wait for the connect timeout time +1 second for the connection to complete if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(m_connectTimeout + 1000)) == pdFALSE) { @@ -278,10 +308,6 @@ bool NimBLEClient::connect(const NimBLEAddress& address, bool deleteAttributes) NIMBLE_LOGI(LOG_TAG, "Connection established"); } - if (deleteAttributes) { - deleteServices(); - } - m_connEstablished = true; m_pClientCallbacks->onConnect(this); @@ -854,6 +880,41 @@ uint16_t NimBLEClient::getMTU() const { return ble_att_mtu(m_connHandle); } // getMTU +/** + * @brief Callback for the MTU exchange API function. + * @details When the MTU exchange is complete the API will call this and report the new MTU. + */ +int NimBLEClient::exchangeMTUCb(uint16_t conn_handle, const ble_gatt_error* error, uint16_t mtu, void* arg) { + NIMBLE_LOGD(LOG_TAG, "exchangeMTUCb: status=%d, mtu=%d", error->status, mtu); + + NimBLEClient* pClient = (NimBLEClient*)arg; + if (pClient->getConnHandle() != conn_handle) { + return 0; + } + + if (error->status != 0) { + NIMBLE_LOGE(LOG_TAG, "exchangeMTUCb() rc=%d %s", error->status, NimBLEUtils::returnCodeToString(error->status)); + pClient->m_lastErr = error->status; + } + + return 0; +} + +/** + * @brief Begin the MTU exchange process with the server. + * @returns true if the request was sent successfully. + */ +bool NimBLEClient::exchangeMTU() { + int rc = ble_gattc_exchange_mtu(m_connHandle, NimBLEClient::exchangeMTUCb, this); + if (rc != 0) { + NIMBLE_LOGE(LOG_TAG, "MTU exchange error; rc=%d %s", rc, NimBLEUtils::returnCodeToString(rc)); + m_lastErr = rc; + return false; + } + + return true; +} // exchangeMTU + /** * @brief Handle a received GAP event. * @param [in] event The event structure sent by the NimBLE stack. @@ -908,21 +969,22 @@ int NimBLEClient::handleGapEvent(struct ble_gap_event* event, void* arg) { case BLE_GAP_EVENT_CONNECT: { // If we aren't waiting for this connection response we should drop the connection immediately. - if (pClient->isConnected() || pClient->m_pTaskData == nullptr) { + if (pClient->isConnected() || (!pClient->m_asyncConnect && pClient->m_pTaskData == nullptr)) { ble_gap_terminate(event->connect.conn_handle, BLE_ERR_REM_USER_CONN_TERM); return 0; } + NimBLEDevice::setConnectionInProgress(false); rc = event->connect.status; if (rc == 0) { NIMBLE_LOGI(LOG_TAG, "Connected event"); pClient->m_connHandle = event->connect.conn_handle; - - rc = ble_gattc_exchange_mtu(pClient->m_connHandle, NULL, NULL); - if (rc != 0) { - NIMBLE_LOGE(LOG_TAG, "MTU exchange error; rc=%d %s", rc, NimBLEUtils::returnCodeToString(rc)); - break; + if (pClient->m_exchangeMTU) { + if (!pClient->exchangeMTU() && !pClient->m_asyncConnect) { + rc = pClient->m_lastErr; + break; + } } // In the case of a multi-connecting device we ignore this device when @@ -930,7 +992,16 @@ int NimBLEClient::handleGapEvent(struct ble_gap_event* event, void* arg) { NimBLEDevice::addIgnored(pClient->m_peerAddress); } else { pClient->m_connHandle = BLE_HS_CONN_HANDLE_NONE; - break; + if (!pClient->m_asyncConnect) { + break; + } + } + + if (pClient->m_asyncConnect) { + pClient->m_connEstablished = rc == 0; + pClient->m_pClientCallbacks->onConnect(pClient); + } else if (!pClient->m_exchangeMTU) { + break; // not wating for MTU exchange so release the task now. } return 0; @@ -1072,7 +1143,9 @@ int NimBLEClient::handleGapEvent(struct ble_gap_event* event, void* arg) { if (pClient->m_connHandle != event->mtu.conn_handle) { return 0; } - NIMBLE_LOGI(LOG_TAG, "mtu update event; conn_handle=%d mtu=%d", event->mtu.conn_handle, event->mtu.value); + + NIMBLE_LOGI(LOG_TAG, "mtu update: mtu=%d", event->mtu.value); + pClient->m_pClientCallbacks->onMTUChange(pClient, event->mtu.value); rc = 0; break; } // BLE_GAP_EVENT_MTU @@ -1204,4 +1277,8 @@ void NimBLEClientCallbacks::onConfirmPasskey(NimBLEConnInfo& connInfo, uint32_t NimBLEDevice::injectConfirmPasskey(connInfo, true); } +void NimBLEClientCallbacks::onMTUChange(NimBLEClient* pClient, uint16_t mtu) { + NIMBLE_LOGD(CB_TAG, "onMTUChange: default"); +} + #endif /* CONFIG_BT_ENABLED && CONFIG_BT_NIMBLE_ROLE_CENTRAL */ diff --git a/src/NimBLEClient.h b/src/NimBLEClient.h index ef5a08f..c9ec090 100644 --- a/src/NimBLEClient.h +++ b/src/NimBLEClient.h @@ -44,9 +44,12 @@ struct BleTaskData; */ class NimBLEClient { public: - bool connect(NimBLEAdvertisedDevice* device, bool deleteAttributes = true); - bool connect(const NimBLEAddress& address, bool deleteAttributes = true); - bool connect(bool deleteAttributes = true); + bool connect(NimBLEAdvertisedDevice* device, + bool deleteAttributes = true, + bool asyncConnect = false, + bool exchangeMTU = true); + bool connect(const NimBLEAddress& address, bool deleteAttributes = true, bool asyncConnect = false, bool exchangeMTU = true); + bool connect(bool deleteAttributes = true, bool asyncConnect = false, bool exchangeMTU = true); bool disconnect(uint8_t reason = BLE_ERR_REM_USER_CONN_TERM); NimBLEAddress getPeerAddress() const; bool setPeerAddress(const NimBLEAddress& address); @@ -59,6 +62,7 @@ class NimBLEClient { bool setConnection(const NimBLEConnInfo& connInfo); bool setConnection(uint16_t connHandle); uint16_t getMTU() const; + bool exchangeMTU(); bool secureConnection() const; void setConnectTimeout(uint32_t timeout); bool setDataLen(uint16_t txOctets); @@ -98,6 +102,7 @@ class NimBLEClient { bool retrieveServices(const NimBLEUUID* uuidFilter = nullptr); static int handleGapEvent(struct ble_gap_event* event, void* arg); + static int exchangeMTUCb(uint16_t conn_handle, const ble_gatt_error* error, uint16_t mtu, void* arg); static int serviceDiscoveredCB(uint16_t connHandle, const struct ble_gatt_error* error, const struct ble_gatt_svc* service, @@ -113,6 +118,8 @@ class NimBLEClient { uint8_t m_terminateFailCount; bool m_deleteCallbacks; bool m_connEstablished; + bool m_asyncConnect; + bool m_exchangeMTU; # if CONFIG_BT_NIMBLE_EXT_ADV uint8_t m_phyMask; # endif @@ -174,6 +181,14 @@ class NimBLEClientCallbacks { * @param [in] connInfo A reference to a NimBLEConnInfo instance with information */ virtual void onIdentity(NimBLEConnInfo& connInfo); + + /** + * @brief Called when the connection MTU changes. + * @param [in] pClient A pointer to the client that the MTU change is associated with. + * @param [in] MTU The new MTU value. + * about the peer connection parameters. + */ + virtual void onMTUChange(NimBLEClient* pClient, uint16_t MTU); }; #endif /* CONFIG_BT_ENABLED && CONFIG_BT_NIMBLE_ROLE_CENTRAL */ diff --git a/src/NimBLEDevice.cpp b/src/NimBLEDevice.cpp index 784514e..1f13709 100644 --- a/src/NimBLEDevice.cpp +++ b/src/NimBLEDevice.cpp @@ -405,7 +405,7 @@ NimBLEClient* NimBLEDevice::getClientByPeerAddress(const NimBLEAddress& addr) { } // getClientPeerAddress /** - * @brief Finds the first disconnected client in the list. + * @brief Finds the first disconnected client available. * @return A pointer to the first client object that is not connected to a peer or nullptr. */ NimBLEClient* NimBLEDevice::getDisconnectedClient() { @@ -418,6 +418,21 @@ NimBLEClient* NimBLEDevice::getDisconnectedClient() { return nullptr; } // getDisconnectedClient +/** + * @brief Get a list of connected clients. + * @return A vector of connected client objects. + */ +std::vector NimBLEDevice::getConnectedClients() { + std::vector clients; + for (const auto clt : m_pClients) { + if (clt != nullptr && clt->isConnected()) { + clients.push_back(clt); + } + } + + return clients; +} // getConnectedClients + # endif // #if defined(CONFIG_BT_NIMBLE_ROLE_CENTRAL) /* -------------------------------------------------------------------------- */ diff --git a/src/NimBLEDevice.h b/src/NimBLEDevice.h index e6c1144..9a722b7 100644 --- a/src/NimBLEDevice.h +++ b/src/NimBLEDevice.h @@ -168,13 +168,14 @@ class NimBLEDevice { # endif # if defined(CONFIG_BT_NIMBLE_ROLE_CENTRAL) - static NimBLEClient* createClient(); - static NimBLEClient* createClient(const NimBLEAddress& peerAddress); - static bool deleteClient(NimBLEClient* pClient); - static NimBLEClient* getClientByHandle(uint16_t connHandle); - static NimBLEClient* getClientByPeerAddress(const NimBLEAddress& peerAddress); - static NimBLEClient* getDisconnectedClient(); - static size_t getCreatedClientCount(); + static NimBLEClient* createClient(); + static NimBLEClient* createClient(const NimBLEAddress& peerAddress); + static bool deleteClient(NimBLEClient* pClient); + static NimBLEClient* getClientByHandle(uint16_t connHandle); + static NimBLEClient* getClientByPeerAddress(const NimBLEAddress& peerAddress); + static NimBLEClient* getDisconnectedClient(); + static size_t getCreatedClientCount(); + static std::vector getConnectedClients(); # endif # if defined(CONFIG_BT_NIMBLE_ROLE_CENTRAL) || defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL)