Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 50 additions & 55 deletions examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* and communicate using the Arduino Stream interface.
*
* This allows you to use familiar methods like print(), println(),
* read(), and available() over BLE, similar to how you would use Serial.
* and write() over BLE, similar to how you would use Serial.
*
* This example connects to the NimBLE_Stream_Server example.
* This example connects to the NimBLE_Stream_Server example using the Nordic UART
* Service (NUS) with separate TX and RX characteristics.
*
* Created: November 2025
* Author: NimBLE-Arduino Contributors
Expand All @@ -16,39 +17,33 @@
#include <Arduino.h>
#include <NimBLEDevice.h>

// Service and Characteristic UUIDs (must match the server)
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
// Nordic UART Service (NUS) UUIDs (must match the server)
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: client subscribes here
#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: client writes here

// Create the stream client instance
/**
* Stream for sending data to the server.
* Attached to the server's RX characteristic (6E400002, WRITE_NR).
* Data received from the server arrives via the onServerNotify() callback below.
*/
NimBLEStreamClient bleStream;

struct RxOverflowStats {
uint32_t droppedOld{0};
uint32_t droppedNew{0};
};

RxOverflowStats g_rxOverflowStats;
uint32_t scanTime = 5000; // Scan duration in milliseconds

NimBLEStream::RxOverflowAction onRxOverflow(const uint8_t* data, size_t len, void* userArg) {
auto* stats = static_cast<RxOverflowStats*>(userArg);
if (stats) {
stats->droppedOld++;
}

// For status/telemetry streams, prioritize newest packets.
(void)data;
(void)len;
return NimBLEStream::DROP_OLDER_DATA;
}

// Connection state variables
static bool doConnect = false;
static bool connected = false;
static const NimBLEAdvertisedDevice* pServerDevice = nullptr;
static NimBLEClient* pClient = nullptr;

/** Callback invoked when the server sends a notification on its TX characteristic (6E400003) */
void onServerNotify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify) {
Serial.print("Received from server: ");
Serial.write(pData, len);
Serial.println();
}

