Skip to content

Commit c141b96

Browse files
h2zerodoudar
andcommitted
Add BLE stream classes.
Co-authored-by: doudar <17362216+doudar@users.noreply.github.com>
1 parent a0a8db9 commit c141b96

21 files changed

Lines changed: 1954 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
name: Build with ESP-IDF ${{ matrix.idf_ver }} for ${{ matrix.idf_target }}
1313
runs-on: ubuntu-latest
1414
strategy:
15+
fail-fast: false
1516
matrix:
1617
# The version names here correspond to the versions of espressif/idf Docker image.
1718
# See https://hub.docker.com/r/espressif/idf/tags and
@@ -22,6 +23,9 @@ jobs:
2223
example:
2324
- NimBLE_Client
2425
- NimBLE_Server
26+
- NimBLE_Stream_Client
27+
- NimBLE_Stream_Server
28+
- NimBLE_Stream_Echo
2529
- Bluetooth_5/NimBLE_extended_client
2630
- Bluetooth_5/NimBLE_extended_server
2731
exclude:

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
docs/doxydocs
2-
dist
2+
dist
3+
.development
4+
_codeql_detected_source_root

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ idf_component_register(
6666
"src/NimBLEScan.cpp"
6767
"src/NimBLEServer.cpp"
6868
"src/NimBLEService.cpp"
69+
"src/NimBLEStream.cpp"
6970
"src/NimBLEUtils.cpp"
7071
"src/NimBLEUUID.cpp"
7172
REQUIRES
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# The following lines of boilerplate have to be in your project's
2+
# CMakeLists in this exact order for cmake to work correctly
3+
cmake_minimum_required(VERSION 3.5)
4+
5+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
6+
project(NimBLE_Stream_Client)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# NimBLE Stream Client Example
2+
3+
This example demonstrates how to use the `NimBLEStreamClient` class to connect to a BLE GATT server and communicate using the familiar Arduino Stream interface.
4+
5+
## Features
6+
7+
- Uses Arduino Stream interface (print, println, read, available, etc.)
8+
- Automatic server discovery and connection
9+
- Bidirectional communication
10+
- Buffered TX/RX using ring buffers
11+
- Automatic reconnection on disconnect
12+
- Similar usage to Serial communication
13+
14+
## How it Works
15+
16+
1. Scans for BLE devices advertising the target service UUID
17+
2. Connects to the server and discovers the stream characteristic
18+
3. Initializes `NimBLEStreamClient` with the remote characteristic
19+
4. Subscribes to notifications to receive data in the RX buffer
20+
5. Uses familiar Stream methods like `print()`, `println()`, `read()`, and `available()`
21+
22+
## Usage
23+
24+
1. Build and flash the NimBLE_Stream_Server example to one ESP32 using ESP-IDF (`idf.py build flash monitor`)
25+
2. Build and flash this client example to another ESP32 using ESP-IDF
26+
3. The client will automatically:
27+
- Scan for the server
28+
- Connect when found
29+
- Set up the stream interface
30+
- Begin bidirectional communication
31+
4. Open `idf.py monitor` on each board to observe stream traffic
32+
33+
## Service UUIDs
34+
35+
Must match the server:
36+
- Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
37+
- Characteristic: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
38+
39+
## Monitor Output
40+
41+
The example displays:
42+
- Server discovery progress
43+
- Connection status
44+
- All data received from the server
45+
- Confirmation of data sent to the server
46+
47+
## Testing
48+
49+
Run with NimBLE_Stream_Server to see bidirectional communication:
50+
- Server sends periodic status messages
51+
- Client sends periodic uptime messages
52+
- Both echo data received from each other
53+
- You can send data from either `idf.py monitor` session
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
set(COMPONENT_SRCS "main.cpp")
2+
set(COMPONENT_ADD_INCLUDEDIRS ".")
3+
4+
register_component()
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/**
2+
* NimBLE_Stream_Client Example:
3+
*
4+
* Demonstrates using NimBLEStreamClient to connect to a BLE GATT server
5+
* and communicate using the Stream-like interface.
6+
*
7+
* This example connects to the NimBLE_Stream_Server example.
8+
*/
9+
10+
#include <inttypes.h>
11+
#include <stdint.h>
12+
#include <stdio.h>
13+
14+
#include "esp_timer.h"
15+
#include "freertos/FreeRTOS.h"
16+
#include "freertos/task.h"
17+
18+
#include <NimBLEDevice.h>
19+
20+
// Service and Characteristic UUIDs (must match the server)
21+
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
22+
#define CHARACTERISTIC_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
23+
24+
// Create the stream client instance
25+
NimBLEStreamClient bleStream;
26+
27+
struct RxOverflowStats {
28+
uint32_t droppedOld{0};
29+
uint32_t droppedNew{0};
30+
};
31+
32+
RxOverflowStats g_rxOverflowStats;
33+
uint32_t scanTime = 5000; // Scan duration in milliseconds
34+
35+
NimBLEStream::RxOverflowAction onRxOverflow(const uint8_t* data, size_t len, void* userArg) {
36+
auto* stats = static_cast<RxOverflowStats*>(userArg);
37+
if (stats) {
38+
stats->droppedOld++;
39+
}
40+
41+
// For status/telemetry streams, prioritize newest packets.
42+
(void)data;
43+
(void)len;
44+
return NimBLEStream::DROP_OLDER_DATA;
45+
}
46+
47+
static uint64_t millis() {
48+
return esp_timer_get_time() / 1000ULL;
49+
}
50+
51+
// Connection state variables
52+
static bool doConnect = false;
53+
static bool connected = false;
54+
static const NimBLEAdvertisedDevice* pServerDevice = nullptr;
55+
static NimBLEClient* pClient = nullptr;
56+
57+
/** Scan callbacks to find the server */
58+
class ScanCallbacks : public NimBLEScanCallbacks {
59+
void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
60+
printf("Advertised Device: %s\n", advertisedDevice->toString().c_str());
61+
62+
// Check if this device advertises our service.
63+
if (advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) {
64+
printf("Found our stream server!\n");
65+
pServerDevice = advertisedDevice;
66+
NimBLEDevice::getScan()->stop();
67+
doConnect = true;
68+
}
69+
}
70+
71+
void onScanEnd(const NimBLEScanResults& results, int reason) override {
72+
(void)results;
73+
(void)reason;
74+
printf("Scan ended\n");
75+
if (!doConnect && !connected) {
76+
printf("Server not found, restarting scan...\n");
77+
NimBLEDevice::getScan()->start(scanTime, false, true);
78+
}
79+
}
80+
} scanCallbacks;
81+
82+
/** Client callbacks for connection/disconnection events */
83+
class ClientCallbacks : public NimBLEClientCallbacks {
84+
void onConnect(NimBLEClient* pClient) override {
85+
printf("Connected to server\n");
86+
// Update connection parameters for better throughput.
87+
pClient->updateConnParams(12, 24, 0, 200);
88+
}
89+
90+
void onDisconnect(NimBLEClient* pClient, int reason) override {
91+
(void)pClient;
92+
printf("Disconnected from server, reason: %d\n", reason);
93+
connected = false;
94+
bleStream.end();
95+
96+
// Restart scanning.
97+
printf("Restarting scan...\n");
98+
NimBLEDevice::getScan()->start(scanTime, false, true);
99+
}
100+
} clientCallbacks;
101+
102+
/** Connect to the BLE Server and set up the stream */
103+
bool connectToServer() {
104+
printf("Connecting to: %s\n", pServerDevice->getAddress().toString().c_str());
105+
106+
// Create or reuse a client.
107+
pClient = NimBLEDevice::getClientByPeerAddress(pServerDevice->getAddress());
108+
if (!pClient) {
109+
pClient = NimBLEDevice::createClient();
110+
if (!pClient) {
111+
printf("Failed to create client\n");
112+
return false;
113+
}
114+
pClient->setClientCallbacks(&clientCallbacks, false);
115+
pClient->setConnectionParams(12, 24, 0, 200);
116+
pClient->setConnectTimeout(5000);
117+
}
118+
119+
// Connect to the remote BLE Server.
120+
if (!pClient->connect(pServerDevice)) {
121+
printf("Failed to connect to server\n");
122+
return false;
123+
}
124+
125+
printf("Connected! Discovering services...\n");
126+
127+
// Get the service and characteristic.
128+
NimBLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID);
129+
if (!pRemoteService) {
130+
printf("Failed to find our service UUID\n");
131+
pClient->disconnect();
132+
return false;
133+
}
134+
printf("Found the stream service\n");
135+
136+
NimBLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID);
137+
if (!pRemoteCharacteristic) {
138+
printf("Failed to find our characteristic UUID\n");
139+
pClient->disconnect();
140+
return false;
141+
}
142+
printf("Found the stream characteristic\n");
143+
144+
// subscribeNotify=true means notifications are stored in the RX buffer.
145+
if (!bleStream.begin(pRemoteCharacteristic, true)) {
146+
printf("Failed to initialize BLE stream!\n");
147+
pClient->disconnect();
148+
return false;
149+
}
150+
151+
bleStream.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats);
152+
153+
printf("BLE Stream initialized successfully!\n");
154+
connected = true;
155+
return true;
156+
}
157+
158+
extern "C" void app_main(void) {
159+
printf("Starting NimBLE Stream Client\n");
160+
161+
/** Initialize NimBLE */
162+
NimBLEDevice::init("NimBLE-StreamClient");
163+
164+
// Create the BLE scan instance and set callbacks.
165+
NimBLEScan* pScan = NimBLEDevice::getScan();
166+
pScan->setScanCallbacks(&scanCallbacks, false);
167+
pScan->setActiveScan(true);
168+
169+
// Start scanning for the server.
170+
printf("Scanning for BLE Stream Server...\n");
171+
pScan->start(scanTime, false, true);
172+
173+
uint32_t lastDroppedOld = 0;
174+
uint32_t lastDroppedNew = 0;
175+
uint64_t lastSend = 0;
176+
177+
for (;;) {
178+
if (g_rxOverflowStats.droppedOld != lastDroppedOld || g_rxOverflowStats.droppedNew != lastDroppedNew) {
179+
lastDroppedOld = g_rxOverflowStats.droppedOld;
180+
lastDroppedNew = g_rxOverflowStats.droppedNew;
181+
printf("RX overflow handled (drop-old=%" PRIu32 ", drop-new=%" PRIu32 ")\n", lastDroppedOld, lastDroppedNew);
182+
}
183+
184+
// If we found a server, try to connect.
185+
if (doConnect) {
186+
doConnect = false;
187+
if (connectToServer()) {
188+
printf("Stream ready for communication!\n");
189+
} else {
190+
printf("Failed to connect to server, restarting scan...\n");
191+
pServerDevice = nullptr;
192+
NimBLEDevice::getScan()->start(scanTime, false, true);
193+
}
194+
}
195+
196+
// If connected, demonstrate stream communication.
197+
if (connected && bleStream) {
198+
if (bleStream.available()) {
199+
printf("Received from server: ");
200+
while (bleStream.available()) {
201+
char c = bleStream.read();
202+
putchar(c);
203+
}
204+
printf("\n");
205+
}
206+
207+
uint64_t now = millis();
208+
if (now - lastSend > 5000) {
209+
lastSend = now;
210+
bleStream.printf("Hello from client! Uptime: %" PRIu64 " seconds\n", now / 1000);
211+
printf("Sent data to server via BLE stream\n");
212+
}
213+
}
214+
215+
vTaskDelay(pdMS_TO_TICKS(10));
216+
}
217+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Override some defaults so BT stack is enabled
2+
# in this example
3+
4+
#
5+
# BT config
6+
#
7+
CONFIG_BT_ENABLED=y
8+
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
9+
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
10+
CONFIG_BTDM_CTRL_MODE_BTDM=n
11+
CONFIG_BT_BLUEDROID_ENABLED=n
12+
CONFIG_BT_NIMBLE_ENABLED=y
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# The following lines of boilerplate have to be in your project's
2+
# CMakeLists in this exact order for cmake to work correctly
3+
cmake_minimum_required(VERSION 3.5)
4+
5+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
6+
project(NimBLE_Stream_Echo)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# NimBLE Stream Echo Example
2+
3+
This is the simplest example demonstrating `NimBLEStreamServer`. It echoes back any data received from BLE clients.
4+
5+
## Features
6+
7+
- Minimal code showing essential NimBLE Stream usage
8+
- Echoes all received data back to the client
9+
- Uses default service and characteristic UUIDs
10+
- Perfect starting point for learning the Stream interface
11+
12+
## How it Works
13+
14+
1. Initializes BLE with minimal configuration
15+
2. Creates a stream server with default UUIDs
16+
3. Waits for client connection and data
17+
4. Echoes received data back to the client
18+
5. Displays received data in the ESP-IDF monitor output
19+
20+
## Default UUIDs
21+
22+
- Service: `0xc0de`
23+
- Characteristic: `0xfeed`
24+
25+
## Usage
26+
27+
1. Build and flash this example to your ESP32 using ESP-IDF (`idf.py build flash monitor`)
28+
2. Connect with a BLE client app (nRF Connect, Serial Bluetooth Terminal, etc.)
29+
3. Find the service `0xc0de` and characteristic `0xfeed`
30+
4. Subscribe to notifications
31+
5. Write data to the characteristic
32+
6. The data will be echoed back and displayed in `idf.py monitor`
33+
34+
## Good For
35+
36+
- Learning the basic NimBLE Stream API
37+
- Testing BLE connectivity
38+
- Starting point for custom applications
39+
- Understanding Stream read/write operations

0 commit comments

Comments
 (0)