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..cd9e7ca60 --- /dev/null +++ b/examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino @@ -0,0 +1,215 @@ +/** + * 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 +#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.deinit(); + + // 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.init(pRemoteCharacteristic, true)) { + Serial.println("Failed to initialize BLE stream!"); + pClient->disconnect(); + return false; + } + + /** Start the stream (begins internal buffers and tasks) */ + if (!bleStream.begin()) { + Serial.println("Failed to start BLE stream!"); + bleStream.deinit(); + 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"); + // Scan will restart automatically + } + } + + // 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..5b91330fb --- /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 FreeRTOS 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..66d818e43 --- /dev/null +++ b/examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino @@ -0,0 +1,52 @@ +/** + * 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 +#include + +NimBLEStreamServer bleStream; + +void setup() { + Serial.begin(115200); + Serial.println("NimBLE Stream Echo Server"); + + // Initialize BLE + NimBLEDevice::init("BLE-Echo"); + NimBLEDevice::createServer(); + + // Initialize stream with default UUIDs, allow writes + bleStream.init(NimBLEUUID(uint16_t(0xc0de)), // Service UUID + NimBLEUUID(uint16_t(0xfeed)), // Characteristic UUID + true, // canWrite + false); // secure + bleStream.begin(); + + // 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.hasSubscriber() && 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..6257c54ea --- /dev/null +++ b/examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino @@ -0,0 +1,130 @@ +/** + * 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 +#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 + * - canWrite: true (allows clients to write data to us) + * - secure: false (no encryption required - set to true for secure connections) + */ + if (!bleStream.init(NimBLEUUID(SERVICE_UUID), + NimBLEUUID(CHARACTERISTIC_UUID), + true, // canWrite - allow receiving data + false)) // secure + { + Serial.println("Failed to initialize BLE stream!"); + return; + } + + /** Start the stream (begins internal buffers and tasks) */ + if (!bleStream.begin()) { + Serial.println("Failed to start 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.hasSubscriber()) { + + // 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..5e0a582c7 --- /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 FreeRTOS 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/examples/STREAM_EXAMPLES.md b/examples/STREAM_EXAMPLES.md new file mode 100644 index 000000000..34f6a9dcc --- /dev/null +++ b/examples/STREAM_EXAMPLES.md @@ -0,0 +1,145 @@ +# NimBLE Stream Examples + +This document describes the new Stream-based examples that demonstrate using BLE characteristics with the familiar Arduino Stream interface. + +## Overview + +The NimBLE Stream classes (`NimBLEStreamServer` and `NimBLEStreamClient`) provide a simple way to use BLE characteristics like serial ports. You can use familiar methods like `print()`, `println()`, `read()`, and `available()` just like you would with `Serial`. + +## Available Examples + +### 1. NimBLE_Stream_Echo +**Difficulty:** Beginner +**Purpose:** Introduction to the Stream API + +This is the simplest example, perfect for getting started. It creates a BLE server that echoes back any data it receives. Uses default UUIDs for quick setup. + +**Key Features:** +- Minimal setup code +- Simple echo functionality +- Good starting point for learning + +**Use Case:** Testing BLE connectivity, learning the basics + +--- + +### 2. NimBLE_Stream_Server +**Difficulty:** Intermediate +**Purpose:** Full-featured server implementation + +A complete BLE server example that demonstrates all the major features of `NimBLEStreamServer`. Shows connection management, bidirectional communication, and proper server setup. + +**Key Features:** +- Custom service and characteristic UUIDs +- Connection callbacks +- Periodic message sending +- Echo functionality +- MTU negotiation +- Connection parameter updates + +**Use Case:** Building custom BLE services, wireless data logging, remote monitoring + +--- + +### 3. NimBLE_Stream_Client +**Difficulty:** Intermediate +**Purpose:** Full-featured client implementation + +Demonstrates how to scan for, connect to, and communicate with a BLE server using `NimBLEStreamClient`. Pairs with the NimBLE_Stream_Server example. + +**Key Features:** +- Automatic server discovery +- Connection management +- Reconnection on disconnect +- Bidirectional communication +- Serial passthrough (type in Serial monitor to send via BLE) + +**Use Case:** Building BLE client applications, data collection from BLE devices + +## Quick Start + +### Running the Echo Example + +1. Upload `NimBLE_Stream_Echo` to your ESP32 +2. Open Serial monitor (115200 baud) +3. Use a BLE app (like nRF Connect) to connect +4. Find service UUID `0xc0de`, characteristic `0xfeed` +5. Subscribe to notifications +6. Write data to see it echoed back + +### Running Server + Client Examples + +1. Upload `NimBLE_Stream_Server` to one ESP32 +2. Upload `NimBLE_Stream_Client` to another ESP32 +3. Open Serial monitors for both (115200 baud) +4. Client will automatically find and connect to server +5. Observe bidirectional communication +6. Type in either Serial monitor to send data + +## Common Use Cases + +- **Wireless Serial Communication:** Replace physical serial connections with BLE +- **Data Logging:** Stream sensor data to a mobile device or another microcontroller +- **Remote Control:** Send commands between devices using familiar print/println +- **Debugging:** Use BLE as a wireless alternative to USB serial debugging +- **IoT Applications:** Simple data exchange between ESP32 devices + +## Stream Interface Methods + +The examples demonstrate these familiar methods: + +**Output (Sending Data):** +- `print(value)` - Print data without newline +- `println(value)` - Print data with newline +- `printf(format, ...)` - Formatted output +- `write(data)` - Write raw bytes + +**Input (Receiving Data):** +- `available()` - Check if data is available +- `read()` - Read a single byte +- `peek()` - Preview next byte without removing it + +**Status:** +- `hasSubscriber()` - Check if a client is connected (server only) +- `ready()` or `operator bool()` - Check if stream is ready + +## Configuration + +Both server and client support configuration before calling `begin()`: + +```cpp +bleStream.setTxBufSize(2048); // TX buffer size +bleStream.setRxBufSize(2048); // RX buffer size +bleStream.setTxTaskStackSize(4096); // Task stack size +bleStream.setTxTaskPriority(1); // Task priority +``` + +## Compatibility + +These examples work with: +- ESP32 (all variants: ESP32, ESP32-C3, ESP32-S3, etc.) +- Nordic chips (with n-able Arduino core) +- Any BLE client supporting GATT characteristics with notifications + +## Additional Resources + +- [Arduino Stream Reference](https://www.arduino.cc/reference/en/language/functions/communication/stream/) +- [NimBLE-Arduino Documentation](https://h2zero.github.io/NimBLE-Arduino/) +- Each example includes a detailed README.md file + +## Troubleshooting + +**Client can't find server:** +- Check that both devices are powered on +- Verify UUIDs match between server and client +- Ensure server advertising is started + +**Data not received:** +- Verify client has subscribed to notifications +- Check that buffers aren't full (increase buffer sizes) +- Ensure both `init()` and `begin()` were called + +**Connection drops:** +- Check for interference +- Reduce connection interval if needed +- Ensure devices are within BLE range diff --git a/src/NimBLEStream.cpp b/src/NimBLEStream.cpp new file mode 100644 index 000000000..8a74db0fc --- /dev/null +++ b/src/NimBLEStream.cpp @@ -0,0 +1,574 @@ +/* + * 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. + */ + +#ifdef ESP_PLATFORM +# include "NimBLEStream.h" +# if CONFIG_BT_NIMBLE_ENABLED && (MYNEWT_VAL(BLE_ROLE_PERIPHERAL) || MYNEWT_VAL(BLE_ROLE_CENTRAL)) + +# include "NimBLEDevice.h" +# include "rom/uart.h" + +static const char* LOG_TAG = "NimBLEStream"; + +// 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 + +void NimBLEStream::txTask(void* arg) { + NimBLEStream* pStream = static_cast(arg); + for (;;) { + size_t itemSize = 0; + void* item = xRingbufferReceive(pStream->m_txBuf, &itemSize, portMAX_DELAY); + if (item) { + pStream->send(reinterpret_cast(item), itemSize); + vRingbufferReturnItem(pStream->m_txBuf, item); + } + } +} + +bool NimBLEStream::begin() { + if (m_txBuf || m_rxBuf || m_txTask) { + NIMBLE_UART_LOGW(LOG_TAG, "Already initialized"); + return true; + } + + if (m_txBufSize) { + m_txBuf = xRingbufferCreate(m_txBufSize, RINGBUF_TYPE_BYTEBUF); + if (!m_txBuf) { + NIMBLE_UART_LOGE(LOG_TAG, "Failed to create TX ringbuffer"); + return false; + } + } + + if (m_rxBufSize) { + m_rxBuf = xRingbufferCreate(m_rxBufSize, RINGBUF_TYPE_BYTEBUF); + if (!m_rxBuf) { + NIMBLE_UART_LOGE(LOG_TAG, "Failed to create RX ringbuffer"); + if (m_txBuf) { + vRingbufferDelete(m_txBuf); + m_txBuf = nullptr; + } + return false; + } + } + + if (xTaskCreate(txTask, "NimBLEStreamTx", m_txTaskStackSize, this, m_txTaskPriority, &m_txTask) != pdPASS) { + NIMBLE_UART_LOGE(LOG_TAG, "Failed to create stream tx task"); + if (m_rxBuf) { + vRingbufferDelete(m_rxBuf); + m_rxBuf = nullptr; + } + if (m_txBuf) { + vRingbufferDelete(m_txBuf); + m_txBuf = nullptr; + } + return false; + } + + return true; +} + +bool NimBLEStream::end() { + // Release any buffered RX item + if (m_rxState.item && m_rxBuf) { + vRingbufferReturnItem(m_rxBuf, m_rxState.item); + m_rxState.item = nullptr; + } + m_rxState.itemSize = 0; + m_rxState.offset = 0; + + if (m_txTask) { + vTaskDelete(m_txTask); + m_txTask = nullptr; + } + + if (m_txBuf) { + vRingbufferDelete(m_txBuf); + m_txBuf = nullptr; + } + + if (m_rxBuf) { + vRingbufferDelete(m_rxBuf); + m_rxBuf = nullptr; + } + + return true; +} + +size_t NimBLEStream::write(const uint8_t* data, size_t len) { + if (!m_txBuf || !data || len == 0) { + return 0; + } + + ble_npl_time_t timeout = 0; + ble_npl_time_ms_to_ticks(getTimeout(), &timeout); + size_t chunk = std::min(len, xRingbufferGetCurFreeSize(m_txBuf)); + if (xRingbufferSend(m_txBuf, data, chunk, static_cast(timeout)) != pdTRUE) { + return 0; + } + return chunk; +} + +size_t NimBLEStream::availableForWrite() const { + return m_txBuf ? xRingbufferGetCurFreeSize(m_txBuf) : 0; +} + +void NimBLEStream::flush(uint32_t timeout_ms) { + if (!m_txBuf) { + return; + } + + ble_npl_time_t deadline = timeout_ms > 0 ? ble_npl_time_get() + ble_npl_time_ms_to_ticks32(timeout_ms) : 0; + + // Wait until TX ring is drained + while (m_txBuf && xRingbufferGetCurFreeSize(m_txBuf) < m_txBufSize) { + if (deadline > 0 && ble_npl_time_get() >= deadline) { + break; + } + ble_npl_time_delay(ble_npl_time_ms_to_ticks32(1)); + } +} + +int NimBLEStream::available() { + if (!m_rxBuf) { + NIMBLE_UART_LOGE(LOG_TAG, "Invalid RX buffer"); + return 0; + } + + // Count buffered RX item remainder + size_t buffered = m_rxState.itemSize > m_rxState.offset ? m_rxState.itemSize - m_rxState.offset : 0; + + // Query items in RX ring + UBaseType_t waiting = 0; + vRingbufferGetInfo(m_rxBuf, nullptr, nullptr, nullptr, nullptr, &waiting); + + return static_cast(buffered + waiting); +} + +int NimBLEStream::read() { + if (!m_rxBuf) { + return -1; + } + + // Return from buffered item if available + if (m_rxState.item && m_rxState.offset < m_rxState.itemSize) { + uint8_t byte = m_rxState.item[m_rxState.offset++]; + + // Release item if we've consumed it all + if (m_rxState.offset >= m_rxState.itemSize) { + vRingbufferReturnItem(m_rxBuf, m_rxState.item); + m_rxState.item = nullptr; + m_rxState.itemSize = 0; + m_rxState.offset = 0; + } + + return static_cast(byte); + } + + // Fetch next item from ringbuffer + size_t itemSize = 0; + uint8_t* item = static_cast(xRingbufferReceive(m_rxBuf, &itemSize, 0)); + if (!item || itemSize == 0) { + return -1; + } + + // Store in buffer state and return first byte + m_rxState.item = item; + m_rxState.itemSize = itemSize; + m_rxState.offset = 1; // Already consumed first byte + + return static_cast(item[0]); +} + +int NimBLEStream::peek() { + if (!m_rxBuf) { + return -1; + } + + // Return from buffered item if available + if (m_rxState.item && m_rxState.offset < m_rxState.itemSize) { + return static_cast(m_rxState.item[m_rxState.offset]); + } + + // Fetch next item from ringbuffer if not already buffered + if (!m_rxState.item) { + size_t itemSize = 0; + uint8_t* item = static_cast(xRingbufferReceive(m_rxBuf, &itemSize, 0)); + if (!item || itemSize == 0) { + return -1; + } + + // Store in buffer state + m_rxState.item = item; + m_rxState.itemSize = itemSize; + m_rxState.offset = 0; + } + + return static_cast(m_rxState.item[m_rxState.offset]); +} + +size_t NimBLEStream::read(uint8_t* buffer, size_t len) { + if (!m_rxBuf || !buffer || len == 0) { + return 0; + } + + size_t total = 0; + + // First, consume any buffered RX item remainder + if (m_rxState.item && m_rxState.offset < m_rxState.itemSize) { + size_t available = m_rxState.itemSize - m_rxState.offset; + size_t copyLen = std::min(len, available); + memcpy(buffer, m_rxState.item + m_rxState.offset, copyLen); + m_rxState.offset += copyLen; + total += copyLen; + + // Release item if fully consumed + if (m_rxState.offset >= m_rxState.itemSize) { + vRingbufferReturnItem(m_rxBuf, m_rxState.item); + m_rxState.item = nullptr; + m_rxState.itemSize = 0; + m_rxState.offset = 0; + } + + if (total >= len) { + return total; + } + } + + // Drain additional RX ringbuffer items + while (total < len) { + size_t itemSize = 0; + uint8_t* item = static_cast(xRingbufferReceive(m_rxBuf, &itemSize, 0)); + if (!item || itemSize == 0) { + break; + } + + size_t copyLen = std::min(len - total, itemSize); + memcpy(buffer + total, item, copyLen); + total += copyLen; + + // If we didn't consume the entire item, buffer it + if (copyLen < itemSize) { + m_rxState.item = item; + m_rxState.itemSize = itemSize; + m_rxState.offset = copyLen; + } else { + // Item fully consumed + vRingbufferReturnItem(m_rxBuf, item); + } + } + + return total; +} + +size_t NimBLEStream::pushRx(const uint8_t* data, size_t len) { + if (!m_rxBuf || !data || len == 0) { + NIMBLE_UART_LOGE(LOG_TAG, "Invalid RX buffer or data"); + return 0; + } + + if (xRingbufferSend(m_rxBuf, data, len, 0) != pdTRUE) { + NIMBLE_UART_LOGE(LOG_TAG, "RX buffer full, dropping %zu bytes", len); + return 0; + } + return len; +} + +# if MYNEWT_VAL(BLE_ROLE_PERIPHERAL) +bool NimBLEStreamServer::init(const NimBLEUUID& svcUuid, const NimBLEUUID& chrUuid, bool canWrite, bool secure) { + if (!NimBLEDevice::isInitialized()) { + NIMBLE_UART_LOGE(LOG_TAG, "NimBLEDevice not initialized"); + return false; + } + + NimBLEServer* pServer = NimBLEDevice::getServer(); + if (!pServer) { + pServer = NimBLEDevice::createServer(); + } + + NimBLEService* pSvc = pServer->getServiceByUUID(svcUuid); + if (!pSvc) { + pSvc = pServer->createService(svcUuid); + } + + if (!pSvc) { + NIMBLE_UART_LOGE(LOG_TAG, "Failed to create service"); + return false; + } + + // Create characteristic with notify + write properties for bidirectional stream + uint32_t props = NIMBLE_PROPERTY::NOTIFY; + if (secure) { + props |= NIMBLE_PROPERTY::READ_ENC; + } + + if (canWrite) { + props |= NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR; + if (secure) { + props |= NIMBLE_PROPERTY::WRITE_ENC; + } + } else { + m_rxBufSize = 0; // disable RX if not writable + } + + m_pChr = pSvc->getCharacteristic(chrUuid); + if (!m_pChr) { + m_pChr = pSvc->createCharacteristic(chrUuid, props); + } + + if (!m_pChr) { + NIMBLE_UART_LOGE(LOG_TAG, "Failed to create characteristic"); + return false; + } + + m_pChr->setCallbacks(&m_charCallbacks); + return pSvc->start(); +} + +void NimBLEStreamServer::deinit() { + if (m_pChr) { + NimBLEService* pSvc = m_pChr->getService(); + if (pSvc) { + pSvc->removeCharacteristic(m_pChr, true); + } + m_pChr = nullptr; + } + NimBLEStream::end(); +} + +size_t NimBLEStreamServer::write(const uint8_t* data, size_t len) { + if (!m_pChr || len == 0 || !hasSubscriber()) { + 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); +} + +bool NimBLEStreamServer::send(const uint8_t* data, size_t len) { + if (!m_pChr || !len || !hasSubscriber()) { + return false; + } + + size_t offset = 0; + while (offset < len) { + size_t chunkLen = std::min(len - offset, getMaxLength()); + while (!m_pChr->notify(data + offset, chunkLen, getPeerHandle())) { + // Retry on ENOMEM (mbuf shortage) + if (m_rc == BLE_HS_ENOMEM || os_msys_num_free() <= 2) { + ble_npl_time_delay(ble_npl_time_ms_to_ticks32(8)); // wait for a minimum connection event time + continue; + } + return false; + } + + offset += chunkLen; + } + return true; +} + +void NimBLEStreamServer::ChrCallbacks::onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) { + // Push received data into RX buffer + auto val = pCharacteristic->getValue(); + if (val.size() > 0) { + m_parent->pushRx(val.data(), val.size()); + } + + if (m_userCallbacks) { + m_userCallbacks->onWrite(pCharacteristic, connInfo); + } +} + +void NimBLEStreamServer::ChrCallbacks::onSubscribe(NimBLECharacteristic* pCharacteristic, + NimBLEConnInfo& connInfo, + uint16_t subValue) { + // only one subscriber supported + if (m_peerHandle != BLE_HS_CONN_HANDLE_NONE && subValue) { + return; + } + + m_peerHandle = subValue ? connInfo.getConnHandle() : BLE_HS_CONN_HANDLE_NONE; + if (m_peerHandle != BLE_HS_CONN_HANDLE_NONE) { + m_maxLen = ble_att_mtu(m_peerHandle) - 3; + if (!m_parent->begin()) { + NIMBLE_UART_LOGE(LOG_TAG, "NimBLEStreamServer failed to begin"); + } + return; + } + + m_parent->end(); + if (m_userCallbacks) { + m_userCallbacks->onSubscribe(pCharacteristic, connInfo, subValue); + } +} + +void NimBLEStreamServer::ChrCallbacks::onStatus(NimBLECharacteristic* pCharacteristic, int code) { + m_parent->m_rc = code; + if (m_userCallbacks) { + m_userCallbacks->onStatus(pCharacteristic, code); + } +} + +# endif // MYNEWT_VAL(BLE_ROLE_PERIPHERAL) + +# if MYNEWT_VAL(BLE_ROLE_CENTRAL) +bool NimBLEStreamClient::init(NimBLERemoteCharacteristic* pChr, bool subscribe) { + if (!pChr) { + return false; + } + + m_pChr = pChr; + m_writeWithRsp = !pChr->canWriteNoResponse(); + + // Subscribe to notifications/indications for RX if requested + if (subscribe && (pChr->canNotify() || pChr->canIndicate())) { + using namespace std::placeholders; + if (!pChr->subscribe(pChr->canNotify(), std::bind(&NimBLEStreamClient::notifyCallback, this, _1, _2, _3, _4))) { + NIMBLE_UART_LOGE(LOG_TAG, "Failed to subscribe for notifications"); + } + } + + if (!subscribe) { + m_rxBufSize = 0; // disable RX if not subscribing + } + + return true; +} + +void NimBLEStreamClient::deinit() { + if (m_pChr && (m_pChr->canNotify() || m_pChr->canIndicate())) { + m_pChr->unsubscribe(); + } + NimBLEStream::end(); + m_pChr = nullptr; +} + +bool NimBLEStreamClient::send(const uint8_t* data, size_t len) { + if (!m_pChr || !data || len == 0) { + return false; + } + return m_pChr->writeValue(data, len, m_writeWithRsp); +} + +void NimBLEStreamClient::notifyCallback(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify) { + if (pData && len > 0) { + pushRx(pData, len); + } + + if (m_userNotifyCallback) { + m_userNotifyCallback(pChar, pData, len, isNotify); + } +} + +// UART logging support +int NimBLEStream::uart_log_printfv(const char* format, va_list arg) { + static char loc_buf[64]; + char* temp = loc_buf; + uint32_t len; + va_list copy; + va_copy(copy, arg); + len = vsnprintf(NULL, 0, format, copy); + va_end(copy); + if (len >= sizeof(loc_buf)) { + temp = (char*)malloc(len + 1); + if (temp == NULL) { + return 0; + } + } + + int wlen = vsnprintf(temp, len + 1, format, arg); + for (int i = 0; i < wlen; i++) { + uart_tx_one_char(temp[i]); + } + + if (len >= sizeof(loc_buf)) { + free(temp); + } + + return wlen; +} + +int NimBLEStream::uart_log_printf(const char* format, ...) { + int len; + va_list arg; + va_start(arg, format); + len = NimBLEStream::uart_log_printfv(format, arg); + va_end(arg); + return len; +} + +# endif // MYNEWT_VAL(BLE_ROLE_CENTRAL) +# endif // CONFIG_BT_NIMBLE_ENABLED && (MYNEWT_VAL(BLE_ROLE_PERIPHERAL) || MYNEWT_VAL(BLE_ROLE_CENTRAL)) +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/src/NimBLEStream.h b/src/NimBLEStream.h new file mode 100644 index 000000000..206b46979 --- /dev/null +++ b/src/NimBLEStream.h @@ -0,0 +1,239 @@ +/* + * 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. + */ + +#ifdef ESP_PLATFORM +# 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)) + +# include "NimBLEUUID.h" +# 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; + 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: + NimBLEStream() = default; + virtual ~NimBLEStream() { end(); } + + bool begin(); + bool end(); + + // Configure TX/RX buffer sizes and task parameters before begin() + void setTxBufSize(uint32_t size) { m_txBufSize = size; } + void setRxBufSize(uint32_t size) { m_rxBufSize = size; } + void setTxTaskStackSize(uint32_t size) { m_txTaskStackSize = size; } + void setTxTaskPriority(uint32_t priority) { m_txTaskPriority = priority; } + + // These logging macros exist to provide log output over UART so that the stream class can + // be used to redirect logs without causing recursion issues. + static int uart_log_printfv(const char* format, va_list arg); + static int uart_log_printf(const char* format, ...); + + // 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 ::value && !std::is_same::value, int>::type = 0> + size_t write(T data) { + return write(static_cast(data)); + } + + size_t availableForWrite() const; + void flush(uint32_t timeout_ms = 0); + + // Stream RX methods + virtual int available() override; + virtual int read() override; + virtual int peek() override; + + // Read up to len bytes into buffer (non-blocking) + size_t read(uint8_t* buffer, size_t len); + + // Serial-like helpers + bool ready() const { return isReady(); } + operator bool() const { return ready(); } + + using Print::write; + + protected: + static void txTask(void* arg); + virtual bool send(const uint8_t* data, size_t len) = 0; + virtual bool isReady() const = 0; + + // Push received data into RX ring (called by subclass callbacks) + size_t pushRx(const uint8_t* data, size_t len); + + // RX buffering state: avoids requeueing/fragmentation + struct RxState { + uint8_t* item{nullptr}; + size_t itemSize{0}; + size_t offset{0}; + }; + + RingbufHandle_t m_txBuf{nullptr}; + RingbufHandle_t m_rxBuf{nullptr}; + TaskHandle_t m_txTask{nullptr}; + uint32_t m_txTaskStackSize{4096}; + uint32_t m_txTaskPriority{tskIDLE_PRIORITY + 1}; + uint32_t m_txBufSize{1024}; + uint32_t m_rxBufSize{1024}; + + mutable RxState m_rxState{}; // Track current RX item to avoid requeueing +}; + +# if MYNEWT_VAL(BLE_ROLE_PERIPHERAL) +# include "NimBLECharacteristic.h" + +class NimBLEStreamServer : public NimBLEStream { + public: + NimBLEStreamServer() : m_charCallbacks(this) {} + ~NimBLEStreamServer() = default; + // non-copyable + NimBLEStreamServer(const NimBLEStreamServer&) = delete; + NimBLEStreamServer& operator=(const NimBLEStreamServer&) = delete; + + bool init(const NimBLEUUID& svcUuid = NimBLEUUID(uint16_t(0xc0de)), + const NimBLEUUID& chrUuid = NimBLEUUID(uint16_t(0xfeed)), + bool canWrite = false, + bool secure = false); + void deinit(); + size_t write(const uint8_t* data, size_t len) override; + uint16_t getPeerHandle() const { return m_charCallbacks.m_peerHandle; } + bool hasSubscriber() const { return m_charCallbacks.m_peerHandle != BLE_HS_CONN_HANDLE_NONE; } + size_t getMaxLength() const { return m_charCallbacks.m_maxLen; } + void setCallbacks(NimBLECharacteristicCallbacks* pCallbacks) { m_charCallbacks.m_userCallbacks = pCallbacks; } + + using NimBLEStream::write; // Inherit template write overloads + + private: + bool send(const uint8_t* data, size_t len) override; + bool isReady() const override { return hasSubscriber(); } + + struct ChrCallbacks : public NimBLECharacteristicCallbacks { + ChrCallbacks(NimBLEStreamServer* parent) + : m_parent(parent), m_userCallbacks(nullptr), m_peerHandle(BLE_HS_CONN_HANDLE_NONE), m_maxLen(0) {} + void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override; + void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue) override; + void onStatus(NimBLECharacteristic* pCharacteristic, int code) override; + // override this to avoid recursion when debug logs are enabled + void onStatus(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, int code) { + if (m_userCallbacks) { + m_userCallbacks->onStatus(pCharacteristic, connInfo, code); + } + } + + NimBLEStreamServer* m_parent; + NimBLECharacteristicCallbacks* m_userCallbacks; + uint16_t m_peerHandle; + uint16_t m_maxLen; + } m_charCallbacks; + + NimBLECharacteristic* m_pChr{nullptr}; + int m_rc{0}; +}; +# endif // BLE_ROLE_PERIPHERAL + +# if MYNEWT_VAL(BLE_ROLE_CENTRAL) +# include "NimBLERemoteCharacteristic.h" + +class NimBLEStreamClient : public NimBLEStream { + public: + NimBLEStreamClient() = default; + ~NimBLEStreamClient() = default; + // 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 init(NimBLERemoteCharacteristic* pChr, bool subscribeNotify = false); + void deinit(); + void setWriteWithResponse(bool useWithRsp) { m_writeWithRsp = useWithRsp; } + void setNotifyCallback(NimBLERemoteCharacteristic::notify_callback cb) { m_userNotifyCallback = cb; } + + using NimBLEStream::write; // Inherit template write overloads + + private: + bool send(const uint8_t* data, size_t len) override; + bool isReady() const override { return m_pChr != nullptr; } + void notifyCallback(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify); + + NimBLERemoteCharacteristic* m_pChr{nullptr}; + bool m_writeWithRsp{false}; + 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)) + +# if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 4 +# define NIMBLE_UART_LOGD(tag, format, ...) NimBLEStream::uart_log_printf("D %s: " format "\n", tag, ##__VA_ARGS__) +# else +# define NIMBLE_UART_LOGD(tag, format, ...) (void)tag +# endif + +# if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 3 +# define NIMBLE_UART_LOGI(tag, format, ...) NimBLEStream::uart_log_printf("I %s: " format "\n", tag, ##__VA_ARGS__) +# else +# define NIMBLE_UART_LOGI(tag, format, ...) (void)tag +# endif + +# if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 2 +# define NIMBLE_UART_LOGW(tag, format, ...) NimBLEStream::uart_log_printf("W %s: " format "\n", tag, ##__VA_ARGS__) +# else +# define NIMBLE_UART_LOGW(tag, format, ...) (void)tag +# endif + +# if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 1 +# define NIMBLE_UART_LOGE(tag, format, ...) NimBLEStream::uart_log_printf("E %s: " format "\n", tag, ##__VA_ARGS__) +# else +# define NIMBLE_UART_LOGE(tag, format, ...) (void)tag +# endif + +# endif // NIMBLE_CPP_STREAM_H +#endif // ESP_PLATFORM