From 471bf1c595b4de4a904da04b501da149e6a0046d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:37:44 +0000 Subject: [PATCH 1/3] Initial plan From fecc914859c31617d53a4f7301a769d5b66466ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:57:36 +0000 Subject: [PATCH 2/3] Fix NimBLE_Stream_Server NUS compatibility: add two-characteristic support Agent-Logs-Url: https://github.com/h2zero/NimBLE-Arduino/sessions/07db667d-a538-4e10-93a5-e30616f1f6a9 Co-authored-by: h2zero <32826625+h2zero@users.noreply.github.com> --- .../NimBLE_Stream_Client.ino | 37 ++- examples/NimBLE_Stream_Client/README.md | 16 +- .../NimBLE_Stream_Server.ino | 22 +- examples/NimBLE_Stream_Server/README.md | 21 +- src/NimBLEStream.cpp | 253 +++++++++++++++++- src/NimBLEStream.h | 50 +++- 6 files changed, 347 insertions(+), 52 deletions(-) diff --git a/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino b/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino index 1ae4c30a7..eb30b17c0 100644 --- a/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino +++ b/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino @@ -7,7 +7,8 @@ * This allows you to use familiar methods like print(), println(), * read(), and available() over BLE, similar to how you would use Serial. * - * This example connects to the NimBLE_Stream_Server example. + * This example connects to the NimBLE_Stream_Server example using the Nordic UART + * Service (NUS) with separate TX and RX characteristics. * * Created: November 2025 * Author: NimBLE-Arduino Contributors @@ -16,9 +17,10 @@ #include #include -// Service and Characteristic UUIDs (must match the server) -#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" -#define CHARACTERISTIC_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +// Nordic UART Service (NUS) UUIDs (must match the server) +#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: client subscribes here +#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: client writes here // Create the stream client instance NimBLEStreamClient bleStream; @@ -116,7 +118,7 @@ bool connectToServer() { Serial.println("Connected! Discovering services..."); - // Get the service and characteristic + // Get the service NimBLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID); if (!pRemoteService) { Serial.println("Failed to find our service UUID"); @@ -125,19 +127,30 @@ bool connectToServer() { } Serial.println("Found the stream service"); - NimBLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID); - if (!pRemoteCharacteristic) { - Serial.println("Failed to find our characteristic UUID"); + // Get the TX characteristic (server sends notifications here; client subscribes for RX) + NimBLERemoteCharacteristic* pTxChar = pRemoteService->getCharacteristic(TX_CHAR_UUID); + if (!pTxChar) { + Serial.println("Failed to find TX characteristic"); pClient->disconnect(); return false; } - Serial.println("Found the stream characteristic"); + Serial.println("Found the TX characteristic"); + + // Get the RX characteristic (server receives writes here; client writes for TX) + NimBLERemoteCharacteristic* pRxChar = pRemoteService->getCharacteristic(RX_CHAR_UUID); + if (!pRxChar) { + Serial.println("Failed to find RX characteristic"); + pClient->disconnect(); + return false; + } + Serial.println("Found the RX characteristic"); /** - * Initialize the stream client with the remote characteristic - * subscribeNotify=true means we'll receive notifications in the RX buffer + * Initialize the stream client with separate TX and RX characteristics: + * - pRxChar: the characteristic we write to (our TX = server's RX, UUID 6E400002) + * - pTxChar: the characteristic we subscribe to (our RX = server's TX, UUID 6E400003) */ - if (!bleStream.begin(pRemoteCharacteristic, true)) { + if (!bleStream.begin(pRxChar, pTxChar)) { Serial.println("Failed to initialize BLE stream!"); pClient->disconnect(); return false; diff --git a/examples/NimBLE_Stream_Client/README.md b/examples/NimBLE_Stream_Client/README.md index ffd348450..ec38f3b5c 100644 --- a/examples/NimBLE_Stream_Client/README.md +++ b/examples/NimBLE_Stream_Client/README.md @@ -6,17 +6,18 @@ This example demonstrates how to use the `NimBLEStreamClient` class to connect t - Uses Arduino Stream interface (print, println, read, available, etc.) - Automatic server discovery and connection -- Bidirectional communication +- Bidirectional communication using the Nordic UART Service (NUS) - Buffered TX/RX using ring buffers - Automatic reconnection on disconnect +- Compatible with NUS terminal apps and the NimBLE_Stream_Server example - Similar usage to Serial communication ## How it Works -1. Scans for BLE devices advertising the target service UUID -2. Connects to the server and discovers the stream characteristic -3. Initializes `NimBLEStreamClient` with the remote characteristic -4. Subscribes to notifications to receive data in the RX buffer +1. Scans for BLE devices advertising the NUS service UUID +2. Connects to the server and discovers the TX and RX characteristics +3. Initializes `NimBLEStreamClient` with separate TX (write) and RX (subscribe) characteristics +4. Subscribes to the TX characteristic to receive data in the RX buffer 5. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()` ## Usage @@ -30,11 +31,12 @@ This example demonstrates how to use the `NimBLEStreamClient` class to connect t - Begin bidirectional communication 4. You can also type in the Serial monitor to send data to the server -## Service UUIDs +## Service UUIDs (Nordic UART Service) Must match the server: - Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` -- Characteristic: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` +- TX Characteristic (server → client, client subscribes): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` +- RX Characteristic (client → server, client writes): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` ## Serial Monitor Output diff --git a/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino b/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino index 960402053..61a7a20d3 100644 --- a/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino +++ b/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino @@ -7,6 +7,9 @@ * This allows you to use familiar methods like print(), println(), * read(), and available() over BLE, similar to how you would use Serial. * + * Uses the Nordic UART Service (NUS) UUIDs with separate TX and RX characteristics + * for compatibility with NUS terminal apps (e.g. nRF UART, Serial Bluetooth Terminal). + * * Created: November 2025 * Author: NimBLE-Arduino Contributors */ @@ -36,10 +39,11 @@ NimBLEStream::RxOverflowAction onRxOverflow(const uint8_t* data, size_t len, voi return NimBLEStream::DROP_OLDER_DATA; } -// Service and Characteristic UUIDs for the stream -// Using custom UUIDs - you can change these as needed -#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" -#define CHARACTERISTIC_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +// Nordic UART Service (NUS) UUIDs +// Using separate TX and RX characteristics for compatibility with NUS terminal apps. +#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: notify (server → client) +#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: write (client → server) /** Server callbacks to handle connection/disconnection events */ class ServerCallbacks : public NimBLEServerCallbacks { @@ -68,21 +72,23 @@ void setup() { /** * Create the BLE server and set callbacks - * Note: The stream will create its own service and characteristic + * Note: The stream will create its own service and characteristics */ NimBLEServer* pServer = NimBLEDevice::createServer(); pServer->setCallbacks(&serverCallbacks); /** - * Initialize the stream server with: + * Initialize the stream server with NUS UUIDs using separate TX and RX characteristics: * - Service UUID - * - Characteristic UUID + * - TX Characteristic UUID: server sends notifications here (client subscribes) + * - RX Characteristic UUID: client writes here (server receives) * - txBufSize: 1024 bytes for outgoing data (notifications) * - rxBufSize: 1024 bytes for incoming data (writes) * - secure: false (no encryption required - set to true for secure connections) */ if (!bleStream.begin(NimBLEUUID(SERVICE_UUID), - NimBLEUUID(CHARACTERISTIC_UUID), + NimBLEUUID(TX_CHAR_UUID), + NimBLEUUID(RX_CHAR_UUID), 1024, // txBufSize 1024, // rxBufSize false)) // secure diff --git a/examples/NimBLE_Stream_Server/README.md b/examples/NimBLE_Stream_Server/README.md index 90ce5e1e2..d1a0cfb4f 100644 --- a/examples/NimBLE_Stream_Server/README.md +++ b/examples/NimBLE_Stream_Server/README.md @@ -6,14 +6,15 @@ This example demonstrates how to use the `NimBLEStreamServer` class to create a - Uses Arduino Stream interface (print, println, read, available, etc.) - Automatic connection management -- Bidirectional communication +- Bidirectional communication using the Nordic UART Service (NUS) - Buffered TX/RX using ring buffers +- Compatible with NUS terminal apps (nRF UART, Serial Bluetooth Terminal, etc.) - Similar usage to Serial communication ## How it Works -1. Creates a BLE GATT server with a custom service and characteristic -2. Initializes `NimBLEStreamServer` with the characteristic configured for notifications and writes +1. Creates a BLE GATT server with the NUS service and two separate characteristics (TX and RX) +2. Initializes `NimBLEStreamServer` with separate TX (notify) and RX (write) characteristics 3. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()` 4. Automatically handles connection state and MTU negotiation @@ -21,22 +22,22 @@ This example demonstrates how to use the `NimBLEStreamServer` class to create a 1. Upload this sketch to your ESP32 2. The device will advertise as "NimBLE-Stream" -3. Connect with a BLE client (such as the NimBLE_Stream_Client example or a mobile app) +3. Connect with a BLE client (such as the NimBLE_Stream_Client example or a NUS terminal app) 4. Once connected, the server will: - Send periodic messages to the client - Echo back any data received from the client - Display all communication on the Serial monitor -## Service UUIDs +## Service UUIDs (Nordic UART Service) - Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` -- Characteristic: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` - -These are based on the Nordic UART Service (NUS) UUIDs for compatibility with many BLE terminal apps. +- TX Characteristic (server → client, notify): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` +- RX Characteristic (client → server, write): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` ## Compatible With - NimBLE_Stream_Client example - nRF Connect mobile app -- Serial Bluetooth Terminal apps -- Any BLE client that supports characteristic notifications and writes +- nRF UART app +- Serial Bluetooth Terminal app +- Any BLE client that supports the Nordic UART Service (NUS) diff --git a/src/NimBLEStream.cpp b/src/NimBLEStream.cpp index 7ed0c9085..8220654ba 100644 --- a/src/NimBLEStream.cpp +++ b/src/NimBLEStream.cpp @@ -646,6 +646,156 @@ bool NimBLEStreamServer::begin( return false; } +/** + * @brief Initialize the NimBLEStreamServer with separate TX and RX characteristics. + * @param pTxChr Pointer to the TX characteristic used for sending notifications to the client. + * @param pRxChr Pointer to the RX characteristic used for receiving writes from the client. + * @param txBufSize Size of the TX buffer. + * @param rxBufSize Size of the RX buffer. + * @return true if initialization was successful, false otherwise. + * @details This overload supports the NUS (Nordic UART Service) pattern where TX and RX use + * separate characteristics. pTxChr must have the NOTIFY property; pRxChr must have WRITE or WRITE_NR. + */ +bool NimBLEStreamServer::begin(NimBLECharacteristic* pTxChr, + NimBLECharacteristic* pRxChr, + uint32_t txBufSize, + uint32_t rxBufSize) { + if (!NimBLEDevice::isInitialized()) { + return false; + } + + if (m_pChr) { + NIMBLE_LOGW(LOG_TAG, "Already initialized with a characteristic"); + return true; + } + + if (!pTxChr) { + NIMBLE_LOGE(LOG_TAG, "TX characteristic is null"); + return false; + } + + bool canNotify = pTxChr->getProperties() & NIMBLE_PROPERTY::NOTIFY; + if (!canNotify && txBufSize > 0) { + NIMBLE_LOGW(LOG_TAG, "TX characteristic does not support NOTIFY, ignoring TX buffer size"); + } + + m_txBufSize = canNotify ? txBufSize : 0; + + if (pRxChr) { + auto rxProps = pRxChr->getProperties(); + bool canWrite = (rxProps & NIMBLE_PROPERTY::WRITE) || (rxProps & NIMBLE_PROPERTY::WRITE_NR); + if (!canWrite && rxBufSize > 0) { + NIMBLE_LOGW(LOG_TAG, "RX characteristic does not support WRITE, ignoring RX buffer size"); + } + m_rxBufSize = canWrite ? rxBufSize : 0; + } else { + m_rxBufSize = 0; + } + + if (!NimBLEStream::begin()) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream buffers"); + return false; + } + + m_charCallbacks.m_userCallbacks = pTxChr->getCallbacks(); + pTxChr->setCallbacks(&m_charCallbacks); + m_pChr = pTxChr; + + if (pRxChr && m_rxBufSize > 0) { + m_rxCharCallbacks.m_userCallbacks = pRxChr->getCallbacks(); + pRxChr->setCallbacks(&m_rxCharCallbacks); + m_pRxChr = pRxChr; + } + + return true; +} + +/** + * @brief Initialize the NimBLEStreamServer, creating a BLE service with separate TX and RX characteristics. + * @param svcUuid UUID of the BLE service to create. + * @param txChrUuid UUID of the TX characteristic (server sends notifications, e.g. NUS TX: 6E400003). + * @param rxChrUuid UUID of the RX characteristic (client writes, e.g. NUS RX: 6E400002). + * @param txBufSize Size of the TX buffer, set to 0 to disable TX. + * @param rxBufSize Size of the RX buffer, set to 0 to disable RX. + * @param secure Whether the characteristics require encryption. + * @return true if initialization was successful, false otherwise. + * @details This is the recommended overload for NUS (Nordic UART Service) compatibility, where + * the TX and RX data paths use separate characteristics. + */ +bool NimBLEStreamServer::begin(const NimBLEUUID& svcUuid, + const NimBLEUUID& txChrUuid, + const NimBLEUUID& rxChrUuid, + uint32_t txBufSize, + uint32_t rxBufSize, + bool secure) { + if (!NimBLEDevice::isInitialized()) { + NIMBLE_LOGE(LOG_TAG, "NimBLEDevice not initialized"); + return false; + } + + if (m_pChr != nullptr) { + NIMBLE_LOGE(LOG_TAG, "NimBLEStreamServer already initialized"); + return false; + } + + NimBLEServer* pServer = NimBLEDevice::getServer(); + if (!pServer) { + pServer = NimBLEDevice::createServer(); + } + + auto pSvc = pServer->createService(svcUuid); + if (!pSvc) { + return false; + } + + m_deleteSvcOnEnd = true; // mark service for deletion on end since we created it here + + // Create TX characteristic with NOTIFY property + uint32_t txProps = 0; + if (txBufSize > 0) { + txProps |= NIMBLE_PROPERTY::NOTIFY; + if (secure) { + txProps |= NIMBLE_PROPERTY::READ_ENC; + } + } + + auto pTxChr = pSvc->createCharacteristic(txChrUuid, txProps); + if (!pTxChr) { + NIMBLE_LOGE(LOG_TAG, "Failed to create TX characteristic"); + pServer->removeService(pSvc, true); + m_deleteSvcOnEnd = false; + return false; + } + + // Create RX characteristic with WRITE property + NimBLECharacteristic* pRxChr = nullptr; + if (rxBufSize > 0) { + uint32_t rxProps = NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR; + if (secure) { + rxProps |= NIMBLE_PROPERTY::WRITE_ENC; + } + pRxChr = pSvc->createCharacteristic(rxChrUuid, rxProps); + if (!pRxChr) { + NIMBLE_LOGE(LOG_TAG, "Failed to create RX characteristic"); + pServer->removeService(pSvc, true); + m_deleteSvcOnEnd = false; + return false; + } + } + + if (!begin(pTxChr, pRxChr, txBufSize, rxBufSize)) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream with characteristics"); + pServer->removeService(pSvc, true); + m_pChr = nullptr; + m_pRxChr = nullptr; + m_deleteSvcOnEnd = false; + end(); + return false; + } + + return true; +} + /** * @brief Stop the NimBLEStreamServer * @details This will stop the stream and delete the service created if it was created by this class. @@ -662,13 +812,18 @@ void NimBLEStreamServer::end() { } } else { m_pChr->setCallbacks(m_charCallbacks.m_userCallbacks); // restore any user callbacks + if (m_pRxChr) { + m_pRxChr->setCallbacks(m_rxCharCallbacks.m_userCallbacks); // restore any user callbacks + } } } - m_pChr = nullptr; - m_charCallbacks.m_peerHandle = BLE_HS_CONN_HANDLE_NONE; - m_charCallbacks.m_userCallbacks = nullptr; - m_deleteSvcOnEnd = false; + m_pChr = nullptr; + m_pRxChr = nullptr; + m_charCallbacks.m_peerHandle = BLE_HS_CONN_HANDLE_NONE; + m_charCallbacks.m_userCallbacks = nullptr; + m_rxCharCallbacks.m_userCallbacks = nullptr; + m_deleteSvcOnEnd = false; NimBLEStream::end(); } @@ -872,6 +1027,21 @@ void NimBLEStreamServer::ChrCallbacks::onStatus(NimBLECharacteristic* pChr, NimB } } +/** + * @brief Callback for when the separate RX characteristic is written to by a client (two-characteristic mode). + * @param pChr Pointer to the RX characteristic that was written to. + * @param connInfo Information about the connection that performed the write. + * @details This will push the received data into the RX buffer and call any user-defined callbacks. + */ +void NimBLEStreamServer::RxChrCallbacks::onWrite(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo) { + auto val = pChr->getValue(); + m_parent->pushRx(val.data(), val.size()); + + if (m_userCallbacks) { + m_userCallbacks->onWrite(pChr, connInfo); + } +} + # endif // MYNEWT_VAL(BLE_ROLE_PERIPHERAL) # if MYNEWT_VAL(BLE_ROLE_CENTRAL) @@ -934,15 +1104,84 @@ bool NimBLEStreamClient::begin(NimBLERemoteCharacteristic* pChr, bool subscribe, return true; } +/** + * @brief Initialize the NimBLEStreamClient with separate TX and RX characteristics. + * @param pTxChr Pointer to the remote characteristic to write to (e.g. NUS RX characteristic: 6E400002). + * @param pRxChr Pointer to the remote characteristic to subscribe to for notifications (e.g. NUS TX: 6E400003). + * @param txBufSize Size of the TX buffer. + * @param rxBufSize Size of the RX buffer. + * @return true if initialization was successful, false otherwise. + * @details This overload supports the NUS (Nordic UART Service) pattern where TX and RX use + * separate characteristics. pTxChr must support write without response; pRxChr must support notify. + */ +bool NimBLEStreamClient::begin(NimBLERemoteCharacteristic* pTxChr, + NimBLERemoteCharacteristic* pRxChr, + uint32_t txBufSize, + uint32_t rxBufSize) { + if (!NimBLEDevice::isInitialized()) { + NIMBLE_LOGE(LOG_TAG, "NimBLE stack not initialized, call NimBLEDevice::init() first"); + return false; + } + + if (m_pChr) { + NIMBLE_LOGW(LOG_TAG, "Already initialized, must end() first"); + return true; + } + + if (!pTxChr) { + NIMBLE_LOGE(LOG_TAG, "TX remote characteristic is null"); + return false; + } + + if (!pTxChr->canWriteNoResponse()) { + NIMBLE_LOGE(LOG_TAG, "TX characteristic does not support write without response"); + return false; + } + + bool subscribe = false; + if (pRxChr) { + if (!pRxChr->canNotify() && !pRxChr->canIndicate()) { + NIMBLE_LOGW(LOG_TAG, "RX characteristic does not support subscriptions, RX disabled"); + } else { + subscribe = true; + } + } + + m_txBufSize = txBufSize; + m_rxBufSize = subscribe ? rxBufSize : 0; + + if (!NimBLEStream::begin()) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream buffers"); + return false; + } + + if (subscribe) { + using namespace std::placeholders; + if (!pRxChr->subscribe(pRxChr->canNotify(), + std::bind(&NimBLEStreamClient::notifyCallback, this, _1, _2, _3, _4))) { + NIMBLE_LOGE(LOG_TAG, "Failed to subscribe for %s", pRxChr->canNotify() ? "notifications" : "indications"); + end(); + return false; + } + m_pRxChr = pRxChr; + } + + m_pChr = pTxChr; + return true; +} + /** * @brief Clean up the NimBLEStreamClient, unsubscribing from notifications and clearing the remote characteristic reference. */ void NimBLEStreamClient::end() { - if (m_pChr && (m_pChr->canNotify() || m_pChr->canIndicate())) { - m_pChr->unsubscribe(); + // In two-characteristic mode, unsubscribe from the dedicated RX char; otherwise from m_pChr. + NimBLERemoteCharacteristic* notifyChr = m_pRxChr ? m_pRxChr : m_pChr; + if (notifyChr && (notifyChr->canNotify() || notifyChr->canIndicate())) { + notifyChr->unsubscribe(); } - m_pChr = nullptr; + m_pChr = nullptr; + m_pRxChr = nullptr; NimBLEStream::end(); } diff --git a/src/NimBLEStream.h b/src/NimBLEStream.h index 47ccb83d8..c319799a5 100644 --- a/src/NimBLEStream.h +++ b/src/NimBLEStream.h @@ -140,14 +140,20 @@ class NimBLEStream : public Stream { class NimBLEStreamServer : public NimBLEStream { public: - NimBLEStreamServer() : m_charCallbacks(this) {} + NimBLEStreamServer() : m_charCallbacks(this), m_rxCharCallbacks(this) {} ~NimBLEStreamServer() override { end(); } // non-copyable NimBLEStreamServer(const NimBLEStreamServer&) = delete; NimBLEStreamServer& operator=(const NimBLEStreamServer&) = delete; - bool begin(NimBLECharacteristic* chr, uint32_t txBufSize = 1024, uint32_t rxBufSize = 1024); + bool begin(NimBLECharacteristic* pChr, uint32_t txBufSize = 1024, uint32_t rxBufSize = 1024); + + // Two-characteristic overload: separate TX (notify) and RX (write) characteristics, e.g. for NUS compatibility. + bool begin(NimBLECharacteristic* pTxChr, + NimBLECharacteristic* pRxChr, + uint32_t txBufSize = 1024, + uint32_t rxBufSize = 1024); // Convenience overload to create service/characteristic internally; service will be deleted on end() bool begin(const NimBLEUUID& svcUuid, @@ -156,6 +162,14 @@ class NimBLEStreamServer : public NimBLEStream { uint32_t rxBufSize = 1024, bool secure = false); + // Convenience overload with separate TX/RX UUIDs (e.g. for NUS); service will be deleted on end() + bool begin(const NimBLEUUID& svcUuid, + const NimBLEUUID& txChrUuid, + const NimBLEUUID& rxChrUuid, + uint32_t txBufSize = 1024, + uint32_t rxBufSize = 1024, + bool secure = false); + void end() override; size_t write(const uint8_t* data, size_t len) override; uint16_t getPeerHandle() const { return m_charCallbacks.m_peerHandle; } @@ -186,7 +200,17 @@ class NimBLEStreamServer : public NimBLEStream { uint16_t m_peerHandle; } m_charCallbacks; - NimBLECharacteristic* m_pChr{nullptr}; + // Callbacks for a separate RX characteristic in two-characteristic mode (e.g. NUS) + struct RxChrCallbacks : public NimBLECharacteristicCallbacks { + RxChrCallbacks(NimBLEStreamServer* parent) : m_parent(parent), m_userCallbacks(nullptr) {} + void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override; + + NimBLEStreamServer* m_parent; + NimBLECharacteristicCallbacks* m_userCallbacks; + } m_rxCharCallbacks; + + NimBLECharacteristic* m_pChr{nullptr}; // TX characteristic (notify) + NimBLECharacteristic* m_pRxChr{nullptr}; // RX characteristic (write) in two-char mode, nullptr in single-char mode int m_rc{0}; // Whether to delete the BLE service when end() is called; set to false if service is managed externally bool m_deleteSvcOnEnd{false}; @@ -207,10 +231,19 @@ class NimBLEStreamClient : public NimBLEStream { // Attach a discovered remote characteristic; app owns discovery/connection. // Set subscribeNotify=true to receive notifications into RX buffer. - bool begin(NimBLERemoteCharacteristic* pChr, - bool subscribeNotify = false, - uint32_t txBufSize = 1024, - uint32_t rxBufSize = 1024); + bool begin(NimBLERemoteCharacteristic* pChr, + bool subscribeNotify = false, + uint32_t txBufSize = 1024, + uint32_t rxBufSize = 1024); + + // Two-characteristic overload: separate TX (write) and RX (subscribe) characteristics, e.g. for NUS compatibility. + // pTxChr: the characteristic to write to (e.g. NUS RX characteristic). + // pRxChr: the characteristic to subscribe to for notifications (e.g. NUS TX characteristic). + bool begin(NimBLERemoteCharacteristic* pTxChr, + NimBLERemoteCharacteristic* pRxChr, + uint32_t txBufSize = 1024, + uint32_t rxBufSize = 1024); + void end() override; void setNotifyCallback(NimBLERemoteCharacteristic::notify_callback cb) { m_userNotifyCallback = cb; } bool ready() const override; @@ -222,7 +255,8 @@ class NimBLEStreamClient : public NimBLEStream { bool send() override; void notifyCallback(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify); - NimBLERemoteCharacteristic* m_pChr{nullptr}; + NimBLERemoteCharacteristic* m_pChr{nullptr}; // TX characteristic (write) + NimBLERemoteCharacteristic* m_pRxChr{nullptr}; // RX characteristic (subscribe) in two-char mode NimBLERemoteCharacteristic::notify_callback m_userNotifyCallback{nullptr}; }; # endif // BLE_ROLE_CENTRAL From 64c51117c714a671949f55196a61e11568ee024f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:43:05 +0000 Subject: [PATCH 3/3] Rework NUS examples using two stream instances; no library changes Agent-Logs-Url: https://github.com/h2zero/NimBLE-Arduino/sessions/ed6ce1db-51ec-4f31-85b2-0f5bee6bd9c4 Co-authored-by: h2zero <32826625+h2zero@users.noreply.github.com> --- .../NimBLE_Stream_Client.ino | 98 +++---- examples/NimBLE_Stream_Client/README.md | 19 +- .../NimBLE_Stream_Server.ino | 89 +++--- examples/NimBLE_Stream_Server/README.md | 17 +- src/NimBLEStream.cpp | 253 +----------------- src/NimBLEStream.h | 50 +--- 6 files changed, 120 insertions(+), 406 deletions(-) diff --git a/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino b/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino index eb30b17c0..a8556c893 100644 --- a/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino +++ b/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino @@ -5,7 +5,7 @@ * and communicate using the Arduino Stream interface. * * This allows you to use familiar methods like print(), println(), - * read(), and available() over BLE, similar to how you would use Serial. + * and write() over BLE, similar to how you would use Serial. * * This example connects to the NimBLE_Stream_Server example using the Nordic UART * Service (NUS) with separate TX and RX characteristics. @@ -18,39 +18,32 @@ #include // Nordic UART Service (NUS) UUIDs (must match the server) -#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" -#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: client subscribes here -#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: client writes here +#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: client subscribes here +#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: client writes here -// Create the stream client instance +/** + * Stream for sending data to the server. + * Attached to the server's RX characteristic (6E400002, WRITE_NR). + * Data received from the server arrives via the onServerNotify() callback below. + */ NimBLEStreamClient bleStream; -struct RxOverflowStats { - uint32_t droppedOld{0}; - uint32_t droppedNew{0}; -}; - -RxOverflowStats g_rxOverflowStats; uint32_t scanTime = 5000; // Scan duration in milliseconds -NimBLEStream::RxOverflowAction onRxOverflow(const uint8_t* data, size_t len, void* userArg) { - auto* stats = static_cast(userArg); - if (stats) { - stats->droppedOld++; - } - - // For status/telemetry streams, prioritize newest packets. - (void)data; - (void)len; - return NimBLEStream::DROP_OLDER_DATA; -} - // Connection state variables static bool doConnect = false; static bool connected = false; static const NimBLEAdvertisedDevice* pServerDevice = nullptr; static NimBLEClient* pClient = nullptr; +/** Callback invoked when the server sends a notification on its TX characteristic (6E400003) */ +void onServerNotify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify) { + Serial.print("Received from server: "); + Serial.write(pData, len); + Serial.println(); +} + /** Scan callbacks to find the server */ class ScanCallbacks : public NimBLEScanCallbacks { void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { @@ -127,7 +120,16 @@ bool connectToServer() { } Serial.println("Found the stream service"); - // Get the TX characteristic (server sends notifications here; client subscribes for RX) + // Get the server's RX characteristic -- client writes here (our TX path) + NimBLERemoteCharacteristic* pRxChar = pRemoteService->getCharacteristic(RX_CHAR_UUID); + if (!pRxChar) { + Serial.println("Failed to find RX characteristic"); + pClient->disconnect(); + return false; + } + Serial.println("Found the RX characteristic"); + + // Get the server's TX characteristic -- client subscribes here (our RX path) NimBLERemoteCharacteristic* pTxChar = pRemoteService->getCharacteristic(TX_CHAR_UUID); if (!pTxChar) { Serial.println("Failed to find TX characteristic"); @@ -136,28 +138,28 @@ bool connectToServer() { } Serial.println("Found the TX characteristic"); - // Get the RX characteristic (server receives writes here; client writes for TX) - NimBLERemoteCharacteristic* pRxChar = pRemoteService->getCharacteristic(RX_CHAR_UUID); - if (!pRxChar) { - Serial.println("Failed to find RX characteristic"); + /** + * Initialize the stream with the server's RX characteristic for writing (our TX). + * The RX characteristic (6E400002) supports WRITE but not NOTIFY, so subscribeNotify + * must be false. Notifications are handled separately on the TX characteristic below. + */ + if (!bleStream.begin(pRxChar, false)) { + Serial.println("Failed to initialize BLE stream!"); pClient->disconnect(); return false; } - Serial.println("Found the RX characteristic"); /** - * Initialize the stream client with separate TX and RX characteristics: - * - pRxChar: the characteristic we write to (our TX = server's RX, UUID 6E400002) - * - pTxChar: the characteristic we subscribe to (our RX = server's TX, UUID 6E400003) + * Subscribe to the server's TX characteristic (6E400003) to receive notifications. + * Received data is handled directly in the onServerNotify callback. */ - if (!bleStream.begin(pRxChar, pTxChar)) { - Serial.println("Failed to initialize BLE stream!"); + if (!pTxChar->subscribe(true, onServerNotify)) { + Serial.println("Failed to subscribe to server TX characteristic"); + bleStream.end(); pClient->disconnect(); return false; } - bleStream.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats); - Serial.println("BLE Stream initialized successfully!"); connected = true; return true; @@ -184,14 +186,6 @@ void setup() { } void loop() { - static uint32_t lastDroppedOld = 0; - static uint32_t lastDroppedNew = 0; - if (g_rxOverflowStats.droppedOld != lastDroppedOld || g_rxOverflowStats.droppedNew != lastDroppedNew) { - lastDroppedOld = g_rxOverflowStats.droppedOld; - lastDroppedNew = g_rxOverflowStats.droppedNew; - Serial.printf("RX overflow handled (drop-old=%lu, drop-new=%lu)\n", lastDroppedOld, lastDroppedNew); - } - // If we found a server, try to connect if (doConnect) { doConnect = false; @@ -204,20 +198,8 @@ void loop() { } } - // If we're connected, demonstrate the stream interface + // If we're connected, use the stream to send data if (connected && bleStream) { - // Check if we received any data from the server - if (bleStream.available()) { - Serial.print("Received from server: "); - - // Read all available data (just like Serial.read()) - while (bleStream.available()) { - char c = bleStream.read(); - Serial.write(c); - } - Serial.println(); - } - // Send a message every 5 seconds using Stream methods static unsigned long lastSend = 0; if (millis() - lastSend > 5000) { @@ -231,7 +213,7 @@ void loop() { Serial.println("Sent data to server via BLE stream"); } - // You can also read from Serial and send over BLE + // Read from Serial and send over BLE if (Serial.available()) { Serial.println("Reading from Serial and sending via BLE:"); while (Serial.available()) { diff --git a/examples/NimBLE_Stream_Client/README.md b/examples/NimBLE_Stream_Client/README.md index ec38f3b5c..da3d49ae2 100644 --- a/examples/NimBLE_Stream_Client/README.md +++ b/examples/NimBLE_Stream_Client/README.md @@ -4,21 +4,22 @@ This example demonstrates how to use the `NimBLEStreamClient` class to connect t ## Features -- Uses Arduino Stream interface (print, println, read, available, etc.) +- Uses Arduino Stream interface (print, println, write, etc.) - Automatic server discovery and connection - Bidirectional communication using the Nordic UART Service (NUS) -- Buffered TX/RX using ring buffers +- TX: `NimBLEStreamClient` writes to the server's RX characteristic (6E400002) +- RX: direct notification callback subscribed to the server's TX characteristic (6E400003) - Automatic reconnection on disconnect -- Compatible with NUS terminal apps and the NimBLE_Stream_Server example +- Compatible with the NimBLE_Stream_Server example and NUS terminal apps - Similar usage to Serial communication ## How it Works 1. Scans for BLE devices advertising the NUS service UUID 2. Connects to the server and discovers the TX and RX characteristics -3. Initializes `NimBLEStreamClient` with separate TX (write) and RX (subscribe) characteristics -4. Subscribes to the TX characteristic to receive data in the RX buffer -5. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()` +3. Initializes `NimBLEStreamClient` with the server's RX characteristic for writing (our TX path) +4. Subscribes directly to the server's TX characteristic to receive notifications (our RX path) +5. Uses familiar Stream methods like `print()`, `println()`, and `write()` to send data ## Usage @@ -43,7 +44,7 @@ Must match the server: The example displays: - Server discovery progress - Connection status -- All data received from the server +- All data received from the server (via notification callback) - Confirmation of data sent to the server ## Testing @@ -51,5 +52,5 @@ The example displays: Run with NimBLE_Stream_Server to see bidirectional communication: - Server sends periodic status messages - Client sends periodic uptime messages -- Both echo data received from each other -- You can send data from either Serial monitor +- Server echoes data back to the client +- You can send data from the Serial monitor diff --git a/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino b/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino index 61a7a20d3..2146f315a 100644 --- a/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino +++ b/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino @@ -17,12 +17,26 @@ #include #include -// Create the stream server instance -NimBLEStreamServer bleStream; +// Nordic UART Service (NUS) UUIDs +#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: notify (server -> client) +#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: write (client -> server) + +/** + * Two stream server instances: + * - bleStreamTx sends notifications to the client (server -> client). + * Backed by the TX characteristic (NOTIFY-only); TX buffer is enabled, RX buffer disabled. + * - bleStreamRx receives data written by the client (client -> server). + * Backed by the RX characteristic (WRITE-only); RX buffer is enabled, TX buffer disabled. + * + * NimBLEStreamServer::begin(pChr) automatically enables only the directions supported + * by the characteristic's properties, so no special configuration is needed. + */ +NimBLEStreamServer bleStreamTx; +NimBLEStreamServer bleStreamRx; struct RxOverflowStats { uint32_t droppedOld{0}; - uint32_t droppedNew{0}; }; RxOverflowStats g_rxOverflowStats; @@ -39,12 +53,6 @@ NimBLEStream::RxOverflowAction onRxOverflow(const uint8_t* data, size_t len, voi return NimBLEStream::DROP_OLDER_DATA; } -// Nordic UART Service (NUS) UUIDs -// Using separate TX and RX characteristics for compatibility with NUS terminal apps. -#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" -#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: notify (server → client) -#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: write (client → server) - /** Server callbacks to handle connection/disconnection events */ class ServerCallbacks : public NimBLEServerCallbacks { void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override { @@ -70,34 +78,31 @@ void setup() { /** Initialize NimBLE and set the device name */ NimBLEDevice::init("NimBLE-Stream"); - /** - * Create the BLE server and set callbacks - * Note: The stream will create its own service and characteristics - */ NimBLEServer* pServer = NimBLEDevice::createServer(); pServer->setCallbacks(&serverCallbacks); /** - * Initialize the stream server with NUS UUIDs using separate TX and RX characteristics: - * - Service UUID - * - TX Characteristic UUID: server sends notifications here (client subscribes) - * - RX Characteristic UUID: client writes here (server receives) - * - txBufSize: 1024 bytes for outgoing data (notifications) - * - rxBufSize: 1024 bytes for incoming data (writes) - * - secure: false (no encryption required - set to true for secure connections) + * Create the NUS service with two characteristics: + * - TX (6E400003): NOTIFY -- server sends data to the client + * - RX (6E400002): WRITE -- client sends data to the server + */ + NimBLEService* pSvc = pServer->createService(SERVICE_UUID); + NimBLECharacteristic* pTxChar = pSvc->createCharacteristic(TX_CHAR_UUID, NIMBLE_PROPERTY::NOTIFY); + NimBLECharacteristic* pRxChar = pSvc->createCharacteristic(RX_CHAR_UUID, + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); + + /** + * Pass each characteristic to its own NimBLEStreamServer instance. + * begin() checks the characteristic properties and enables only the supported + * direction: pTxChar (NOTIFY-only) enables the TX buffer; pRxChar (WRITE-only) + * enables the RX buffer. */ - if (!bleStream.begin(NimBLEUUID(SERVICE_UUID), - NimBLEUUID(TX_CHAR_UUID), - NimBLEUUID(RX_CHAR_UUID), - 1024, // txBufSize - 1024, // rxBufSize - false)) // secure - { + if (!bleStreamTx.begin(pTxChar) || !bleStreamRx.begin(pRxChar)) { Serial.println("Failed to initialize BLE stream!"); return; } - bleStream.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats); + bleStreamRx.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats); /** * Create advertising instance and add service UUID @@ -116,39 +121,37 @@ void setup() { void loop() { static uint32_t lastDroppedOld = 0; - static uint32_t lastDroppedNew = 0; - if (g_rxOverflowStats.droppedOld != lastDroppedOld || g_rxOverflowStats.droppedNew != lastDroppedNew) { + if (g_rxOverflowStats.droppedOld != lastDroppedOld) { lastDroppedOld = g_rxOverflowStats.droppedOld; - lastDroppedNew = g_rxOverflowStats.droppedNew; - Serial.printf("RX overflow handled (drop-old=%lu, drop-new=%lu)\n", lastDroppedOld, lastDroppedNew); + Serial.printf("RX overflow: %lu packets dropped\n", lastDroppedOld); } - // Check if a client is subscribed (connected and listening) - if (bleStream.ready()) { + // bleStreamTx.ready() is true when a client has subscribed to the TX characteristic + if (bleStreamTx.ready()) { // Send a message every 2 seconds using Stream methods static unsigned long lastSend = 0; if (millis() - lastSend > 2000) { lastSend = millis(); // Using familiar Serial-like methods! - bleStream.print("Hello from BLE Server! Time: "); - bleStream.println(millis()); + bleStreamTx.print("Hello from BLE Server! Time: "); + bleStreamTx.println(millis()); // You can also use printf - bleStream.printf("Free heap: %d bytes\n", ESP.getFreeHeap()); + bleStreamTx.printf("Free heap: %d bytes\n", ESP.getFreeHeap()); Serial.println("Sent data to client via BLE stream"); } - // Check if we received any data from the client - if (bleStream.available()) { + // Check if we received any data written by the client on the RX characteristic + if (bleStreamRx.available()) { Serial.print("Received from client: "); // Read all available data (just like Serial.read()) - while (bleStream.available()) { - char c = bleStream.read(); - Serial.write(c); // Echo to Serial - bleStream.write(c); // Echo back to BLE client + while (bleStreamRx.available()) { + char c = bleStreamRx.read(); + Serial.write(c); // Echo to Serial + bleStreamTx.write(c); // Echo back to BLE client via TX notification } Serial.println(); } diff --git a/examples/NimBLE_Stream_Server/README.md b/examples/NimBLE_Stream_Server/README.md index d1a0cfb4f..f0449e96b 100644 --- a/examples/NimBLE_Stream_Server/README.md +++ b/examples/NimBLE_Stream_Server/README.md @@ -13,10 +13,11 @@ This example demonstrates how to use the `NimBLEStreamServer` class to create a ## How it Works -1. Creates a BLE GATT server with the NUS service and two separate characteristics (TX and RX) -2. Initializes `NimBLEStreamServer` with separate TX (notify) and RX (write) characteristics -3. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()` -4. Automatically handles connection state and MTU negotiation +1. Creates the NUS service with two characteristics (TX and RX) +2. Initializes two `NimBLEStreamServer` instances — one for TX (notifications) and one for RX (writes) +3. `NimBLEStreamServer::begin()` automatically enables only the direction supported by the characteristic's properties: the TX characteristic (NOTIFY-only) enables the TX buffer; the RX characteristic (WRITE-only) enables the RX buffer +4. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()` +5. Automatically handles connection state and MTU negotiation ## Usage @@ -24,15 +25,15 @@ This example demonstrates how to use the `NimBLEStreamServer` class to create a 2. The device will advertise as "NimBLE-Stream" 3. Connect with a BLE client (such as the NimBLE_Stream_Client example or a NUS terminal app) 4. Once connected, the server will: - - Send periodic messages to the client - - Echo back any data received from the client + - Send periodic messages to the client via the TX characteristic + - Echo back any data received from the client on the RX characteristic - Display all communication on the Serial monitor ## Service UUIDs (Nordic UART Service) - Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` -- TX Characteristic (server → client, notify): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` -- RX Characteristic (client → server, write): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` +- TX Characteristic (server notifies → client subscribes): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` +- RX Characteristic (client writes → server receives): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` ## Compatible With diff --git a/src/NimBLEStream.cpp b/src/NimBLEStream.cpp index 8220654ba..7ed0c9085 100644 --- a/src/NimBLEStream.cpp +++ b/src/NimBLEStream.cpp @@ -646,156 +646,6 @@ bool NimBLEStreamServer::begin( return false; } -/** - * @brief Initialize the NimBLEStreamServer with separate TX and RX characteristics. - * @param pTxChr Pointer to the TX characteristic used for sending notifications to the client. - * @param pRxChr Pointer to the RX characteristic used for receiving writes from the client. - * @param txBufSize Size of the TX buffer. - * @param rxBufSize Size of the RX buffer. - * @return true if initialization was successful, false otherwise. - * @details This overload supports the NUS (Nordic UART Service) pattern where TX and RX use - * separate characteristics. pTxChr must have the NOTIFY property; pRxChr must have WRITE or WRITE_NR. - */ -bool NimBLEStreamServer::begin(NimBLECharacteristic* pTxChr, - NimBLECharacteristic* pRxChr, - uint32_t txBufSize, - uint32_t rxBufSize) { - if (!NimBLEDevice::isInitialized()) { - return false; - } - - if (m_pChr) { - NIMBLE_LOGW(LOG_TAG, "Already initialized with a characteristic"); - return true; - } - - if (!pTxChr) { - NIMBLE_LOGE(LOG_TAG, "TX characteristic is null"); - return false; - } - - bool canNotify = pTxChr->getProperties() & NIMBLE_PROPERTY::NOTIFY; - if (!canNotify && txBufSize > 0) { - NIMBLE_LOGW(LOG_TAG, "TX characteristic does not support NOTIFY, ignoring TX buffer size"); - } - - m_txBufSize = canNotify ? txBufSize : 0; - - if (pRxChr) { - auto rxProps = pRxChr->getProperties(); - bool canWrite = (rxProps & NIMBLE_PROPERTY::WRITE) || (rxProps & NIMBLE_PROPERTY::WRITE_NR); - if (!canWrite && rxBufSize > 0) { - NIMBLE_LOGW(LOG_TAG, "RX characteristic does not support WRITE, ignoring RX buffer size"); - } - m_rxBufSize = canWrite ? rxBufSize : 0; - } else { - m_rxBufSize = 0; - } - - if (!NimBLEStream::begin()) { - NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream buffers"); - return false; - } - - m_charCallbacks.m_userCallbacks = pTxChr->getCallbacks(); - pTxChr->setCallbacks(&m_charCallbacks); - m_pChr = pTxChr; - - if (pRxChr && m_rxBufSize > 0) { - m_rxCharCallbacks.m_userCallbacks = pRxChr->getCallbacks(); - pRxChr->setCallbacks(&m_rxCharCallbacks); - m_pRxChr = pRxChr; - } - - return true; -} - -/** - * @brief Initialize the NimBLEStreamServer, creating a BLE service with separate TX and RX characteristics. - * @param svcUuid UUID of the BLE service to create. - * @param txChrUuid UUID of the TX characteristic (server sends notifications, e.g. NUS TX: 6E400003). - * @param rxChrUuid UUID of the RX characteristic (client writes, e.g. NUS RX: 6E400002). - * @param txBufSize Size of the TX buffer, set to 0 to disable TX. - * @param rxBufSize Size of the RX buffer, set to 0 to disable RX. - * @param secure Whether the characteristics require encryption. - * @return true if initialization was successful, false otherwise. - * @details This is the recommended overload for NUS (Nordic UART Service) compatibility, where - * the TX and RX data paths use separate characteristics. - */ -bool NimBLEStreamServer::begin(const NimBLEUUID& svcUuid, - const NimBLEUUID& txChrUuid, - const NimBLEUUID& rxChrUuid, - uint32_t txBufSize, - uint32_t rxBufSize, - bool secure) { - if (!NimBLEDevice::isInitialized()) { - NIMBLE_LOGE(LOG_TAG, "NimBLEDevice not initialized"); - return false; - } - - if (m_pChr != nullptr) { - NIMBLE_LOGE(LOG_TAG, "NimBLEStreamServer already initialized"); - return false; - } - - NimBLEServer* pServer = NimBLEDevice::getServer(); - if (!pServer) { - pServer = NimBLEDevice::createServer(); - } - - auto pSvc = pServer->createService(svcUuid); - if (!pSvc) { - return false; - } - - m_deleteSvcOnEnd = true; // mark service for deletion on end since we created it here - - // Create TX characteristic with NOTIFY property - uint32_t txProps = 0; - if (txBufSize > 0) { - txProps |= NIMBLE_PROPERTY::NOTIFY; - if (secure) { - txProps |= NIMBLE_PROPERTY::READ_ENC; - } - } - - auto pTxChr = pSvc->createCharacteristic(txChrUuid, txProps); - if (!pTxChr) { - NIMBLE_LOGE(LOG_TAG, "Failed to create TX characteristic"); - pServer->removeService(pSvc, true); - m_deleteSvcOnEnd = false; - return false; - } - - // Create RX characteristic with WRITE property - NimBLECharacteristic* pRxChr = nullptr; - if (rxBufSize > 0) { - uint32_t rxProps = NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR; - if (secure) { - rxProps |= NIMBLE_PROPERTY::WRITE_ENC; - } - pRxChr = pSvc->createCharacteristic(rxChrUuid, rxProps); - if (!pRxChr) { - NIMBLE_LOGE(LOG_TAG, "Failed to create RX characteristic"); - pServer->removeService(pSvc, true); - m_deleteSvcOnEnd = false; - return false; - } - } - - if (!begin(pTxChr, pRxChr, txBufSize, rxBufSize)) { - NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream with characteristics"); - pServer->removeService(pSvc, true); - m_pChr = nullptr; - m_pRxChr = nullptr; - m_deleteSvcOnEnd = false; - end(); - return false; - } - - return true; -} - /** * @brief Stop the NimBLEStreamServer * @details This will stop the stream and delete the service created if it was created by this class. @@ -812,18 +662,13 @@ void NimBLEStreamServer::end() { } } else { m_pChr->setCallbacks(m_charCallbacks.m_userCallbacks); // restore any user callbacks - if (m_pRxChr) { - m_pRxChr->setCallbacks(m_rxCharCallbacks.m_userCallbacks); // restore any user callbacks - } } } - m_pChr = nullptr; - m_pRxChr = nullptr; - m_charCallbacks.m_peerHandle = BLE_HS_CONN_HANDLE_NONE; - m_charCallbacks.m_userCallbacks = nullptr; - m_rxCharCallbacks.m_userCallbacks = nullptr; - m_deleteSvcOnEnd = false; + m_pChr = nullptr; + m_charCallbacks.m_peerHandle = BLE_HS_CONN_HANDLE_NONE; + m_charCallbacks.m_userCallbacks = nullptr; + m_deleteSvcOnEnd = false; NimBLEStream::end(); } @@ -1027,21 +872,6 @@ void NimBLEStreamServer::ChrCallbacks::onStatus(NimBLECharacteristic* pChr, NimB } } -/** - * @brief Callback for when the separate RX characteristic is written to by a client (two-characteristic mode). - * @param pChr Pointer to the RX characteristic that was written to. - * @param connInfo Information about the connection that performed the write. - * @details This will push the received data into the RX buffer and call any user-defined callbacks. - */ -void NimBLEStreamServer::RxChrCallbacks::onWrite(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo) { - auto val = pChr->getValue(); - m_parent->pushRx(val.data(), val.size()); - - if (m_userCallbacks) { - m_userCallbacks->onWrite(pChr, connInfo); - } -} - # endif // MYNEWT_VAL(BLE_ROLE_PERIPHERAL) # if MYNEWT_VAL(BLE_ROLE_CENTRAL) @@ -1104,84 +934,15 @@ bool NimBLEStreamClient::begin(NimBLERemoteCharacteristic* pChr, bool subscribe, return true; } -/** - * @brief Initialize the NimBLEStreamClient with separate TX and RX characteristics. - * @param pTxChr Pointer to the remote characteristic to write to (e.g. NUS RX characteristic: 6E400002). - * @param pRxChr Pointer to the remote characteristic to subscribe to for notifications (e.g. NUS TX: 6E400003). - * @param txBufSize Size of the TX buffer. - * @param rxBufSize Size of the RX buffer. - * @return true if initialization was successful, false otherwise. - * @details This overload supports the NUS (Nordic UART Service) pattern where TX and RX use - * separate characteristics. pTxChr must support write without response; pRxChr must support notify. - */ -bool NimBLEStreamClient::begin(NimBLERemoteCharacteristic* pTxChr, - NimBLERemoteCharacteristic* pRxChr, - uint32_t txBufSize, - uint32_t rxBufSize) { - if (!NimBLEDevice::isInitialized()) { - NIMBLE_LOGE(LOG_TAG, "NimBLE stack not initialized, call NimBLEDevice::init() first"); - return false; - } - - if (m_pChr) { - NIMBLE_LOGW(LOG_TAG, "Already initialized, must end() first"); - return true; - } - - if (!pTxChr) { - NIMBLE_LOGE(LOG_TAG, "TX remote characteristic is null"); - return false; - } - - if (!pTxChr->canWriteNoResponse()) { - NIMBLE_LOGE(LOG_TAG, "TX characteristic does not support write without response"); - return false; - } - - bool subscribe = false; - if (pRxChr) { - if (!pRxChr->canNotify() && !pRxChr->canIndicate()) { - NIMBLE_LOGW(LOG_TAG, "RX characteristic does not support subscriptions, RX disabled"); - } else { - subscribe = true; - } - } - - m_txBufSize = txBufSize; - m_rxBufSize = subscribe ? rxBufSize : 0; - - if (!NimBLEStream::begin()) { - NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream buffers"); - return false; - } - - if (subscribe) { - using namespace std::placeholders; - if (!pRxChr->subscribe(pRxChr->canNotify(), - std::bind(&NimBLEStreamClient::notifyCallback, this, _1, _2, _3, _4))) { - NIMBLE_LOGE(LOG_TAG, "Failed to subscribe for %s", pRxChr->canNotify() ? "notifications" : "indications"); - end(); - return false; - } - m_pRxChr = pRxChr; - } - - m_pChr = pTxChr; - return true; -} - /** * @brief Clean up the NimBLEStreamClient, unsubscribing from notifications and clearing the remote characteristic reference. */ void NimBLEStreamClient::end() { - // In two-characteristic mode, unsubscribe from the dedicated RX char; otherwise from m_pChr. - NimBLERemoteCharacteristic* notifyChr = m_pRxChr ? m_pRxChr : m_pChr; - if (notifyChr && (notifyChr->canNotify() || notifyChr->canIndicate())) { - notifyChr->unsubscribe(); + if (m_pChr && (m_pChr->canNotify() || m_pChr->canIndicate())) { + m_pChr->unsubscribe(); } - m_pChr = nullptr; - m_pRxChr = nullptr; + m_pChr = nullptr; NimBLEStream::end(); } diff --git a/src/NimBLEStream.h b/src/NimBLEStream.h index c319799a5..47ccb83d8 100644 --- a/src/NimBLEStream.h +++ b/src/NimBLEStream.h @@ -140,20 +140,14 @@ class NimBLEStream : public Stream { class NimBLEStreamServer : public NimBLEStream { public: - NimBLEStreamServer() : m_charCallbacks(this), m_rxCharCallbacks(this) {} + NimBLEStreamServer() : m_charCallbacks(this) {} ~NimBLEStreamServer() override { end(); } // non-copyable NimBLEStreamServer(const NimBLEStreamServer&) = delete; NimBLEStreamServer& operator=(const NimBLEStreamServer&) = delete; - bool begin(NimBLECharacteristic* pChr, uint32_t txBufSize = 1024, uint32_t rxBufSize = 1024); - - // Two-characteristic overload: separate TX (notify) and RX (write) characteristics, e.g. for NUS compatibility. - bool begin(NimBLECharacteristic* pTxChr, - NimBLECharacteristic* pRxChr, - uint32_t txBufSize = 1024, - uint32_t rxBufSize = 1024); + bool begin(NimBLECharacteristic* chr, uint32_t txBufSize = 1024, uint32_t rxBufSize = 1024); // Convenience overload to create service/characteristic internally; service will be deleted on end() bool begin(const NimBLEUUID& svcUuid, @@ -162,14 +156,6 @@ class NimBLEStreamServer : public NimBLEStream { uint32_t rxBufSize = 1024, bool secure = false); - // Convenience overload with separate TX/RX UUIDs (e.g. for NUS); service will be deleted on end() - bool begin(const NimBLEUUID& svcUuid, - const NimBLEUUID& txChrUuid, - const NimBLEUUID& rxChrUuid, - uint32_t txBufSize = 1024, - uint32_t rxBufSize = 1024, - bool secure = false); - void end() override; size_t write(const uint8_t* data, size_t len) override; uint16_t getPeerHandle() const { return m_charCallbacks.m_peerHandle; } @@ -200,17 +186,7 @@ class NimBLEStreamServer : public NimBLEStream { uint16_t m_peerHandle; } m_charCallbacks; - // Callbacks for a separate RX characteristic in two-characteristic mode (e.g. NUS) - struct RxChrCallbacks : public NimBLECharacteristicCallbacks { - RxChrCallbacks(NimBLEStreamServer* parent) : m_parent(parent), m_userCallbacks(nullptr) {} - void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override; - - NimBLEStreamServer* m_parent; - NimBLECharacteristicCallbacks* m_userCallbacks; - } m_rxCharCallbacks; - - NimBLECharacteristic* m_pChr{nullptr}; // TX characteristic (notify) - NimBLECharacteristic* m_pRxChr{nullptr}; // RX characteristic (write) in two-char mode, nullptr in single-char mode + NimBLECharacteristic* m_pChr{nullptr}; int m_rc{0}; // Whether to delete the BLE service when end() is called; set to false if service is managed externally bool m_deleteSvcOnEnd{false}; @@ -231,19 +207,10 @@ class NimBLEStreamClient : public NimBLEStream { // Attach a discovered remote characteristic; app owns discovery/connection. // Set subscribeNotify=true to receive notifications into RX buffer. - bool begin(NimBLERemoteCharacteristic* pChr, - bool subscribeNotify = false, - uint32_t txBufSize = 1024, - uint32_t rxBufSize = 1024); - - // Two-characteristic overload: separate TX (write) and RX (subscribe) characteristics, e.g. for NUS compatibility. - // pTxChr: the characteristic to write to (e.g. NUS RX characteristic). - // pRxChr: the characteristic to subscribe to for notifications (e.g. NUS TX characteristic). - bool begin(NimBLERemoteCharacteristic* pTxChr, - NimBLERemoteCharacteristic* pRxChr, - uint32_t txBufSize = 1024, - uint32_t rxBufSize = 1024); - + bool begin(NimBLERemoteCharacteristic* pChr, + bool subscribeNotify = false, + uint32_t txBufSize = 1024, + uint32_t rxBufSize = 1024); void end() override; void setNotifyCallback(NimBLERemoteCharacteristic::notify_callback cb) { m_userNotifyCallback = cb; } bool ready() const override; @@ -255,8 +222,7 @@ class NimBLEStreamClient : public NimBLEStream { bool send() override; void notifyCallback(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify); - NimBLERemoteCharacteristic* m_pChr{nullptr}; // TX characteristic (write) - NimBLERemoteCharacteristic* m_pRxChr{nullptr}; // RX characteristic (subscribe) in two-char mode + NimBLERemoteCharacteristic* m_pChr{nullptr}; NimBLERemoteCharacteristic::notify_callback m_userNotifyCallback{nullptr}; }; # endif // BLE_ROLE_CENTRAL