diff --git a/.gitignore b/.gitignore index b37ecce25..89963ebe6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ docs/doxydocs .development +_codeql_detected_source_root diff --git a/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino b/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino new file mode 100644 index 000000000..cefc1a47e --- /dev/null +++ b/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino @@ -0,0 +1,206 @@ +/** + * NimBLE_Stream_Client Example: + * + * Demonstrates using NimBLEStreamClient to connect to a BLE GATT server + * 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. + * + * This example connects to the NimBLE_Stream_Server example. + * + * Created: November 2025 + * Author: NimBLE-Arduino Contributors + */ + +#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" + +// Create the stream client instance +NimBLEStreamClient bleStream; + +// Connection state variables +static bool doConnect = false; +static bool connected = false; +static const NimBLEAdvertisedDevice* pServerDevice = nullptr; +static NimBLEClient* pClient = nullptr; + +/** Scan callbacks to find the server */ +class ScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { + Serial.printf("Advertised Device: %s\n", advertisedDevice->toString().c_str()); + + // Check if this device advertises our service + if (advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) { + Serial.println("Found our stream server!"); + pServerDevice = advertisedDevice; + NimBLEDevice::getScan()->stop(); + doConnect = true; + } + } + + void onScanEnd(const NimBLEScanResults& results, int reason) override { + Serial.println("Scan ended"); + if (!doConnect && !connected) { + Serial.println("Server not found, restarting scan..."); + NimBLEDevice::getScan()->start(5, false, true); + } + } +} scanCallbacks; + +/** Client callbacks for connection/disconnection events */ +class ClientCallbacks : public NimBLEClientCallbacks { + void onConnect(NimBLEClient* pClient) override { + Serial.println("Connected to server"); + // Update connection parameters for better throughput + pClient->updateConnParams(12, 24, 0, 200); + } + + void onDisconnect(NimBLEClient* pClient, int reason) override { + Serial.printf("Disconnected from server, reason: %d\n", reason); + connected = false; + bleStream.end(); + + // Restart scanning + Serial.println("Restarting scan..."); + NimBLEDevice::getScan()->start(5, false, true); + } +} clientCallbacks; + +/** Connect to the BLE Server and set up the stream */ +bool connectToServer() { + Serial.printf("Connecting to: %s\n", pServerDevice->getAddress().toString().c_str()); + + // Create or reuse a client + pClient = NimBLEDevice::getClientByPeerAddress(pServerDevice->getAddress()); + if (!pClient) { + pClient = NimBLEDevice::createClient(); + if (!pClient) { + Serial.println("Failed to create client"); + return false; + } + pClient->setClientCallbacks(&clientCallbacks, false); + pClient->setConnectionParams(12, 24, 0, 200); + pClient->setConnectTimeout(5000); + } + + // Connect to the remote BLE Server + if (!pClient->connect(pServerDevice)) { + Serial.println("Failed to connect to server"); + return false; + } + + Serial.println("Connected! Discovering services..."); + + // Get the service and characteristic + NimBLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID); + if (!pRemoteService) { + Serial.println("Failed to find our service UUID"); + pClient->disconnect(); + return false; + } + Serial.println("Found the stream service"); + + NimBLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID); + if (!pRemoteCharacteristic) { + Serial.println("Failed to find our characteristic UUID"); + pClient->disconnect(); + return false; + } + Serial.println("Found the stream characteristic"); + + /** + * Initialize the stream client with the remote characteristic + * subscribeNotify=true means we'll receive notifications in the RX buffer + */ + if (!bleStream.begin(pRemoteCharacteristic, true)) { + Serial.println("Failed to initialize BLE stream!"); + pClient->disconnect(); + return false; + } + + Serial.println("BLE Stream initialized successfully!"); + connected = true; + return true; +} + +void setup() { + Serial.begin(115200); + Serial.println("Starting NimBLE Stream Client"); + + /** Initialize NimBLE */ + NimBLEDevice::init("NimBLE-StreamClient"); + + /** + * Create the BLE scan instance and set callbacks + * Configure scan parameters + */ + NimBLEScan* pScan = NimBLEDevice::getScan(); + pScan->setScanCallbacks(&scanCallbacks, false); + pScan->setInterval(100); + pScan->setWindow(99); + pScan->setActiveScan(true); + + /** Start scanning for the server */ + Serial.println("Scanning for BLE Stream Server..."); + pScan->start(5, false, true); +} + +void loop() { + // If we found a server, try to connect + if (doConnect) { + doConnect = false; + if (connectToServer()) { + Serial.println("Stream ready for communication!"); + } else { + Serial.println("Failed to connect to server, restarting scan..."); + pServerDevice = nullptr; + NimBLEDevice::getScan()->start(5, false, true); + } + } + + // If we're connected, demonstrate the stream interface + 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) { + lastSend = millis(); + + // Using familiar Serial-like methods! + bleStream.print("Hello from client! Uptime: "); + bleStream.print(millis() / 1000); + bleStream.println(" seconds"); + + Serial.println("Sent data to server via BLE stream"); + } + + // You can also read from Serial and send over BLE + if (Serial.available()) { + Serial.println("Reading from Serial and sending via BLE:"); + while (Serial.available()) { + char c = Serial.read(); + Serial.write(c); // Echo locally + bleStream.write(c); // Send via BLE + } + Serial.println(); + } + } + + delay(10); +} diff --git a/examples/NimBLE_Stream_Client/README.md b/examples/NimBLE_Stream_Client/README.md new file mode 100644 index 000000000..ffd348450 --- /dev/null +++ b/examples/NimBLE_Stream_Client/README.md @@ -0,0 +1,53 @@ +# NimBLE Stream Client Example + +This example demonstrates how to use the `NimBLEStreamClient` class to connect to a BLE GATT server and communicate using the familiar Arduino Stream interface. + +## Features + +- Uses Arduino Stream interface (print, println, read, available, etc.) +- Automatic server discovery and connection +- Bidirectional communication +- Buffered TX/RX using ring buffers +- Automatic reconnection on disconnect +- 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 +5. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()` + +## Usage + +1. First, upload the NimBLE_Stream_Server example to one ESP32 +2. Upload this client sketch to another ESP32 +3. The client will automatically: + - Scan for the server + - Connect when found + - Set up the stream interface + - Begin bidirectional communication +4. You can also type in the Serial monitor to send data to the server + +## Service UUIDs + +Must match the server: +- Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` +- Characteristic: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` + +## Serial Monitor Output + +The example displays: +- Server discovery progress +- Connection status +- All data received from the server +- Confirmation of data sent to the server + +## Testing + +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 diff --git a/examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino b/examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino new file mode 100644 index 000000000..395a4d5a3 --- /dev/null +++ b/examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino @@ -0,0 +1,62 @@ +/** + * NimBLE_Stream_Echo Example: + * + * A minimal example demonstrating NimBLEStreamServer. + * Echoes back any data received from BLE clients. + * + * This is the simplest way to use the NimBLE Stream interface, + * showing just the essential setup and read/write operations. + * + * Created: November 2025 + * Author: NimBLE-Arduino Contributors + */ + +#include +#include + +NimBLEStreamServer bleStream; + +void setup() { + Serial.begin(115200); + Serial.println("NimBLE Stream Echo Server"); + + // Initialize BLE + NimBLEDevice::init("BLE-Echo"); + auto pServer = NimBLEDevice::createServer(); + pServer->advertiseOnDisconnect(true); // Keep advertising after clients disconnect + + /** + * Initialize the stream server with: + * - Service UUID + * - Characteristic UUID + * - 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(uint16_t(0xc0de)), // Service UUID + NimBLEUUID(uint16_t(0xfeed)), // Characteristic UUID + 1024, // txBufSize + 1024, // rxBufSize + false)) { // secure + Serial.println("Failed to initialize BLE stream"); + return; + } + + // Start advertising + NimBLEDevice::getAdvertising()->start(); + Serial.println("Ready! Connect with a BLE client and send data."); +} + +void loop() { + // Echo any received data back to the client + if (bleStream.ready() && bleStream.available()) { + Serial.print("Echo: "); + while (bleStream.available()) { + char c = bleStream.read(); + Serial.write(c); + bleStream.write(c); // Echo back + } + Serial.println(); + } + delay(10); +} diff --git a/examples/NimBLE_Stream_Echo/README.md b/examples/NimBLE_Stream_Echo/README.md new file mode 100644 index 000000000..4a80224d0 --- /dev/null +++ b/examples/NimBLE_Stream_Echo/README.md @@ -0,0 +1,39 @@ +# NimBLE Stream Echo Example + +This is the simplest example demonstrating `NimBLEStreamServer`. It echoes back any data received from BLE clients. + +## Features + +- Minimal code showing essential NimBLE Stream usage +- Echoes all received data back to the client +- Uses default service and characteristic UUIDs +- Perfect starting point for learning the Stream interface + +## How it Works + +1. Initializes BLE with minimal configuration +2. Creates a stream server with default UUIDs +3. Waits for client connection and data +4. Echoes received data back to the client +5. Displays received data on Serial monitor + +## Default UUIDs + +- Service: `0xc0de` +- Characteristic: `0xfeed` + +## Usage + +1. Upload this sketch to your ESP32 +2. Connect with a BLE client app (nRF Connect, Serial Bluetooth Terminal, etc.) +3. Find the service `0xc0de` and characteristic `0xfeed` +4. Subscribe to notifications +5. Write data to the characteristic +6. The data will be echoed back and displayed in Serial monitor + +## Good For + +- Learning the basic NimBLE Stream API +- Testing BLE connectivity +- Starting point for custom applications +- Understanding Stream read/write operations diff --git a/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino b/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino new file mode 100644 index 000000000..086954551 --- /dev/null +++ b/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino @@ -0,0 +1,124 @@ +/** + * NimBLE_Stream_Server Example: + * + * Demonstrates using NimBLEStreamServer to create a BLE GATT server + * that behaves like a serial port 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. + * + * Created: November 2025 + * Author: NimBLE-Arduino Contributors + */ + +#include +#include + +// Create the stream server instance +NimBLEStreamServer bleStream; + +// 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" + +/** Server callbacks to handle connection/disconnection events */ +class ServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override { + Serial.printf("Client connected: %s\n", connInfo.getAddress().toString().c_str()); + // Optionally update connection parameters for better throughput + pServer->updateConnParams(connInfo.getConnHandle(), 12, 24, 0, 200); + } + + void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override { + Serial.printf("Client disconnected - reason: %d, restarting advertising\n", reason); + NimBLEDevice::startAdvertising(); + } + + void onMTUChange(uint16_t MTU, NimBLEConnInfo& connInfo) override { + Serial.printf("MTU updated: %u for connection ID: %u\n", MTU, connInfo.getConnHandle()); + } +} serverCallbacks; + +void setup() { + Serial.begin(115200); + Serial.println("Starting NimBLE Stream Server"); + + /** 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 characteristic + */ + NimBLEServer* pServer = NimBLEDevice::createServer(); + pServer->setCallbacks(&serverCallbacks); + + /** + * Initialize the stream server with: + * - Service UUID + * - Characteristic UUID + * - 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), + 1024, // txBufSize + 1024, // rxBufSize + false)) // secure + { + Serial.println("Failed to initialize BLE stream!"); + return; + } + + /** + * Create advertising instance and add service UUID + * This makes the stream service discoverable + */ + NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setName("NimBLE-Stream"); + pAdvertising->enableScanResponse(true); + pAdvertising->start(); + + Serial.println("BLE Stream Server ready!"); + Serial.println("Waiting for client connection..."); + Serial.println("Once connected, you can send data from the client."); +} + +void loop() { + // Check if a client is subscribed (connected and listening) + if (bleStream.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()); + + // You can also use printf + bleStream.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()) { + 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 + } + Serial.println(); + } + } else { + // No subscriber, just wait + delay(100); + } +} diff --git a/examples/NimBLE_Stream_Server/README.md b/examples/NimBLE_Stream_Server/README.md new file mode 100644 index 000000000..90ce5e1e2 --- /dev/null +++ b/examples/NimBLE_Stream_Server/README.md @@ -0,0 +1,42 @@ +# NimBLE Stream Server Example + +This example demonstrates how to use the `NimBLEStreamServer` class to create a BLE GATT server that behaves like a serial port using the familiar Arduino Stream interface. + +## Features + +- Uses Arduino Stream interface (print, println, read, available, etc.) +- Automatic connection management +- Bidirectional communication +- Buffered TX/RX using ring buffers +- 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 +3. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()` +4. Automatically handles connection state and MTU negotiation + +## Usage + +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) +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: `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. + +## Compatible With + +- NimBLE_Stream_Client example +- nRF Connect mobile app +- Serial Bluetooth Terminal apps +- Any BLE client that supports characteristic notifications and writes diff --git a/src/NimBLEDevice.h b/src/NimBLEDevice.h index 28baadf63..df63b3dc0 100644 --- a/src/NimBLEDevice.h +++ b/src/NimBLEDevice.h @@ -245,7 +245,7 @@ class NimBLEDevice { # endif # ifdef ESP_PLATFORM -# if NIMBLE_CPP_SCAN_DUPL_ENABLED +# if NIMBLE_CPP_SCAN_DUPL_ENABLED static uint16_t m_scanDuplicateSize; static uint8_t m_scanFilterMode; static uint16_t m_scanDuplicateResetTime; @@ -306,6 +306,7 @@ class NimBLEDevice { # if MYNEWT_VAL(BLE_ROLE_CENTRAL) || MYNEWT_VAL(BLE_ROLE_PERIPHERAL) # include "NimBLEConnInfo.h" +# include "NimBLEStream.h" # endif # include "NimBLEAddress.h" diff --git a/src/NimBLEStream.cpp b/src/NimBLEStream.cpp new file mode 100644 index 000000000..92a15563a --- /dev/null +++ b/src/NimBLEStream.cpp @@ -0,0 +1,955 @@ +/* + * Copyright 2020-2025 Ryan Powell and + * esp-nimble-cpp, NimBLE-Arduino contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NimBLEStream.h" +#if CONFIG_BT_NIMBLE_ENABLED && (MYNEWT_VAL(BLE_ROLE_PERIPHERAL) || MYNEWT_VAL(BLE_ROLE_CENTRAL)) + +# include "NimBLEDevice.h" +# include "NimBLELog.h" +# if defined(CONFIG_NIMBLE_CPP_IDF) +# include "os/os_mbuf.h" +# include "nimble/nimble_port.h" +# else +# include "nimble/porting/nimble/include/os/os_mbuf.h" +# include "nimble/porting/nimble/include/nimble/nimble_port.h" +# endif +# include +# include +# include +# include + +static const char* LOG_TAG = "NimBLEStream"; + +struct NimBLEStream::ByteRingBuffer { + /** @brief Guard for ByteRingBuffer to manage locking. */ + struct Guard { + const ByteRingBuffer& _b; + bool _locked; + Guard(const ByteRingBuffer& b) : _b(b), _locked(b.lock()) {} + ~Guard() { + if (_locked) _b.unlock(); + } + operator bool() const { return _locked; } // Allows: if (Guard g{*this}) { ... } + }; + + /** @brief Construct a ByteRingBuffer with the specified capacity. */ + explicit ByteRingBuffer(size_t capacity) : m_capacity(capacity) { + memset(&m_mutex, 0, sizeof(m_mutex)); + auto rc = ble_npl_mutex_init(&m_mutex); + if (rc != BLE_NPL_OK) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize ring buffer mutex, error: %d", rc); + return; + } + + m_buf = static_cast(malloc(capacity)); + if (!m_buf) { + NIMBLE_LOGE(LOG_TAG, "Failed to allocate ring buffer memory"); + return; + } + } + + /** @brief Destroy the ByteRingBuffer and release resources. */ + ~ByteRingBuffer() { + if (m_buf) { + free(m_buf); + } + ble_npl_mutex_deinit(&m_mutex); + } + + /** @brief Check if the ByteRingBuffer is valid. */ + bool valid() const { return m_buf != nullptr; } + + /** @brief Get the capacity of the ByteRingBuffer. */ + size_t capacity() const { return m_capacity; } + + /** @brief Get the current size of the ByteRingBuffer. */ + size_t size() const { + Guard g(*this); + return g ? m_size : 0; + } + + /** @brief Get the available free space in the ByteRingBuffer. */ + size_t freeSize() const { + Guard g(*this); + return g ? m_capacity - m_size : 0; + } + + /** + * @brief Write data to the ByteRingBuffer. + * @param data Pointer to the data to write. + * @param len Length of the data to write. + * @returns the number of bytes actually written, which may be less than len if the buffer does not have enough free space. + */ + size_t write(const uint8_t* data, size_t len) { + if (!data || len == 0) { + return 0; + } + + Guard g(*this); + if (!g || m_size >= m_capacity) { + return 0; + } + + size_t count = std::min(len, m_capacity - m_size); + size_t first = std::min(count, m_capacity - m_head); + memcpy(m_buf + m_head, data, first); + size_t remain = count - first; + if (remain > 0) { + memcpy(m_buf, data + first, remain); + } + + m_head = (m_head + count) % m_capacity; + m_size += count; + return count; + } + + /** + * @brief Read data from the ByteRingBuffer. + * @param out Pointer to the buffer where read data will be stored. + * @param len Maximum number of bytes to read. + * @returns the number of bytes actually read. + */ + size_t read(uint8_t* out, size_t len) { + if (!out || len == 0) { + return 0; + } + + Guard g(*this); + if (!g || m_size == 0) { + return 0; + } + + size_t count = std::min(len, m_size); + size_t first = std::min(count, m_capacity - m_tail); + memcpy(out, m_buf + m_tail, first); + size_t remain = count - first; + if (remain > 0) { + memcpy(out + first, m_buf, remain); + } + + m_tail = (m_tail + count) % m_capacity; + m_size -= count; + return count; + } + + /** + * @brief Peek at data in the ByteRingBuffer without removing it. + * @param out Pointer to the buffer where peeked data will be stored. + * @param len Maximum number of bytes to peek. + * @returns the number of bytes actually peeked. + */ + size_t peek(uint8_t* out, size_t len) { + if (!out || len == 0) { + return 0; + } + + Guard g(*this); + if (!g || m_size == 0) { + return 0; + } + + size_t count = std::min(len, m_size); + size_t first = std::min(count, m_capacity - m_tail); + memcpy(out, m_buf + m_tail, first); + size_t remain = count - first; + if (remain > 0) { + memcpy(out + first, m_buf, remain); + } + + return count; + } + + /** + * @brief Drop data from the ByteRingBuffer without reading it. + * @param len Maximum number of bytes to drop. + * @returns the number of bytes actually dropped. + */ + size_t drop(size_t len) { + if (len == 0) { + return 0; + } + + Guard g(*this); + if (!g || m_size == 0) { + return 0; + } + size_t count = std::min(len, m_size); + m_tail = (m_tail + count) % m_capacity; + m_size -= count; + return count; + } + + private: + /** + * @brief Lock the ByteRingBuffer for exclusive access. + * @return true if the lock was successfully acquired, false otherwise. + */ + bool lock() const { return valid() && ble_npl_mutex_pend(&m_mutex, BLE_NPL_TIME_FOREVER) == BLE_NPL_OK; } + + /** + * @brief Unlock the ByteRingBuffer after exclusive access. + */ + void unlock() const { ble_npl_mutex_release(&m_mutex); } + + uint8_t* m_buf{nullptr}; + size_t m_capacity{0}; + size_t m_head{0}; + size_t m_tail{0}; + size_t m_size{0}; + mutable ble_npl_mutex m_mutex{}; +}; + +// Stub Print/Stream implementations when Arduino not available +# if !NIMBLE_CPP_ARDUINO_STRING_AVAILABLE +# include + +size_t Print::print(const char* s) { + if (!s) return 0; + return write(reinterpret_cast(s), strlen(s)); +} + +size_t Print::println(const char* s) { + size_t n = print(s); + static const char crlf[] = "\r\n"; + n += write(reinterpret_cast(crlf), 2); + return n; +} + +size_t Print::printf(const char* fmt, ...) { + if (!fmt) { + return 0; + } + + char stackBuf[128]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(stackBuf, sizeof(stackBuf), fmt, ap); + va_end(ap); + if (n < 0) { + return 0; + } + + if (static_cast(n) < sizeof(stackBuf)) { + return write(reinterpret_cast(stackBuf), static_cast(n)); + } + + // allocate for larger output + size_t needed = static_cast(n) + 1; + char* buf = static_cast(malloc(needed)); + if (!buf) { + return 0; + } + + va_start(ap, fmt); + vsnprintf(buf, needed, fmt, ap); + va_end(ap); + size_t ret = write(reinterpret_cast(buf), static_cast(n)); + free(buf); + return ret; +} +# endif + +/** + * @brief Initialize the NimBLEStream, creating TX and RX buffers and setting up events. + * @return true if initialization was successful, false otherwise. + */ +bool NimBLEStream::begin() { + if (m_txBuf || m_rxBuf) { + NIMBLE_LOGW(LOG_TAG, "Already initialized"); + return true; + } + + if (m_txBufSize == 0 && m_rxBufSize == 0) { + NIMBLE_LOGE(LOG_TAG, "Cannot initialize stream with both TX and RX buffer sizes set to 0"); + return false; + } + + if (ble_npl_callout_init(&m_txDrainCallout, nimble_port_get_dflt_eventq(), NimBLEStream::txDrainCalloutCb, this) != 0) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize TX drain callout"); + return false; + } + m_coInitialized = true; + + ble_npl_event_init(&m_txDrainEvent, NimBLEStream::txDrainEventCb, this); + m_eventInitialized = true; + + if (m_txBufSize) { + m_txBuf = new ByteRingBuffer(m_txBufSize); + if (!m_txBuf || !m_txBuf->valid()) { + NIMBLE_LOGE(LOG_TAG, "Failed to create TX ringbuffer"); + end(); + return false; + } + } + + if (m_rxBufSize) { + m_rxBuf = new ByteRingBuffer(m_rxBufSize); + if (!m_rxBuf || !m_rxBuf->valid()) { + NIMBLE_LOGE(LOG_TAG, "Failed to create RX ringbuffer"); + end(); + return false; + } + } + + return true; +} + +/** + * @brief Clean up the NimBLEStream, stopping events and deleting buffers. + */ +void NimBLEStream::end() { + if (m_coInitialized) { + ble_npl_callout_stop(&m_txDrainCallout); + ble_npl_callout_deinit(&m_txDrainCallout); + m_coInitialized = false; + } + + if (m_eventInitialized) { + ble_npl_eventq_remove(nimble_port_get_dflt_eventq(), &m_txDrainEvent); + ble_npl_event_deinit(&m_txDrainEvent); + m_eventInitialized = false; + } + + if (m_txBuf) { + delete m_txBuf; + m_txBuf = nullptr; + } + + if (m_rxBuf) { + delete m_rxBuf; + m_rxBuf = nullptr; + } +} + +/** + * @brief Write data to the stream, which will be sent over BLE. + * @param data Pointer to the data to write. + * @param len Length of the data to write. + * @return the number of bytes actually written to the stream buffer. + */ +size_t NimBLEStream::write(const uint8_t* data, size_t len) { + if (!m_txBuf) { + return 0; + } + + auto written = m_txBuf->write(data, len); + drainTx(); + return written; +} + +/** + * @brief Get the available free space in the stream's TX buffer. + * @return the number of bytes that can be written to the stream without blocking. + */ +size_t NimBLEStream::availableForWrite() const { + return m_txBuf ? m_txBuf->freeSize() : 0; +} + +/** + * @brief Schedule the stream to attempt to send data from the TX buffer. + * @details This should be called whenever new data is written to the TX buffer + * or when a send attempt fails due to lack of BLE buffers. + */ +void NimBLEStream::drainTx() { + if (!m_txBuf || m_txBuf->size() == 0) { + return; + } + + ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &m_txDrainEvent); +} + +/** + * @brief Event callback for when the stream is scheduled to drain the TX buffer. + * @param ev Pointer to the event that triggered the callback. + * @details This will attempt to send data from the TX buffer. If sending fails due to + * lack of BLE buffers, it will reschedule itself to try again after a short delay. + */ +void NimBLEStream::txDrainEventCb(struct ble_npl_event* ev) { + if (!ev) { + return; + } + + auto* stream = static_cast(ble_npl_event_get_arg(ev)); + if (!stream) { + return; + } + + if (stream->send()) { + // Schedule a short delayed retry to give the stack time to free buffers, use 5ms for now + // TODO: consider options for the delay time and retry strategy if the stack is persistently out of buffers + ble_npl_callout_reset(&stream->m_txDrainCallout, ble_npl_time_ms_to_ticks32(5)); + } +} + +/** + * @brief Callout callback for when the stream is scheduled to retry draining the TX buffer. + * @param ev Pointer to the event that triggered the callback. + * @details This will call drainTx() to attempt to send data from the TX buffer again. + */ +void NimBLEStream::txDrainCalloutCb(struct ble_npl_event* ev) { + if (!ev) { + return; + } + + auto* stream = static_cast(ble_npl_event_get_arg(ev)); + if (!stream) { + return; + } + + stream->drainTx(); +} + +/** + * @brief Get the number of bytes available to read from the stream. + * @return the number of bytes that can be read from the stream. + */ +int NimBLEStream::available() { + if (!m_rxBuf) { + return 0; + } + + return static_cast(m_rxBuf->size()); +} + +/** + * @brief Read a single byte from the stream. + * @return the byte read as an int, or -1 if no data is available. + */ +int NimBLEStream::read() { + if (!m_rxBuf) { + return -1; + } + + uint8_t byte = 0; + if (m_rxBuf->read(&byte, 1) == 0) { + return -1; + } + + return static_cast(byte); +} + +/** + * @brief Peek at the next byte in the stream without removing it. + * @return the byte peeked as an int, or -1 if no data is available. + */ +int NimBLEStream::peek() { + if (!m_rxBuf) { + return -1; + } + + uint8_t byte = 0; + if (m_rxBuf->peek(&byte, 1) == 0) { + return -1; + } + + return static_cast(byte); +} + +/** + * @brief Read data from the stream into a buffer. + * @param buffer Pointer to the buffer where read data will be stored. + * @param len Maximum number of bytes to read. + * @return the number of bytes actually read from the stream. + */ +size_t NimBLEStream::read(uint8_t* buffer, size_t len) { + if (!m_rxBuf) { + return 0; + } + + return m_rxBuf->read(buffer, len); +} + +/** + * @brief Push received data into the stream's RX buffer. + * @param data Pointer to the data to push into the RX buffer. + * @param len Length of the data to push. + * @return the number of bytes actually pushed into the RX buffer, which may be less than + * len if the buffer does not have enough free space. + */ +size_t NimBLEStream::pushRx(const uint8_t* data, size_t len) { + if (!m_rxBuf) { + return 0; + } + + size_t freeSize = m_rxBuf->freeSize(); + if (len > freeSize) { + const bool dropOlderData = m_rxOverflowCallback && m_rxOverflowCallback(data, len); + if (!dropOlderData) { + NIMBLE_LOGE(LOG_TAG, "RX buffer overflow, dropping current data"); + return 0; + } + + if (len >= m_rxBuf->capacity()) { + m_rxBuf->drop(m_rxBuf->size()); + const uint8_t* tail = data + (len - m_rxBuf->capacity()); + size_t written = m_rxBuf->write(tail, m_rxBuf->capacity()); + if (written != m_rxBuf->capacity()) { + NIMBLE_LOGE(LOG_TAG, "RX buffer overflow, %zu bytes dropped", m_rxBuf->capacity() - written); + } + return written; + } + + const size_t requiredSpace = len - freeSize; + size_t dropped = m_rxBuf->drop(requiredSpace); + if (dropped < requiredSpace) { + NIMBLE_LOGE(LOG_TAG, "RX buffer overflow, failed to drop enough buffered data"); + return 0; + } + } + + return m_rxBuf->write(data, len); +} + +# if MYNEWT_VAL(BLE_ROLE_PERIPHERAL) +/** + * @brief Initialize the NimBLEStreamServer with an existing characteristic. + * @param pChr Pointer to the existing NimBLECharacteristic to use for the stream. + * @param txBufSize Size of the TX buffer. + * @param rxBufSize Size of the RX buffer. + * @return true if initialization was successful, false otherwise. + * @details The provided characteristic must have the NOTIFY property set for write operations + * to work and the WRITE or WRITE_NR property set for read operations to work. + * The stream will manage setting its own callbacks on the characteristic, but will save and call + * any existing user callbacks if they were set. + * The RX buffer will only be created if the characteristic has WRITE or WRITE_NR properties. + * The TX buffer will only be created if the characteristic has NOTIFY properties. + */ +bool NimBLEStreamServer::begin(NimBLECharacteristic* pChr, 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 (!pChr) { + NIMBLE_LOGE(LOG_TAG, "Characteristic is null"); + return false; + } + + auto props = pChr->getProperties(); + bool canWrite = (props & NIMBLE_PROPERTY::WRITE) || (props & NIMBLE_PROPERTY::WRITE_NR); + if (!canWrite && rxBufSize > 0) { + NIMBLE_LOGW(LOG_TAG, "Characteristic does not support WRITE, ignoring RX buffer size"); + } + + bool canNotify = props & NIMBLE_PROPERTY::NOTIFY; + if (!canNotify && txBufSize > 0) { + NIMBLE_LOGW(LOG_TAG, "Characteristic does not support NOTIFY, ignoring TX buffer size"); + } + + m_rxBufSize = canWrite ? rxBufSize : 0; // disable RX if not writable + m_txBufSize = canNotify ? txBufSize : 0; // disable TX if notifications not supported + + if (!NimBLEStream::begin()) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream buffers"); + return false; + } + + m_charCallbacks.m_userCallbacks = pChr->getCallbacks(); + pChr->setCallbacks(&m_charCallbacks); + m_pChr = pChr; + return true; +} + +/** + * @brief Initialize the NimBLEStreamServer, creating a BLE service and characteristic for streaming. + * @param svcUuid UUID of the BLE service to create. + * @param chrUuid UUID of the BLE characteristic to create. + * @param txBufSize Size of the TX buffer, set to 0 to disable TX and create a write-only characteristic. + * @param rxBufSize Size of the RX buffer, set to 0 to disable RX and create a notify-only characteristic. + * @param secure Whether the characteristic requires encryption. + * @return true if initialization was successful, false otherwise. + */ +bool NimBLEStreamServer::begin( + const NimBLEUUID& svcUuid, const NimBLEUUID& chrUuid, 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; + } + + // Create characteristic with notify + write properties for bidirectional stream + uint32_t props = 0; + if (txBufSize > 0) { + props |= NIMBLE_PROPERTY::NOTIFY; + if (secure) { + props |= NIMBLE_PROPERTY::READ_ENC; + } + } + + if (rxBufSize > 0) { + props |= NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR; + if (secure) { + props |= NIMBLE_PROPERTY::WRITE_ENC; + } + } + + auto pChr = pSvc->createCharacteristic(chrUuid, props); + if (!pChr) { + NIMBLE_LOGE(LOG_TAG, "Failed to create characteristic"); + goto error; + } + + m_deleteSvcOnEnd = true; // mark service for deletion on end since we created it here + + if (!pSvc->start()) { + NIMBLE_LOGE(LOG_TAG, "Failed to start service"); + goto error; + } + + if (!begin(pChr, txBufSize, rxBufSize)) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream with characteristic"); + goto error; + } + + return true; + +error: + pServer->removeService(pSvc, true); // delete service and all its characteristics + m_pChr = nullptr; // reset characteristic pointer as it's now invalid after service removal + end(); + return false; +} + +/** + * @brief Stop the NimBLEStreamServer + * @details This will stop the stream and delete the service created if it was created by this class. + */ +void NimBLEStreamServer::end() { + if (m_pChr) { + if (m_deleteSvcOnEnd) { + auto pSvc = m_pChr->getService(); + if (pSvc) { + auto pServer = pSvc->getServer(); + if (pServer) { + pServer->removeService(pSvc, true); + } + } + } else { + m_pChr->setCallbacks(m_charCallbacks.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; + NimBLEStream::end(); +} + +/** + * @brief Write data to the stream, which will be sent as BLE notifications. + * @param data Pointer to the data to write. + * @param len Length of the data to write. + * @return the number of bytes actually written to the stream buffer. + */ +size_t NimBLEStreamServer::write(const uint8_t* data, size_t len) { + if (!m_pChr || len == 0 || !ready()) { + return 0; + } + +# if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 4 + // Skip server gap events to avoid log recursion + static const char filterStr[] = "handleGapEvent"; + constexpr size_t filterLen = sizeof(filterStr) - 1; + if (len >= filterLen + 3) { + for (size_t i = 3; i <= len - filterLen; i++) { + if (memcmp(data + i, filterStr, filterLen) == 0) { + return len; // drop to avoid recursion + } + } + } +# endif + + return NimBLEStream::write(data, len); +} + +/** + * @brief Attempt to send data from the TX buffer as BLE notifications. + * @return true if a retry should be scheduled, false otherwise. + * @details This will try to send as much data as possible from the TX buffer in chunks + * that fit within the current BLE MTU. If sending fails due to lack of BLE buffers, it will return true + * to indicate that a retry should be scheduled, but it will not drop any data from the TX buffer. + * For other errors or if all data is sent, it returns false. + */ +bool NimBLEStreamServer::send() { + if (!m_pChr || !m_txBuf || !ready()) { + return false; + } + + size_t mtu = ble_att_mtu(getPeerHandle()); + if (mtu < 23) { + return false; + } + + size_t maxDataLen = std::min(mtu - 3, sizeof(m_txChunkBuf)); + + while (m_txBuf->size()) { + size_t chunkLen = m_txBuf->peek(m_txChunkBuf, maxDataLen); + if (!chunkLen) { + break; + } + + if (!m_pChr->notify(m_txChunkBuf, chunkLen, getPeerHandle())) { + if (m_rc == BLE_HS_ENOMEM || os_msys_num_free() <= 2) { + // NimBLE stack out of buffers, likely due to pending notifications/indications + // Don't drop data, but wait for stack to free buffers and try again later + return true; + } + + return false; // disconnect or other error don't retry send, preserve data for next attempt + } + + m_txBuf->drop(chunkLen); + } + + return false; // no more data to send +} + +/** + * @brief Check if the stream is ready to send/receive data, which requires an active BLE connection. + * @return true if the stream is ready, false otherwise. + */ +bool NimBLEStreamServer::ready() const { + if (m_charCallbacks.m_peerHandle == BLE_HS_CONN_HANDLE_NONE) { + return false; + } + + return ble_gap_conn_find(m_charCallbacks.m_peerHandle, NULL) == 0; +} + +/** + * @brief Callback for when the characteristic is written to by a client. + * @param pChr Pointer to the 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::ChrCallbacks::onWrite(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo) { + // Push received data into RX buffer + auto val = pChr->getValue(); + m_parent->pushRx(val.data(), val.size()); + + if (m_userCallbacks) { + m_userCallbacks->onWrite(pChr, connInfo); + } +} + +/** + * @brief Callback for when a client subscribes or unsubscribes to notifications/indications. + * @param pChr Pointer to the characteristic that was subscribed/unsubscribed. + * @param connInfo Information about the connection that performed the subscribe/unsubscribe. + * @param subValue The new subscription value (0 for unsubscribe, non-zero for subscribe). + * @details This will track the subscriber's connection handle and call any user-defined callbacks. + * Only one subscriber is supported; if another client tries to subscribe while one is already subscribed, it will be ignored. + */ +void NimBLEStreamServer::ChrCallbacks::onSubscribe(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo, uint16_t subValue) { + // If we have a stored peer handle, ensure it still refers to an active connection. + // If the connection has gone away without an explicit unsubscribe, clear it so a new + // subscriber can be accepted. + if (m_peerHandle != BLE_HS_CONN_HANDLE_NONE) { + if (ble_gap_conn_find(m_peerHandle, nullptr) != 0) { + m_peerHandle = BLE_HS_CONN_HANDLE_NONE; + } + } + + // only one subscriber supported + if (subValue && m_peerHandle != BLE_HS_CONN_HANDLE_NONE) { + NIMBLE_LOGI(LOG_TAG, + "Already have a subscriber, rejecting new subscription from conn handle %d", + connInfo.getConnHandle()); + return; + } + + m_peerHandle = subValue ? connInfo.getConnHandle() : BLE_HS_CONN_HANDLE_NONE; + if (m_userCallbacks) { + m_userCallbacks->onSubscribe(pChr, connInfo, subValue); + } +} + +/** + * @brief Callback for when the connection status changes (e.g. disconnect). + * @param pChr Pointer to the characteristic associated with the status change. + * @param connInfo Information about the connection that changed status. + * @param code The new status code (e.g. success or error code). + * @details The code is used to track when the stack is out of buffers (BLE_HS_ENOMEM) + * to trigger retries without dropping data. User-defined callbacks will also be called if set. + */ +void NimBLEStreamServer::ChrCallbacks::onStatus(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo, int code) { + m_parent->m_rc = code; + if (m_userCallbacks) { + m_userCallbacks->onStatus(pChr, connInfo, code); + } +} + +# endif // MYNEWT_VAL(BLE_ROLE_PERIPHERAL) + +# if MYNEWT_VAL(BLE_ROLE_CENTRAL) +/** + * @brief Initialize the NimBLEStreamClient, setting up the remote characteristic and subscribing to notifications if requested. + * @param pChr Pointer to the remote characteristic to use for streaming. + * @param subscribe Whether to subscribe to notifications/indications from the characteristic for RX. + * @param txBufSize Size of the TX buffer. + * @param rxBufSize Size of the RX buffer. + * @return true if initialization was successful, false otherwise. + * @details The characteristic must support write without response for TX. + * If subscribe is true, it will subscribe to notifications/indications for RX. + * If subscribe is false, the RX buffer will not be created and no notifications will be received. + */ +bool NimBLEStreamClient::begin(NimBLERemoteCharacteristic* pChr, bool subscribe, 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 (!pChr) { + NIMBLE_LOGE(LOG_TAG, "Remote characteristic is null"); + return false; + } + + if (!pChr->canWriteNoResponse()) { + NIMBLE_LOGE(LOG_TAG, "Characteristic does not support write without response"); + return false; + } + + if (subscribe && !pChr->canNotify() && !pChr->canIndicate()) { + NIMBLE_LOGW(LOG_TAG, "Characteristic does not support subscriptions, RX disabled"); + subscribe = false; // disable subscribe if not supported + } + + m_txBufSize = txBufSize; + m_rxBufSize = subscribe ? rxBufSize : 0; // disable RX buffer if not subscribing + + if (!NimBLEStream::begin()) { + NIMBLE_LOGE(LOG_TAG, "Failed to initialize stream buffers"); + return false; + } + + // Subscribe to notifications/indications for RX if requested + if (subscribe) { + using namespace std::placeholders; + if (!pChr->subscribe(pChr->canNotify(), std::bind(&NimBLEStreamClient::notifyCallback, this, _1, _2, _3, _4))) { + NIMBLE_LOGE(LOG_TAG, "Failed to subscribe for %s", pChr->canNotify() ? "notifications" : "indications"); + end(); + return false; + } + } + + m_pChr = pChr; + 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(); + } + + m_pChr = nullptr; + NimBLEStream::end(); +} + +/** + * @brief Write data to the stream, which will be sent as BLE writes to the remote characteristic. + * @return True if a retry should be scheduled due to lack of BLE buffers, false otherwise. + * @details This will try to send as much data as possible from the TX buffer in chunks + * that fit within the memory buffers. If sending fails due to lack of BLE buffers, it will return true + * to indicate that a retry should be scheduled, but it will not drop any data from the TX buffer. + * For other errors or if all data is sent, it returns false. + */ +bool NimBLEStreamClient::send() { + if (!ready() || !m_txBuf) { + return false; + } + + auto mtu = m_pChr->getClient()->getMTU(); + if (mtu < 23) { + return false; + } + + size_t maxDataLen = std::min(mtu - 3, sizeof(m_txChunkBuf)); + + while (m_txBuf->size()) { + size_t chunkLen = m_txBuf->peek(m_txChunkBuf, maxDataLen); + if (!chunkLen) { + break; + } + + if (!m_pChr->writeValue(m_txChunkBuf, chunkLen, false)) { + if (os_msys_num_free() <= 2) { + // NimBLE stack out of buffers, likely due to pending writes + // Don't drop data, wait for stack to free buffers and try again later + return true; + } + + break; // preserve data, no retry + } + + m_txBuf->drop(chunkLen); + } + + return false; // don't retry, it's either sent or we are disconnected +} + +/** + * @brief Check if the stream is ready for communication. + * @return true if the stream is ready, false otherwise. + * @details The stream is considered ready if the remote characteristic is set and the client connection is active. + */ +bool NimBLEStreamClient::ready() const { + return m_pChr != nullptr && m_pChr->getClient()->isConnected(); +} + +/** + * @brief Callback for when a notification or indication is received from the remote characteristic. + * @param pChar Pointer to the characteristic that sent the notification/indication. + * @param pData Pointer to the data received in the notification/indication. + * @param len Length of the data received. + * @param isNotify True if the data was received as a notification, false if it was received as an indication. + * @details This will push the received data into the RX buffer and call any user-defined callbacks. + */ +void NimBLEStreamClient::notifyCallback(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify) { + pushRx(pData, len); + if (m_userNotifyCallback) { + m_userNotifyCallback(pChar, pData, len, isNotify); + } +} +# endif // MYNEWT_VAL(BLE_ROLE_CENTRAL) +#endif // CONFIG_BT_NIMBLE_ENABLED && (MYNEWT_VAL(BLE_ROLE_PERIPHERAL) || MYNEWT_VAL(BLE_ROLE_CENTRAL)) diff --git a/src/NimBLEStream.h b/src/NimBLEStream.h new file mode 100644 index 000000000..dbb9e176f --- /dev/null +++ b/src/NimBLEStream.h @@ -0,0 +1,218 @@ +/* + * Copyright 2020-2025 Ryan Powell and + * esp-nimble-cpp, NimBLE-Arduino contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NIMBLE_CPP_STREAM_H +#define NIMBLE_CPP_STREAM_H + +#include "syscfg/syscfg.h" +#if CONFIG_BT_NIMBLE_ENABLED && (MYNEWT_VAL(BLE_ROLE_PERIPHERAL) || MYNEWT_VAL(BLE_ROLE_CENTRAL)) + +# if defined(CONFIG_NIMBLE_CPP_IDF) +# include "nimble/nimble_npl.h" +# else +# include "nimble/nimble/include/nimble/nimble_npl.h" +# endif + +# include +# include +# include + +# if NIMBLE_CPP_ARDUINO_STRING_AVAILABLE +# include +# else + +// Minimal Stream/Print stubs when Arduino not available +class Print { + public: + virtual ~Print() {} + virtual size_t write(uint8_t) = 0; + virtual size_t write(const uint8_t* buffer, size_t size) = 0; + size_t print(const char* s); + size_t println(const char* s); + size_t printf(const char* format, ...) __attribute__((format(printf, 2, 3))); +}; + +class Stream : public Print { + public: + virtual int available() = 0; + virtual int read() = 0; + virtual int peek() = 0; + virtual void flush() {} + void setTimeout(unsigned long timeout) { m_timeout = timeout; } + unsigned long getTimeout() const { return m_timeout; } + + protected: + unsigned long m_timeout{0}; +}; +# endif + +class NimBLEStream : public Stream { + public: + typedef std::function rx_overflow_callback_t; + + NimBLEStream() = default; + virtual ~NimBLEStream() { end(); } + + // Print/Stream TX methods + virtual size_t write(const uint8_t* data, size_t len) override; + virtual size_t write(uint8_t data) override { return write(&data, 1); } + + // Template for other integral types (char, int, long, etc.) + template + typename std::enable_if::value && !std::is_same::value, size_t>::type write(T data) { + return write(static_cast(data)); + } + + virtual void flush() override {} + + size_t availableForWrite() const; + + // Read up to len bytes into buffer (non-blocking) + size_t read(uint8_t* buffer, size_t len); + + // Stream RX methods + virtual int available() override; + virtual int read() override; + virtual int peek() override; + virtual bool ready() const = 0; + + /** + * @brief Set a callback to be invoked when incoming data exceeds RX buffer capacity. + * @param cb The callback function, which should return true to drop older buffered data and + * make room for the new data, or false to drop the new data instead. + */ + void setRxOverflowCallback(rx_overflow_callback_t cb) { m_rxOverflowCallback = cb; } + + operator bool() const { return ready(); } + + using Print::write; + + struct ByteRingBuffer; + + protected: + bool begin(); + void drainTx(); + size_t pushRx(const uint8_t* data, size_t len); + virtual void end(); + virtual bool send() = 0; + static void txDrainEventCb(struct ble_npl_event* ev); + static void txDrainCalloutCb(struct ble_npl_event* ev); + + ByteRingBuffer* m_txBuf{nullptr}; + ByteRingBuffer* m_rxBuf{nullptr}; + uint8_t m_txChunkBuf[MYNEWT_VAL(BLE_ATT_PREFERRED_MTU)]; + uint32_t m_txBufSize{1024}; + uint32_t m_rxBufSize{1024}; + ble_npl_event m_txDrainEvent{}; + ble_npl_callout m_txDrainCallout{}; + bool m_coInitialized{false}; + bool m_eventInitialized{false}; + rx_overflow_callback_t m_rxOverflowCallback{nullptr}; +}; + +# if MYNEWT_VAL(BLE_ROLE_PERIPHERAL) +# include "NimBLECharacteristic.h" + +class NimBLEStreamServer : public NimBLEStream { + public: + NimBLEStreamServer() : m_charCallbacks(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); + + // Convenience overload to create service/characteristic internally; service will be deleted on end() + bool begin(const NimBLEUUID& svcUuid, + const NimBLEUUID& chrUuid, + 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; } + void setCallbacks(NimBLECharacteristicCallbacks* pCallbacks) { m_charCallbacks.m_userCallbacks = pCallbacks; } + bool ready() const override; + + using NimBLEStream::write; // Inherit template write overloads + + protected: + bool send() override; + + struct ChrCallbacks : public NimBLECharacteristicCallbacks { + ChrCallbacks(NimBLEStreamServer* parent) + : m_parent(parent), m_userCallbacks(nullptr), m_peerHandle(BLE_HS_CONN_HANDLE_NONE) {} + void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override; + void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue) override; + void onStatus(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, int code) override; + // override this to avoid recursion when debug logs are enabled + void onStatus(NimBLECharacteristic* pCharacteristic, int code) override { + if (m_userCallbacks != nullptr) { + m_userCallbacks->onStatus(pCharacteristic, code); + } + } + + NimBLEStreamServer* m_parent; + NimBLECharacteristicCallbacks* m_userCallbacks; + uint16_t m_peerHandle; + } m_charCallbacks; + + 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}; +}; +# endif // BLE_ROLE_PERIPHERAL + +# if MYNEWT_VAL(BLE_ROLE_CENTRAL) +# include "NimBLERemoteCharacteristic.h" + +class NimBLEStreamClient : public NimBLEStream { + public: + NimBLEStreamClient() = default; + ~NimBLEStreamClient() override { end(); } + + // non-copyable + NimBLEStreamClient(const NimBLEStreamClient&) = delete; + NimBLEStreamClient& operator=(const NimBLEStreamClient&) = delete; + + // 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); + void end() override; + void setNotifyCallback(NimBLERemoteCharacteristic::notify_callback cb) { m_userNotifyCallback = cb; } + bool ready() const override; + + using NimBLEStream::write; // Inherit template write overloads + + protected: + bool send() override; + void notifyCallback(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify); + + NimBLERemoteCharacteristic* m_pChr{nullptr}; + NimBLERemoteCharacteristic::notify_callback m_userNotifyCallback{nullptr}; +}; +# endif // BLE_ROLE_CENTRAL + +#endif // CONFIG_BT_NIMBLE_ENABLED && (MYNEWT_VAL(BLE_ROLE_PERIPHERAL) || MYNEWT_VAL(BLE_ROLE_CENTRAL)) +#endif // NIMBLE_CPP_STREAM_H diff --git a/src/nimble/console/console.h b/src/nimble/console/console.h index 4b32c8b15..33aae03d8 100644 --- a/src/nimble/console/console.h +++ b/src/nimble/console/console.h @@ -5,9 +5,10 @@ #ifdef __cplusplus extern "C" { -#endif - +#define console_printf(_fmt, ...) ::printf(_fmt, ##__VA_ARGS__) +#else #define console_printf(_fmt, ...) printf(_fmt, ##__VA_ARGS__) +#endif #ifdef __cplusplus } diff --git a/src/nimconfig.h b/src/nimconfig.h index 5b230f0ab..72a8a7068 100644 --- a/src/nimconfig.h +++ b/src/nimconfig.h @@ -180,12 +180,12 @@ # define CONFIG_BT_NIMBLE_USE_ESP_TIMER (1) # endif -#define MYNEWT_VAL_BLE_USE_ESP_TIMER (CONFIG_BT_NIMBLE_USE_ESP_TIMER) +# define MYNEWT_VAL_BLE_USE_ESP_TIMER (CONFIG_BT_NIMBLE_USE_ESP_TIMER) -#ifdef CONFIG_BT_NIMBLE_EXT_ADV // Workaround for PlatformIO build flags causing redefinition warnings -# undef MYNEWT_VAL_BLE_EXT_ADV -# define MYNEWT_VAL_BLE_EXT_ADV (CONFIG_BT_NIMBLE_EXT_ADV) -#endif +# ifdef CONFIG_BT_NIMBLE_EXT_ADV // Workaround for PlatformIO build flags causing redefinition warnings +# undef MYNEWT_VAL_BLE_EXT_ADV +# define MYNEWT_VAL_BLE_EXT_ADV (CONFIG_BT_NIMBLE_EXT_ADV) +# endif #else // !ESP_PLATFORM # if defined(NRF51)