38 Commits

Author SHA1 Message Date
h2zero
0f5a184aa3 Add BLE stream classes. 2025-11-24 08:09:07 -07:00
h2zero
f216e95770 Add characteristic callbacks onStatus overload with conn info.
Adds a new overloaded callback to NimBLECharacteristicCallbacks for the notification/indication onStatus method that provides a NimBLEConnInfo reference.
2025-11-17 19:21:13 -07:00
h2zero
222f1590ed Refactor notify/indicate
This refactors the handling of sending notifications and indications for greater efficiency.
* Adds client subscription state tracking to NimBLECharacteristic rather than relying on the stack.
* Notifications/indications are now sent directly, no longer calling the callback to read the values.
  This avoids delays and flash writes in the stack, allowing for greater throughput.
2025-11-17 19:21:13 -07:00
srgg
149716a506 correct container byte size calculation to writeValue, notify, and indicate 2025-11-16 09:22:40 -07:00
srgg
4199c52af1 fix: correct byte size calculation for ATT values set from containers 2025-11-16 09:22:37 -07:00
h2zero
20158d62d0 [Bugfix] make sure the notify event is sent to server created clients 2025-10-24 13:19:42 -06:00
Quentin F
f6c8728ca3 Update 1.x_to2.x_migration_guide.md 2025-10-24 13:19:03 -06:00
h2zero
e0d3c4be39 Update workflows + add release publish 2025-10-24 13:09:46 -06:00
Guo-Rong
d163a9fdc6 Find client by handle during disconnect event.
If the peer has RPA enabled, searching by address fails due to address
resolution.
If this occurs, attempt to find the client by connection handle.
2025-10-23 11:55:24 -06:00
Chris Morgan
133c1a5da4 Usage_tips.md - Note that the library is threadsafe. 2025-10-23 11:54:37 -06:00
Chris Morgan
f622cdff0c README.md - Add a note about threadsafety 2025-10-23 11:54:37 -06:00
Chris Morgan
68068677ab Usage_tips.md - 'Device Local Name' information to help guide setting the GATT Device Name or Advertising name. 2025-09-28 19:31:04 -06:00
Chris Morgan
2c6ab706b3 Usage_tips.md - Detail persisted bonds limitations and considerations relative to CONFIG_BT_NIMBLE_MAX_CCCDS 2025-09-23 21:04:11 -06:00
h2zero
6f0b9ddf5d Convert NIMBLE_CPP macros to MYNEWT. 2025-09-06 16:59:55 -06:00
h2zero
8f9e85a46a Release 2.3.3 2025-09-05 16:11:11 -06:00
h2zero
7706f5a6b2 Support up to 1650 bytes of advertisement with extended advertising. 2025-09-05 15:44:47 -06:00
h2zero
1ffd013794 [Bugfix] Extended advertisements not reporting full data.
Extended advertisement reports would be truncated incorrectly as the handler was not checking the data status.