/** Scan callbacks to find the server */
class ScanCallbacks : public NimBLEScanCallbacks {
void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
Expand Down Expand Up @@ -116,7 +111,7 @@ bool connectToServer() {

Serial.println("Connected! Discovering services...");

// Get the service and characteristic
// Get the service
NimBLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID);
if (!pRemoteService) {
Serial.println("Failed to find our service UUID");
Expand All @@ -125,25 +120,45 @@ bool connectToServer() {
}
Serial.println("Found the stream service");

NimBLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID);
if (!pRemoteCharacteristic) {
Serial.println("Failed to find our characteristic UUID");
// Get the server's RX characteristic -- client writes here (our TX path)
NimBLERemoteCharacteristic* pRxChar = pRemoteService->getCharacteristic(RX_CHAR_UUID);
if (!pRxChar) {
Serial.println("Failed to find RX characteristic");
pClient->disconnect();
return false;
}
Serial.println("Found the RX characteristic");

// Get the server's TX characteristic -- client subscribes here (our RX path)
NimBLERemoteCharacteristic* pTxChar = pRemoteService->getCharacteristic(TX_CHAR_UUID);
if (!pTxChar) {
Serial.println("Failed to find TX characteristic");
pClient->disconnect();
return false;
}
Serial.println("Found the stream characteristic");
Serial.println("Found the TX characteristic");

/**
* Initialize the stream client with the remote characteristic
* subscribeNotify=true means we'll receive notifications in the RX buffer
* Initialize the stream with the server's RX characteristic for writing (our TX).
* The RX characteristic (6E400002) supports WRITE but not NOTIFY, so subscribeNotify
* must be false. Notifications are handled separately on the TX characteristic below.
*/
if (!bleStream.begin(pRemoteCharacteristic, true)) {
if (!bleStream.begin(pRxChar, false)) {
Serial.println("Failed to initialize BLE stream!");
pClient->disconnect();
return false;
}

bleStream.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats);
/**
* Subscribe to the server's TX characteristic (6E400003) to receive notifications.
* Received data is handled directly in the onServerNotify callback.
*/
if (!pTxChar->subscribe(true, onServerNotify)) {
Serial.println("Failed to subscribe to server TX characteristic");
bleStream.end();
pClient->disconnect();
return false;
}

Serial.println("BLE Stream initialized successfully!");
connected = true;
Expand Down Expand Up @@ -171,14 +186,6 @@ void setup() {
}

void loop() {
static uint32_t lastDroppedOld = 0;
static uint32_t lastDroppedNew = 0;
if (g_rxOverflowStats.droppedOld != lastDroppedOld || g_rxOverflowStats.droppedNew != lastDroppedNew) {
lastDroppedOld = g_rxOverflowStats.droppedOld;
lastDroppedNew = g_rxOverflowStats.droppedNew;
Serial.printf("RX overflow handled (drop-old=%lu, drop-new=%lu)\n", lastDroppedOld, lastDroppedNew);
}

// If we found a server, try to connect
if (doConnect) {
doConnect = false;
Expand All @@ -191,20 +198,8 @@ void loop() {
}
}

// If we're connected, demonstrate the stream interface
// If we're connected, use the stream to send data
if (connected && bleStream) {
// Check if we received any data from the server
if (bleStream.available()) {
Serial.print("Received from server: ");

// Read all available data (just like Serial.read())
while (bleStream.available()) {
char c = bleStream.read();
Serial.write(c);
}
Serial.println();
}

// Send a message every 5 seconds using Stream methods
static unsigned long lastSend = 0;
if (millis() - lastSend > 5000) {
Expand All @@ -218,7 +213,7 @@ void loop() {
Serial.println("Sent data to server via BLE stream");
}

// You can also read from Serial and send over BLE
// Read from Serial and send over BLE
if (Serial.available()) {
Serial.println("Reading from Serial and sending via BLE:");
while (Serial.available()) {
Expand Down
29 changes: 16 additions & 13 deletions examples/NimBLE_Stream_Client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ This example demonstrates how to use the `NimBLEStreamClient` class to connect t

## Features

- Uses Arduino Stream interface (print, println, read, available, etc.)
- Uses Arduino Stream interface (print, println, write, etc.)
- Automatic server discovery and connection
- Bidirectional communication
- Buffered TX/RX using ring buffers
- Bidirectional communication using the Nordic UART Service (NUS)
- TX: `NimBLEStreamClient` writes to the server's RX characteristic (6E400002)
- RX: direct notification callback subscribed to the server's TX characteristic (6E400003)
- Automatic reconnection on disconnect
- Compatible with the NimBLE_Stream_Server example and NUS terminal apps
- 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()`
1. Scans for BLE devices advertising the NUS service UUID
2. Connects to the server and discovers the TX and RX characteristics
3. Initializes `NimBLEStreamClient` with the server's RX characteristic for writing (our TX path)
4. Subscribes directly to the server's TX characteristic to receive notifications (our RX path)
5. Uses familiar Stream methods like `print()`, `println()`, and `write()` to send data

## Usage

Expand All @@ -30,24 +32,25 @@ This example demonstrates how to use the `NimBLEStreamClient` class to connect t
- Begin bidirectional communication
4. You can also type in the Serial monitor to send data to the server

## Service UUIDs
## Service UUIDs (Nordic UART Service)

Must match the server:
- Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- Characteristic: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
- TX Characteristic (server → client, client subscribes): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`
- RX Characteristic (client → server, client writes): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`

## Serial Monitor Output

The example displays:
- Server discovery progress
- Connection status
- All data received from the server
- All data received from the server (via notification callback)
- Confirmation of data sent to the server

## Testing

Run with NimBLE_Stream_Server to see bidirectional communication:
- Server sends periodic status messages
- Client sends periodic uptime messages
- Both echo data received from each other
- You can send data from either Serial monitor
- Server echoes data back to the client
- You can send data from the Serial monitor
89 changes: 49 additions & 40 deletions examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,36 @@
* This allows you to use familiar methods like print(), println(),
* read(), and available() over BLE, similar to how you would use Serial.
*
* Uses the Nordic UART Service (NUS) UUIDs with separate TX and RX characteristics
* for compatibility with NUS terminal apps (e.g. nRF UART, Serial Bluetooth Terminal).
*
* Created: November 2025
* Author: NimBLE-Arduino Contributors
*/

#include <Arduino.h>
#include <NimBLEDevice.h>

// Create the stream server instance
NimBLEStreamServer bleStream;
// Nordic UART Service (NUS) UUIDs
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Server TX: notify (server -> client)
#define RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Server RX: write (client -> server)

/**
* Two stream server instances:
* - bleStreamTx sends notifications to the client (server -> client).
* Backed by the TX characteristic (NOTIFY-only); TX buffer is enabled, RX buffer disabled.
* - bleStreamRx receives data written by the client (client -> server).
* Backed by the RX characteristic (WRITE-only); RX buffer is enabled, TX buffer disabled.
*
* NimBLEStreamServer::begin(pChr) automatically enables only the directions supported
* by the characteristic's properties, so no special configuration is needed.
*/
NimBLEStreamServer bleStreamTx;
NimBLEStreamServer bleStreamRx;

struct RxOverflowStats {
uint32_t droppedOld{0};
uint32_t droppedNew{0};
};

RxOverflowStats g_rxOverflowStats;
Expand All @@ -36,11 +53,6 @@ NimBLEStream::RxOverflowAction onRxOverflow(const uint8_t* data, size_t len, voi
return NimBLEStream::DROP_OLDER_DATA;
}

// Service and Characteristic UUIDs for the stream
// Using custom UUIDs - you can change these as needed
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"

/** Server callbacks to handle connection/disconnection events */
class ServerCallbacks : public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override {
Expand All @@ -66,32 +78,31 @@ void setup() {
/** Initialize NimBLE and set the device name */
NimBLEDevice::init("NimBLE-Stream");

/**
* Create the BLE server and set callbacks
* Note: The stream will create its own service and 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)
* Create the NUS service with two characteristics:
* - TX (6E400003): NOTIFY -- server sends data to the client
* - RX (6E400002): WRITE -- client sends data to the server
*/
NimBLEService* pSvc = pServer->createService(SERVICE_UUID);
NimBLECharacteristic* pTxChar = pSvc->createCharacteristic(TX_CHAR_UUID, NIMBLE_PROPERTY::NOTIFY);
NimBLECharacteristic* pRxChar = pSvc->createCharacteristic(RX_CHAR_UUID,
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR);

/**
* Pass each characteristic to its own NimBLEStreamServer instance.
* begin() checks the characteristic properties and enables only the supported
* direction: pTxChar (NOTIFY-only) enables the TX buffer; pRxChar (WRITE-only)
* enables the RX buffer.
*/
if (!bleStream.begin(NimBLEUUID(SERVICE_UUID),
NimBLEUUID(CHARACTERISTIC_UUID),
1024, // txBufSize
1024, // rxBufSize
false)) // secure
{
if (!bleStreamTx.begin(pTxChar) || !bleStreamRx.begin(pRxChar)) {
Serial.println("Failed to initialize BLE stream!");
return;
}

bleStream.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats);
bleStreamRx.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats);

/**
* Create advertising instance and add service UUID
Expand All @@ -110,39 +121,37 @@ void setup() {

void loop() {
static uint32_t lastDroppedOld = 0;
static uint32_t lastDroppedNew = 0;
if (g_rxOverflowStats.droppedOld != lastDroppedOld || g_rxOverflowStats.droppedNew != lastDroppedNew) {
if (g_rxOverflowStats.droppedOld != lastDroppedOld) {
lastDroppedOld = g_rxOverflowStats.droppedOld;
lastDroppedNew = g_rxOverflowStats.droppedNew;
Serial.printf("RX overflow handled (drop-old=%lu, drop-new=%lu)\n", lastDroppedOld, lastDroppedNew);
Serial.printf("RX overflow: %lu packets dropped\n", lastDroppedOld);
}

// Check if a client is subscribed (connected and listening)
if (bleStream.ready()) {
// bleStreamTx.ready() is true when a client has subscribed to the TX characteristic
if (bleStreamTx.ready()) {
// Send a message every 2 seconds using Stream methods
static unsigned long lastSend = 0;
if (millis() - lastSend > 2000) {
lastSend = millis();

// Using familiar Serial-like methods!
bleStream.print("Hello from BLE Server! Time: ");
bleStream.println(millis());
bleStreamTx.print("Hello from BLE Server! Time: ");
bleStreamTx.println(millis());

// You can also use printf
bleStream.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
bleStreamTx.printf("Free heap: %d bytes\n", ESP.getFreeHeap());

Serial.println("Sent data to client via BLE stream");
}

// Check if we received any data from the client
if (bleStream.available()) {
// Check if we received any data written by the client on the RX characteristic
if (bleStreamRx.available()) {
Serial.print("Received from client: ");

// Read all available data (just like Serial.read())
while (bleStream.available()) {
char c = bleStream.read();
Serial.write(c); // Echo to Serial
bleStream.write(c); // Echo back to BLE client
while (bleStreamRx.available()) {
char c = bleStreamRx.read();
Serial.write(c); // Echo to Serial
bleStreamTx.write(c); // Echo back to BLE client via TX notification
}
Serial.println();
}
Expand Down
Loading
Loading