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/2] 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/2] 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