Correct advertisement length and set status on update.
2025-09-05 15:39:46 -06:00
h2zero
e8f7147ac5 [Bugfix] NimBLEAdvertisedDevice::isConnectable incorrect result 2025-09-05 15:11:38 -06:00
h2zero
6ee2a951f5 Release 2.3.2 2025-09-02 14:54:24 -06:00
h2zero
4b74939b6d Improve macros for code enablement 2025-09-02 14:36:02 -06:00
h2zero
9f7b9042e0 Fix docs build 2025-09-02 14:36:02 -06:00
h2zero
2cd5dc2aa2 Fix build with idf v5.5+ and specific roles are defined. 2025-08-25 09:11:13 -06:00
h2zero
9df8cc7dd1 Refactor to use MYNEWT_VAL macros.
This replaces the previously prefixed CONFIG_BT_X config macros with the underlying MYNEWT_VAL_X config macros that they affected.
2025-08-24 16:35:38 -06:00
iranl
88df909cfb Fix undefined reference to ble_svc_gap_device_name_set when GATT server is disabled (#349)
* Fix undefined reference to ble_svc_gap_device_name_set when GATT server is disabled

* Do not affect ESP-IDF <5.5.0
2025-08-24 16:07:59 -06:00
h2zero
8cefc0a562 [Bugfix] OnConnectfail not called when connection not established.
Workaround for when the disconnect event is sent when no connection has been established.
Espressif changed this from a connect event with error code to disconnect event.
2025-08-01 17:08:44 -06:00
h2zero
8af38e7eb9 Change default security settings to BLE secure connections off.
Fixing some connection issues when enabled, users should enable if desired.
2025-06-27 18:08:42 -06:00
h2zero
e7fead903c [Bugfix](workaround) OnConnect not being called.
Upstream changes have resulted in a possible status of BLE_ERR_UNSUPP_REM_FEATURE, this resulted in the onConnect callback not being called despite the connection actually being created.
This works around that bug to ensure that the connections are correctly tracked.
2025-06-27 18:07:46 -06:00
h2zero
a57c45e1de Fix build with idf versions < 5.x 2025-06-27 18:07:41 -06:00
h2zero
edfc838bef [Bugfix] allow peripheral and central roles without broadcast/scan. 2025-06-20 10:41:28 -06:00
h2zero
f1ead9959d Bump idf_component version 2025-06-11 11:40:42 -06:00
h2zero
b30421c19d Fix library.json version 2025-06-11 11:34:10 -06:00
h2zero
bdb868d125 Release 2.3.1 2025-06-11 11:14:52 -06:00
h2zero
503939c66f Update docs 2025-06-11 11:11:58 -06:00
Larry Davis
2640c44b45 Support passing data directly from NimBLEBeacon.getData() to NimBLEAdvertisementData.setManufacturerData() 2025-06-11 11:11:11 -06:00
h2zero
fec2d7a279 [Bugfix] NimBLEScan delete.
Calling NimBLEDevice::deint with the `clearAll` parameter set to `true` will delete the scan and any scan results but it was calling `clearall` which uses critical sections, this could cause a crash because the stack has already been de-initialized.
2025-06-09 17:13:30 -06:00
h2zero
e2cee2d994 Fix server client read/write not returning when encryption is used.
When the client created by the server reads or writes to an attribute and it triggers a pairing action the task will not be released because the client does not get the event.
This passes the event to the client to prevent the task from being hung.
2025-06-09 17:13:12 -06:00
h2zero
39f974625c Fix builds when exluding roles 2025-06-02 18:06:40 -06:00
John Boiles
169290f047 Allow esp_wifi_remote >= 0.5.3
`esp_wifi_remote` >= v0.10.0 is necessary to use esp-nimble-cpp with the latest ESP-IDF master branch.
2025-05-21 11:14:43 -06:00
7 changed files with 724 additions and 28 deletions

View File

@@ -18,7 +18,7 @@ jobs:
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-docker-image.html
# for details.
idf_ver: ["release-v4.4", "release-v5.4", "release-v5.5"]
idf_target: ["esp32", "esp32s3", "esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32c61", "esp32h2", "esp32p4"]
idf_target: ["esp32", "esp32s3", "esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2", "esp32p4"]
example:
- NimBLE_Client
- NimBLE_Server
@@ -35,10 +35,6 @@ jobs:
idf_target: "esp32c5"
- idf_ver: release-v4.4
idf_target: "esp32c6"
- idf_ver: release-v4.4
idf_target: "esp32c61"
- idf_ver: release-v5.4
idf_target: "esp32c61"
- idf_ver: release-v4.4
idf_target: "esp32h2"
- idf_ver: release-v4.4

View File

@@ -37,7 +37,6 @@ idf_component_register(
"esp32c3"
"esp32c5"
"esp32c6"
"esp32c61"
"esp32h2"
"esp32p4"
INCLUDE_DIRS

View File

@@ -1303,11 +1303,10 @@ bool NimBLEDevice::setDeviceName(const std::string& deviceName) {
/**
* @brief Set a custom callback for gap events.
* @param [in] handler The function to call when gap events occur.
* @param [in] arg Argument to pass to the handler.
* @returns
*/
bool NimBLEDevice::setCustomGapHandler(gap_event_handler handler, void* arg) {
int rc = ble_gap_event_listener_register(&m_listener, handler, arg);
bool NimBLEDevice::setCustomGapHandler(gap_event_handler handler) {
int rc = ble_gap_event_listener_register(&m_listener, handler, NULL);
if (rc == BLE_HS_EALREADY) {
NIMBLE_LOGI(LOG_TAG, "Already listening to GAP events.");
return true;

View File

@@ -133,7 +133,7 @@ class NimBLEDevice {
static void setScanDuplicateCacheSize(uint16_t cacheSize);
static void setScanFilterMode(uint8_t type);
static void setScanDuplicateCacheResetTime(uint16_t time);
static bool setCustomGapHandler(gap_event_handler handler, void* arg = nullptr);
static bool setCustomGapHandler(gap_event_handler handler);
static void setSecurityAuth(bool bonding, bool mitm, bool sc);
static void setSecurityAuth(uint8_t auth);
static void setSecurityIOCap(uint8_t iocap);

View File

@@ -94,24 +94,8 @@ int NimBLERemoteCharacteristic::descriptorDiscCB(
bool NimBLERemoteCharacteristic::retrieveDescriptors(NimBLEDescriptorFilter* pFilter) const {
NIMBLE_LOGD(LOG_TAG, ">> retrieveDescriptors() for characteristic: %s", getUUID().toString().c_str());
const auto pSvc = getRemoteService();
uint16_t endHandle = pSvc->getEndHandle();
// Find the handle of the next characteristic to limit the descriptor search range.
const auto& chars = pSvc->getCharacteristics(false);
for (auto it = chars.begin(); it != chars.end(); ++it) {
if ((*it)->getHandle() == this->getHandle()) {
auto next_it = std::next(it);
if (next_it != chars.end()) {
endHandle = (*next_it)->getHandle() - 1;
NIMBLE_LOGD(LOG_TAG, "Search range limited to handle 0x%04X", endHandle);
}
break;
}
}
// If this is the last handle then there are no descriptors
if (getHandle() == endHandle) {
if (getHandle() == getRemoteService()->getEndHandle()) {
NIMBLE_LOGD(LOG_TAG, "<< retrieveDescriptors(): found 0 descriptors.");
return true;
}
@@ -124,7 +108,7 @@ bool NimBLERemoteCharacteristic::retrieveDescriptors(NimBLEDescriptorFilter* pFi
int rc = ble_gattc_disc_all_dscs(getClient()->getConnHandle(),
getHandle(),
endHandle,
getRemoteService()->getEndHandle(),
NimBLERemoteCharacteristic::descriptorDiscCB,
pFilter);
if (rc != 0) {

498
src/NimBLEStream.cpp Normal file
View File

@@ -0,0 +1,498 @@
/*
* Copyright 2020-2025 Ryan Powell <ryan@nable-embedded.io> 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
size_t Print::print(const char* s) {
if (!s) return 0;
return write(reinterpret_cast<const uint8_t*>(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<const uint8_t*>(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<size_t>(n) < sizeof(stackBuf)) {
return write(reinterpret_cast<const uint8_t*>(stackBuf), static_cast<size_t>(n));
}
// allocate for larger output
size_t needed = static_cast<size_t>(n) + 1;
char* buf = static_cast<char*>(malloc(needed));
if (!buf) {
return 0;
}
va_start(ap, fmt);
vsnprintf(buf, needed, fmt, ap);
va_end(ap);
size_t ret = write(reinterpret_cast<const uint8_t*>(buf), static_cast<size_t>(n));
free(buf);
return ret;
}
# endif
void NimBLEStream::txTask(void* arg) {
NimBLEStream* pStream = static_cast<NimBLEStream*>(arg);
for (;;) {
size_t itemSize = 0;
void* item = xRingbufferReceive(pStream->m_txBuf, &itemSize, portMAX_DELAY);
if (item) {
pStream->send(reinterpret_cast<uint8_t*>(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() {
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;
}
m_hasPeek = false;
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<TickType_t>(timeout)) != pdTRUE) {
return 0;
}
return chunk;
}
size_t NimBLEStream::availableForWrite() const {
return m_txBuf ? xRingbufferGetCurFreeSize(m_txBuf) : 0;
}
void NimBLEStream::flush() {
// Wait until TX ring is drained
while (m_txBuf && xRingbufferGetCurFreeSize(m_txBuf) < m_txBufSize) {
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;
}
if (m_hasPeek) {
return 1; // at least the peeked byte
}
// Query items in RX ring
UBaseType_t waiting = 0;
vRingbufferGetInfo(m_rxBuf, nullptr, nullptr, nullptr, nullptr, &waiting);
return static_cast<int>(waiting);
}
int NimBLEStream::read() {
if (!m_rxBuf) {
return -1;
}
// Return peeked byte if available
if (m_hasPeek) {
m_hasPeek = false;
return static_cast<int>(m_peekByte);
}
size_t itemSize = 0;
uint8_t* item = static_cast<uint8_t*>(xRingbufferReceive(m_rxBuf, &itemSize, 0));
if (!item || itemSize == 0) return -1;
uint8_t byte = item[0];
// If item has more bytes, put the rest back
if (itemSize > 1) {
xRingbufferSend(m_rxBuf, item + 1, itemSize - 1, 0);
}
vRingbufferReturnItem(m_rxBuf, item);
return static_cast<int>(byte);
}
int NimBLEStream::peek() {
if (!m_rxBuf) {
return -1;
}
if (m_hasPeek) {
return static_cast<int>(m_peekByte);
}
size_t itemSize = 0;
uint8_t* item = static_cast<uint8_t*>(xRingbufferReceive(m_rxBuf, &itemSize, 0));
if (!item || itemSize == 0) {
return -1;
}
m_peekByte = item[0];
m_hasPeek = true;
// Put the entire item back
xRingbufferSend(m_rxBuf, item, itemSize, 0);
vRingbufferReturnItem(m_rxBuf, item);
return static_cast<int>(m_peekByte);
}
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;
}
// Clear peek state when new data arrives
m_hasPeek = false;
if (xRingbufferSend(m_rxBuf, data, len, 0) != pdTRUE) {
NIMBLE_UART_LOGE(LOG_TAG, "RX buffer full, dropping %u 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;
}
size_t NimBLEStreamClient::write(const uint8_t* data, size_t len) {
if (!m_pChr || !data || len == 0) {
return 0;
}
return NimBLEStream::write(data, len);
}
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 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 len;
}
int uart_log_printf(const char* format, ...) {
int len;
va_list arg;
va_start(arg, format);
len = 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

220
src/NimBLEStream.h Normal file
View File

@@ -0,0 +1,220 @@
/*
* Copyright 2020-2025 Ryan Powell <ryan@nable-embedded.io> 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 <freertos/FreeRTOS.h>
# include <freertos/ringbuf.h>
# if NIMBLE_CPP_ARDUINO_STRING_AVAILABLE
# include <Stream.h>
# 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; }
// 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); }
size_t availableForWrite() const;
void flush() override;
// Stream RX methods
virtual int available() override;
virtual int read() override;
virtual int peek() override;
// 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);
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};
// RX peek state
mutable uint8_t m_peekByte{0};
mutable bool m_hasPeek{false};
};
# 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; }
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();
size_t write(const uint8_t* data, size_t len) override;
void setWriteWithResponse(bool useWithRsp) { m_writeWithRsp = useWithRsp; }
void setNotifyCallback(NimBLERemoteCharacteristic::notify_callback cb) { m_userNotifyCallback = cb; }
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))
// These logging macros exist to provide log output over UART so that it stream classes 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, ...);
# if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 4
# define NIMBLE_UART_LOGD(tag, format, ...) 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, ...) 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, ...) 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, ...) 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