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
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
name: Build with ESP-IDF ${{ matrix.idf_ver }} for ${{ matrix.idf_target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The version names here correspond to the versions of espressif/idf Docker image.
# See https://hub.docker.com/r/espressif/idf/tags and
Expand All @@ -22,6 +23,9 @@ jobs:
example:
- NimBLE_Client
- NimBLE_Server
- NimBLE_Stream_Client
- NimBLE_Stream_Server
- NimBLE_Stream_Echo
- Bluetooth_5/NimBLE_extended_client
- Bluetooth_5/NimBLE_extended_server
exclude:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
docs/doxydocs
dist
dist
.development
_codeql_detected_source_root
6 changes: 6 additions & 0 deletions examples/NimBLE_Stream_Client/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(NimBLE_Stream_Client)
53 changes: 53 additions & 0 deletions examples/NimBLE_Stream_Client/README.md
Original file line number Diff line number Diff line change
@@ -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. Build and flash the NimBLE_Stream_Server example to one ESP32 using ESP-IDF (`idf.py build flash monitor`)
2. Build and flash this client example to another ESP32 using ESP-IDF
3. The client will automatically:
- Scan for the server
- Connect when found
- Set up the stream interface
- Begin bidirectional communication
4. Open `idf.py monitor` on each board to observe stream traffic

## Service UUIDs

Must match the server:
- Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- Characteristic: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`

## 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 `idf.py monitor` session
4 changes: 4 additions & 0 deletions examples/NimBLE_Stream_Client/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
set(COMPONENT_SRCS "main.cpp")
set(COMPONENT_ADD_INCLUDEDIRS ".")

register_component()
217 changes: 217 additions & 0 deletions examples/NimBLE_Stream_Client/main/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* NimBLE_Stream_Client Example:
*
* Demonstrates using NimBLEStreamClient to connect to a BLE GATT server
* and communicate using the Stream-like interface.
*
* This example connects to the NimBLE_Stream_Server example.
*/

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.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"

// Create the stream client instance
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;
}

static uint64_t millis() {
return esp_timer_get_time() / 1000ULL;
}

// 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 {
printf("Advertised Device: %s\n", advertisedDevice->toString().c_str());

// Check if this device advertises our service.
if (advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) {
printf("Found our stream server!\n");
pServerDevice = advertisedDevice;
NimBLEDevice::getScan()->stop();
doConnect = true;
}
}

void onScanEnd(const NimBLEScanResults& results, int reason) override {
(void)results;
(void)reason;
printf("Scan ended\n");
if (!doConnect && !connected) {
printf("Server not found, restarting scan...\n");
NimBLEDevice::getScan()->start(scanTime, false, true);
}
}
} scanCallbacks;

/** Client callbacks for connection/disconnection events */
class ClientCallbacks : public NimBLEClientCallbacks {
void onConnect(NimBLEClient* pClient) override {
printf("Connected to server\n");
// Update connection parameters for better throughput.
pClient->updateConnParams(12, 24, 0, 200);
}

void onDisconnect(NimBLEClient* pClient, int reason) override {
(void)pClient;
printf("Disconnected from server, reason: %d\n", reason);
connected = false;
bleStream.end();

// Restart scanning.
printf("Restarting scan...\n");
NimBLEDevice::getScan()->start(scanTime, false, true);
}
} clientCallbacks;

/** Connect to the BLE Server and set up the stream */
bool connectToServer() {
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) {
printf("Failed to create client\n");
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)) {
printf("Failed to connect to server\n");
return false;
}

printf("Connected! Discovering services...\n");

// Get the service and characteristic.
NimBLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID);
if (!pRemoteService) {
printf("Failed to find our service UUID\n");
pClient->disconnect();
return false;
}
printf("Found the stream service\n");

NimBLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID);
if (!pRemoteCharacteristic) {
printf("Failed to find our characteristic UUID\n");
pClient->disconnect();
return false;
}
printf("Found the stream characteristic\n");

// subscribeNotify=true means notifications are stored in the RX buffer.
if (!bleStream.begin(pRemoteCharacteristic, true)) {
printf("Failed to initialize BLE stream!\n");
pClient->disconnect();
return false;
}

bleStream.setRxOverflowCallback(onRxOverflow, &g_rxOverflowStats);

printf("BLE Stream initialized successfully!\n");
connected = true;
return true;
}

extern "C" void app_main(void) {
printf("Starting NimBLE Stream Client\n");

/** Initialize NimBLE */
NimBLEDevice::init("NimBLE-StreamClient");

// Create the BLE scan instance and set callbacks.
NimBLEScan* pScan = NimBLEDevice::getScan();
pScan->setScanCallbacks(&scanCallbacks, false);
pScan->setActiveScan(true);

// Start scanning for the server.
printf("Scanning for BLE Stream Server...\n");
pScan->start(scanTime, false, true);

uint32_t lastDroppedOld = 0;
uint32_t lastDroppedNew = 0;
uint64_t lastSend = 0;

for (;;) {
if (g_rxOverflowStats.droppedOld != lastDroppedOld || g_rxOverflowStats.droppedNew != lastDroppedNew) {
lastDroppedOld = g_rxOverflowStats.droppedOld;
lastDroppedNew = g_rxOverflowStats.droppedNew;
printf("RX overflow handled (drop-old=%" PRIu32 ", drop-new=%" PRIu32 ")\n", lastDroppedOld, lastDroppedNew);
}

// If we found a server, try to connect.
if (doConnect) {
doConnect = false;
if (connectToServer()) {
printf("Stream ready for communication!\n");
} else {
printf("Failed to connect to server, restarting scan...\n");
pServerDevice = nullptr;
NimBLEDevice::getScan()->start(scanTime, false, true);
}
}

// If connected, demonstrate stream communication.
if (connected && bleStream) {
if (bleStream.available()) {
printf("Received from server: ");
while (bleStream.available()) {
char c = bleStream.read();
putchar(c);
}
printf("\n");
}

uint64_t now = esp_timer_get_time() / 1000ULL;
if (now - lastSend > 5000) {
lastSend = now;
bleStream.printf("Hello from client! Uptime: %" PRIu64 " seconds\n", now / 1000);
printf("Sent data to server via BLE stream\n");
}
}

vTaskDelay(pdMS_TO_TICKS(10));
}
}
12 changes: 12 additions & 0 deletions examples/NimBLE_Stream_Client/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Override some defaults so BT stack is enabled
# in this example

#
# BT config
#
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
6 changes: 6 additions & 0 deletions examples/NimBLE_Stream_Echo/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(NimBLE_Stream_Echo)
39 changes: 39 additions & 0 deletions examples/NimBLE_Stream_Echo/README.md
Original file line number Diff line number Diff line change
@@ -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 in the ESP-IDF monitor output

## Default UUIDs

- Service: `0xc0de`
- Characteristic: `0xfeed`

## Usage

1. Build and flash this example to your ESP32 using ESP-IDF (`idf.py build flash monitor`)
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 `idf.py monitor`

## Good For

- Learning the basic NimBLE Stream API
- Testing BLE connectivity
- Starting point for custom applications
- Understanding Stream read/write operations
4 changes: 4 additions & 0 deletions examples/NimBLE_Stream_Echo/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
set(COMPONENT_SRCS "main.cpp")
set(COMPONENT_ADD_INCLUDEDIRS ".")

register_component()
Loading
Loading