Skip to content

Add BLE stream classes.#1068

Open
h2zero wants to merge 1 commit intomasterfrom
stream-class
Open

Add BLE stream classes.#1068
h2zero wants to merge 1 commit intomasterfrom
stream-class

Conversation

@h2zero
Copy link
Owner

@h2zero h2zero commented Nov 18, 2025

Adds a data stream client and server classes.

The server class sends data to clients through notifications and accepts data written to the characteristic by clients.
The client writes data to the server characteristic and receives data through notifications.

Summary by CodeRabbit

  • New Features

    • Added a NimBLE Stream API for serial‑like, bidirectional BLE communication supporting peripheral (server) and central (client) roles with buffered I/O, MTU‑aware chunking, and automatic reconnect.
  • Documentation

    • Added three end‑to‑end examples with READMEs: stream server, echo server, and stream client demonstrating setup and usage and testing steps.
  • Chores

    • Expanded repository ignore list.
    • Minor build/console macro adjustments and formatting tidies.

@h2zero h2zero marked this pull request as draft November 18, 2025 23:54
@h2zero h2zero force-pushed the stream-class branch 3 times, most recently from 1b7cb22 to 6a67147 Compare November 24, 2025 15:07
@h2zero h2zero force-pushed the stream-class branch 2 times, most recently from bcfcf75 to 9d832b4 Compare January 15, 2026 00:18
@coderabbitai
Copy link

coderabbitai bot commented Mar 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a multi-role NimBLE streaming subsystem (NimBLEStream: server and client) with ring-buffered TX/RX, MTU-aware chunking and drain logic; three Arduino examples (Server, Client, Echo) with READMEs; minor console and nimconfig preprocessor formatting tweaks; and an added .gitignore entry.

Changes

Cohort / File(s) Summary
Core Streaming Implementation
src/NimBLEStream.h, src/NimBLEStream.cpp, src/NimBLEDevice.h
Add NimBLEStream API and implementation: ByteRingBuffer, base NimBLEStream, NimBLEStreamServer and NimBLEStreamClient (begin/end, send/drain, RX notification handling, MTU chunking). Wire header into NimBLEDevice includes.
Arduino Examples & Docs
examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino, examples/NimBLE_Stream_Server/README.md, examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino, examples/NimBLE_Stream_Client/README.md, examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino, examples/NimBLE_Stream_Echo/README.md
Add three sketches (Server, Client, Echo) demonstrating stream usage, bidirectional read/write, discovery/connection and reconnection handling; include READMEs documenting usage, UUIDs, and testing steps.
Console & Build Config
src/nimble/console/console.h, src/nimconfig.h
Adjust console_printf macro placement/expansion for C++ vs C and apply minor preprocessor formatting edits in nimconfig.h; no behavioral changes.
Repository Ignore
.gitignore
Add _codeql_detected_source_root to the ignore list.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Stream as NimBLEStreamServer
    participant BLE as BLE Stack
    participant Peer as Remote BLE Peer

    App->>Stream: begin(svcUUID, chrUUID)
    Stream->>BLE: register service/characteristic & callbacks
    Peer->>BLE: connect & subscribe
    BLE->>Stream: onSubscribe
    App->>Stream: write(data)
    Stream->>Stream: enqueue TX, schedule drain
    Stream->>BLE: send notification (MTU-limited)
    BLE->>Peer: deliver notification
    Peer->>BLE: write characteristic (RX)
    BLE->>Stream: onWrite -> push RX
    App->>Stream: read() -> return RX data
    Peer->>BLE: disconnect
    BLE->>Stream: onDisconnect -> clear state
Loading
sequenceDiagram
    participant App as Application
    participant Stream as NimBLEStreamClient
    participant BLE as BLE Stack
    participant Server as Remote BLE Server

    App->>BLE: scan for server
    BLE->>App: report discovered server
    App->>Stream: begin(remoteChar, subscribeNotify=true)
    Stream->>BLE: subscribe to notifications
    App->>Stream: write(data)
    Stream->>Stream: enqueue TX, schedule drain
    Stream->>BLE: write characteristic (MTU-limited)
    BLE->>Server: deliver write
    Server->>BLE: notify
    BLE->>Stream: notifyCallback -> push RX
    App->>Stream: read() -> return RX data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐰 I hopped in code with buffers bright,
I nibbled MTU chunks through night,
Server, client, echo sing—
Notifications on the wing,
A streaming carrot—pure delight! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add BLE stream classes' accurately summarizes the main change: introducing new NimBLEStreamServer and NimBLEStreamClient classes for BLE data streaming.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch stream-class
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@h2zero h2zero marked this pull request as ready for review March 14, 2026 22:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/NimBLEStream.h (1)

110-111: Use {} for proper zero-initialization of aggregate types.

Initializing ble_npl_event and ble_npl_callout with {NULL} only initializes the first member and leaves remaining members uninitialized. Both are aggregate structs with multiple members that should be zero-initialized. Use {} instead for standard C++ aggregate initialization that ensures all members are properly zeroed.

♻️ Suggested change
-    ble_npl_event   m_txDrainEvent{NULL};
-    ble_npl_callout m_txDrainCallout{NULL};
+    ble_npl_event   m_txDrainEvent{};
+    ble_npl_callout m_txDrainCallout{};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEStream.h` around lines 110 - 111, The member initializers
m_txDrainEvent and m_txDrainCallout should use aggregate zero-initialization
instead of {NULL}; update the declarations of ble_npl_event m_txDrainEvent and
ble_npl_callout m_txDrainCallout in NimBLEStream.h to initialize with {} so all
struct members are zero-initialized (replace the current {NULL} initializers
with {} for the symbols m_txDrainEvent and m_txDrainCallout).
examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino (1)

75-77: Add a defensive null check for pServerDevice in connectToServer().

connectToServer() dereferences pServerDevice immediately (Line 76). A guard prevents accidental null dereference if call flow changes later.

🔧 Proposed fix
 bool connectToServer() {
+    if (pServerDevice == nullptr) {
+        Serial.println("No server selected");
+        return false;
+    }
     Serial.printf("Connecting to: %s\n", pServerDevice->getAddress().toString().c_str());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino` around lines 75 - 77,
The connectToServer() function dereferences pServerDevice immediately; add a
defensive null check at the start of connectToServer() to verify pServerDevice
is not null (e.g., if (pServerDevice == nullptr) { Serial.println("No server
device available"); return false; }) so you avoid null dereference, log a clear
message and return false early; update any callers if they rely on a different
return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino`:
- Around line 155-162: The doConnect handling block (where doConnect is set
false and connectToServer() is called) can leave scanning stopped if onScanEnd
already ran; update the failure branch inside that block (in the same function
handling doConnect) to explicitly restart BLE scanning when connectToServer()
returns false by invoking the scan restart API used elsewhere in the code (e.g.,
the NimBLE scan start call or Scan->start()), and keep the existing logging;
reference the doConnect check and the connectToServer() call to locate where to
add the explicit scan restart.

In `@examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino`:
- Around line 25-26: The server is created in setup() via
NimBLEDevice::createServer() but advertising is not restarted after a client
disconnects, preventing new connections; add a BLE server callback by
subclassing NimBLEServerCallbacks (implement onDisconnect) and in onDisconnect
call the server advertising restart (e.g., server->getAdvertising()->start() or
NimBLEDevice::startAdvertising()) so advertising is resumed after disconnect;
register this callback with the server returned by NimBLEDevice::createServer()
in setup().

In `@src/NimBLEStream.cpp`:
- Around line 50-71: The destructor calls ble_npl_mutex_deinit unconditionally
even if ble_npl_mutex_init failed; add a member flag (e.g., bool
m_mutexInitialized) to ByteRingBuffer, set it true only after ble_npl_mutex_init
returns BLE_NPL_OK in the constructor, and in the ~ByteRingBuffer() only call
ble_npl_mutex_deinit(&m_mutex) when m_mutexInitialized is true (leave existing
m_buf free logic as-is).
- Around line 483-494: The pushRx implementation currently drops the whole
packet when m_rxBuf->freeSize() < len; change it to accept a partial write
instead: compute accepted = std::min(len, m_rxBuf->freeSize()), call
m_rxBuf->write(data, accepted) and return the number of bytes written;
optionally update the log to indicate a partial drop when accepted < len. Update
NimBLEStream::pushRx to use m_rxBuf->freeSize() and ByteRingBuffer::write()
accordingly.

---

Nitpick comments:
In `@examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino`:
- Around line 75-77: The connectToServer() function dereferences pServerDevice
immediately; add a defensive null check at the start of connectToServer() to
verify pServerDevice is not null (e.g., if (pServerDevice == nullptr) {
Serial.println("No server device available"); return false; }) so you avoid null
dereference, log a clear message and return false early; update any callers if
they rely on a different return behavior.

In `@src/NimBLEStream.h`:
- Around line 110-111: The member initializers m_txDrainEvent and
m_txDrainCallout should use aggregate zero-initialization instead of {NULL};
update the declarations of ble_npl_event m_txDrainEvent and ble_npl_callout
m_txDrainCallout in NimBLEStream.h to initialize with {} so all struct members
are zero-initialized (replace the current {NULL} initializers with {} for the
symbols m_txDrainEvent and m_txDrainCallout).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57228ec8-04c6-4beb-abb7-4aa6254cd25d

📥 Commits

Reviewing files that changed from the base of the PR and between 8576ae8 and 2abe280.

📒 Files selected for processing (12)
  • .gitignore
  • examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino
  • examples/NimBLE_Stream_Client/README.md
  • examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino
  • examples/NimBLE_Stream_Echo/README.md
  • examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino
  • examples/NimBLE_Stream_Server/README.md
  • src/NimBLEDevice.h
  • src/NimBLEStream.cpp
  • src/NimBLEStream.h
  • src/nimble/console/console.h
  • src/nimconfig.h

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino (1)

38-43: Consider copying device address instead of storing pointer.

The pServerDevice pointer references memory managed by the NimBLE scan results. While this pattern is common in NimBLE examples and typically works, the pointer may become invalid if scan results are cleared before connectToServer() is called. For more robust code, consider storing the address directly:

static NimBLEAddress serverAddress;
// In onResult:
serverAddress = *advertisedDevice->getAddress();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino` around lines 38 - 43,
The code stores a pointer to the scan result in pServerDevice which may become
invalid later; instead, copy and store the device address (use NimBLEAddress) in
onResult and use that address in connectToServer(), replacing uses of
pServerDevice with the saved serverAddress; update any references in functions
like connectToServer() and the onResult handler to construct/lookup a
NimBLEDevice from the saved NimBLEAddress rather than dereferencing the original
advertisedDevice pointer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino`:
- Around line 75-77: connectToServer() dereferences pServerDevice without
checking for null; add a null-check at the start of connectToServer() to handle
the case where pServerDevice is nullptr (log an error via Serial or return
false) before calling pServerDevice->getAddress(), ensuring the function returns
cleanly (e.g., false) when pServerDevice is not set.

---

Nitpick comments:
In `@examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino`:
- Around line 38-43: The code stores a pointer to the scan result in
pServerDevice which may become invalid later; instead, copy and store the device
address (use NimBLEAddress) in onResult and use that address in
connectToServer(), replacing uses of pServerDevice with the saved serverAddress;
update any references in functions like connectToServer() and the onResult
handler to construct/lookup a NimBLEDevice from the saved NimBLEAddress rather
than dereferencing the original advertisedDevice pointer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7e95589-ed6d-4143-ae99-cee4bb3fb91b

📥 Commits

Reviewing files that changed from the base of the PR and between 2abe280 and c4e2970.

📒 Files selected for processing (3)
  • examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino
  • src/NimBLEStream.cpp
  • src/NimBLEStream.h

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/NimBLEStream.cpp (1)

682-693: Note: Debug log recursion prevention filter.

The string filtering to prevent log recursion is a pragmatic solution. Consider adding a brief comment explaining why this specific string triggers recursion (GAP event logging → characteristic write → more GAP events) for future maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEStream.cpp` around lines 682 - 693, Add a brief explanatory comment
above the recursion-prevention filter: explain that the literal "handleGapEvent"
(variable filterStr) corresponds to GAP event logging paths which can cause a
feedback loop where logging the GAP event triggers a characteristic write and
thus generates further GAP events, so the code checks data/len for that
substring and returns early to drop the log entry and avoid recursion; reference
the variables filterStr, filterLen, data, and len so maintainers can find the
filter logic quickly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/NimBLEStream.cpp`:
- Around line 682-693: Add a brief explanatory comment above the
recursion-prevention filter: explain that the literal "handleGapEvent" (variable
filterStr) corresponds to GAP event logging paths which can cause a feedback
loop where logging the GAP event triggers a characteristic write and thus
generates further GAP events, so the code checks data/len for that substring and
returns early to drop the log entry and avoid recursion; reference the variables
filterStr, filterLen, data, and len so maintainers can find the filter logic
quickly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44e19378-3c8c-438e-a9c5-2fcfec21372c

📥 Commits

Reviewing files that changed from the base of the PR and between 76a82ad and 6bae48d.

📒 Files selected for processing (2)
  • src/NimBLEStream.cpp
  • src/NimBLEStream.h

Co-authored-by: doudar <17362216+doudar@users.noreply.github.com>
@h2zero h2zero requested a review from Copilot March 15, 2026 00:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/NimBLEStream.h (1)

93-98: Tighten the overflow callback contract.

The callback fires when a new chunk no longer fits in the available RX space, not just when len exceeds the total buffer capacity. Calling out the two outcomes—drop current data vs drop oldest buffered data—would make the header match pushRx() and set the right expectations for integrators.

Based on learnings, the preferred direction for RX overflow in NimBLEStream is a user-settable callback so the application can react when data is dropped.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEStream.h` around lines 93 - 98, Update the setRxOverflowCallback
comment to exactly match pushRx() semantics: state that the
rx_overflow_callback_t is invoked when an incoming chunk cannot fit in the
available RX space (not merely when len > total buffer capacity), and document
the two outcomes the callback controls—return true to drop the oldest buffered
data to make room for the incoming chunk, or return false to drop the incoming
chunk; reference setRxOverflowCallback, rx_overflow_callback_t, pushRx(), and
m_rxOverflowCallback so integrators know where this behavior is enforced and
that they can react to dropped data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/NimBLEStream.cpp`:
- Around line 488-514: The RX overflow handling in NimBLEStream.cpp uses
separate ByteRingBuffer calls (freeSize(), drop(), write()) allowing a
concurrent read() to make requiredSpace stale; fix by adding a single atomic
ByteRingBuffer method (e.g., dropAndWrite or ensureSpaceThenWrite) that acquires
the ring-buffer mutex and performs the drop + write decision and operations
under one lock, then replace the overflow branch in the function handling
m_rxBuf with a single call to that new atomic method; reference m_rxBuf,
freeSize(), drop(), write(), and read() when locating changes.
- Around line 758-765: In NimBLEStreamServer::ChrCallbacks::onWrite, detect
write-only mode (begin called with txBufSize == 0 and rxBufSize > 0) and
populate the peer handle so ready()/operator bool() reflect an active peer: if
m_parent's TX is disabled and m_parent->m_peerHandle is not set, call
m_parent->setPeerHandle(connInfo.getConnHandle()) (or assign
m_parent->m_peerHandle = connInfo.getConnHandle() using the class' existing
accessor) when handling onWrite; otherwise leave behavior unchanged. This
ensures write-only streams become "ready" even without onSubscribe(); if you
don't want to support write-only streams, instead update the API
contract/comments on ready()/operator bool() to state they only track
subscribers from onSubscribe().
- Around line 776-795: When NimBLEStreamServer::ChrCallbacks::onSubscribe
accepts a new subscriber (m_peerHandle updated) it must drop any queued unsent
bytes left from the previous peer to avoid delivering them to the new
connection; update onSubscribe to clear m_txBuf (or call the method that empties
it) whenever you retire the old peer (i.e., when ble_gap_conn_find fails and you
set m_peerHandle = BLE_HS_CONN_HANDLE_NONE, and also when assigning m_peerHandle
= connInfo.getConnHandle() for a new subscriber), and ensure send() behavior
remains unchanged except that queued data is flushed/cleared on peer rollover.

---

Nitpick comments:
In `@src/NimBLEStream.h`:
- Around line 93-98: Update the setRxOverflowCallback comment to exactly match
pushRx() semantics: state that the rx_overflow_callback_t is invoked when an
incoming chunk cannot fit in the available RX space (not merely when len > total
buffer capacity), and document the two outcomes the callback controls—return
true to drop the oldest buffered data to make room for the incoming chunk, or
return false to drop the incoming chunk; reference setRxOverflowCallback,
rx_overflow_callback_t, pushRx(), and m_rxOverflowCallback so integrators know
where this behavior is enforced and that they can react to dropped data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ca87acc-8895-41c5-836f-899a61bc43aa

📥 Commits

Reviewing files that changed from the base of the PR and between 6bae48d and 4ea61fb.

📒 Files selected for processing (12)
  • .gitignore
  • examples/NimBLE_Stream_Client/NimBLE_Stream_Client.ino
  • examples/NimBLE_Stream_Client/README.md
  • examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino
  • examples/NimBLE_Stream_Echo/README.md
  • examples/NimBLE_Stream_Server/NimBLE_Stream_Server.ino
  • examples/NimBLE_Stream_Server/README.md
  • src/NimBLEDevice.h
  • src/NimBLEStream.cpp
  • src/NimBLEStream.h
  • src/nimble/console/console.h
  • src/nimconfig.h
✅ Files skipped from review due to trivial changes (2)
  • examples/NimBLE_Stream_Server/README.md
  • src/nimconfig.h
🚧 Files skipped from review as they are similar to previous changes (4)
  • examples/NimBLE_Stream_Echo/README.md
  • .gitignore
  • examples/NimBLE_Stream_Echo/NimBLE_Stream_Echo.ino
  • src/nimble/console/console.h

Comment on lines +488 to +514
size_t freeSize = m_rxBuf->freeSize();
if (len > freeSize) {
const bool dropOlderData = m_rxOverflowCallback && m_rxOverflowCallback(data, len);
if (!dropOlderData) {
NIMBLE_LOGE(LOG_TAG, "RX buffer overflow, dropping current data");
return 0;
}

if (len >= m_rxBuf->capacity()) {
m_rxBuf->drop(m_rxBuf->size());
const uint8_t* tail = data + (len - m_rxBuf->capacity());
size_t written = m_rxBuf->write(tail, m_rxBuf->capacity());
if (written != m_rxBuf->capacity()) {
NIMBLE_LOGE(LOG_TAG, "RX buffer overflow, %zu bytes dropped", m_rxBuf->capacity() - written);
}
return written;
}

const size_t requiredSpace = len - freeSize;
size_t dropped = m_rxBuf->drop(requiredSpace);
if (dropped < requiredSpace) {
NIMBLE_LOGE(LOG_TAG, "RX buffer overflow, failed to drop enough buffered data");
return 0;
}
}

return m_rxBuf->write(data, len);
Copy link

@coderabbitai coderabbitai bot Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Make the RX overflow path atomic.

freeSize(), drop(), and write() each take the ring-buffer mutex separately. A concurrent read() between those calls can make requiredSpace stale, so this branch can drop more buffered data than necessary or reject the incoming chunk even though enough space exists by the time the write happens. Move the overflow decision and the drop/write sequence into one ByteRingBuffer operation under a single lock.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEStream.cpp` around lines 488 - 514, The RX overflow handling in
NimBLEStream.cpp uses separate ByteRingBuffer calls (freeSize(), drop(),
write()) allowing a concurrent read() to make requiredSpace stale; fix by adding
a single atomic ByteRingBuffer method (e.g., dropAndWrite or
ensureSpaceThenWrite) that acquires the ring-buffer mutex and performs the drop
+ write decision and operations under one lock, then replace the overflow branch
in the function handling m_rxBuf with a single call to that new atomic method;
reference m_rxBuf, freeSize(), drop(), write(), and read() when locating
changes.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a recursive mutex, recheck

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me check the NPL mutex implementation to verify this.

@h2zero, you're correct that ble_npl_mutex uses xSemaphoreCreateRecursiveMutex() and xSemaphoreTakeRecursive on FreeRTOS — a recursive mutex. However, a recursive mutex only prevents the same task from deadlocking when it re-acquires the lock. It does not prevent two different tasks from interleaving between separate lock/unlock cycles.

The scenario I flagged is a cross-task race:

  • pushRx runs on the NimBLE host task (dispatched via the event queue from onWrite/notifyCallback).
  • read() / available() are called from the user's Arduino loop task.

Between the separate mutex acquisitions for freeSize(), drop(), and write() in pushRx, the user task can acquire the mutex and call read(), changing the buffer's occupancy. The recursive nature of the mutex doesn't help here — it only allows the same task to re-enter, not serialize across tasks.

So the sequence:

[NimBLE task]  freeSize() → unlock → ...
[User task]    read() → unlock   ← can sneak in here
[NimBLE task]  drop(requiredSpace) → unlock → write() → unlock

…still produces a stale requiredSpace calculation. An atomic dropAndWrite method that holds the lock across the full drop+write sequence is still the correct fix. The concern stands.


✏️ Learnings added
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:488-514
Timestamp: 2026-03-15T00:51:54.533Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino), the ble_npl_mutex used in ByteRingBuffer is a recursive mutex (xSemaphoreCreateRecursiveMutex on FreeRTOS). This prevents same-task deadlocks on re-entry but does NOT prevent cross-task interleaving between separate lock/unlock cycles in pushRx (NimBLE task) and read() (user/Arduino task).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:483-494
Timestamp: 2026-03-14T23:33:16.711Z
Learning: In NimBLEStream (src/NimBLEStream.cpp, NimBLE-Arduino), the author's preference regarding RX buffer overflow in pushRx is that the bigger concern is not partial vs. full drops, but that the application layer has no notification mechanism when data is dropped. A user-settable overflow callback is the preferred direction over silently accepting partial writes.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:50-71
Timestamp: 2026-03-14T23:36:28.104Z
Learning: Guideline: In NimBLEStream.cpp, it's acceptable for ble_npl_mutex_deinit to be called on a zero-initialized mutex (e.g., when ble_npl_mutex_init failed). Ensure review confirms that the ByteRingBuffer constructor zeroes the mutex memory before init (memset to 0) so both fresh construction and reinit cases are safe. If applicable, consider adding a defensive memset to 0 after deinitialization in the destructor to guard against memory reuse, but only if this aligns with the project’s memory handling policy. Verify that the NPL layer handles null/zero checks and that the codepaths consistently treat an uninitialized mutex as safe to deinit.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:776-795
Timestamp: 2026-03-15T00:44:23.085Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino, src/NimBLEStream.cpp), when flushing the TX buffer on peer rollover in NimBLEStreamServer::ChrCallbacks::onSubscribe, the flush should only occur when the new subscriber's connection handle differs from the previous one. If the same peer re-subscribes after a momentary connection loss, the TX buffer should be preserved so transmission can resume seamlessly. Explicit unsubscribes (subValue == 0) should still flush the buffer. Save prevHandle before clearing m_peerHandle so the comparison is possible.

Comment on lines +758 to +765
void NimBLEStreamServer::ChrCallbacks::onWrite(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo) {
// Push received data into RX buffer
auto val = pChr->getValue();
m_parent->pushRx(val.data(), val.size());

if (m_userCallbacks) {
m_userCallbacks->onWrite(pChr, connInfo);
}
Copy link

@coderabbitai coderabbitai bot Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Track an RX peer for write-only streams.

begin(..., txBufSize = 0, rxBufSize > 0) explicitly allows a write-only stream, but m_peerHandle is only populated from onSubscribe(). In that mode RX data can arrive here and be buffered while ready() / operator bool() stay false forever. If write-only mode is meant to be supported, capture connInfo.getConnHandle() here when TX is disabled; otherwise the API contract should say that ready() only reflects a notification subscriber.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEStream.cpp` around lines 758 - 765, In
NimBLEStreamServer::ChrCallbacks::onWrite, detect write-only mode (begin called
with txBufSize == 0 and rxBufSize > 0) and populate the peer handle so
ready()/operator bool() reflect an active peer: if m_parent's TX is disabled and
m_parent->m_peerHandle is not set, call
m_parent->setPeerHandle(connInfo.getConnHandle()) (or assign
m_parent->m_peerHandle = connInfo.getConnHandle() using the class' existing
accessor) when handling onWrite; otherwise leave behavior unchanged. This
ensures write-only streams become "ready" even without onSubscribe(); if you
don't want to support write-only streams, instead update the API
contract/comments on ready()/operator bool() to state they only track
subscribers from onSubscribe().

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that doesn't make sense

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@h2zero, you're right — my comment was off-base. Re-reading the code more carefully:

  • onWrite() pushes data into m_rxBuf unconditionally, without any ready() check, so incoming client writes are buffered regardless of peer-handle state.
  • write() (server → client, notifications) does check ready(), but in a TX-disabled / RX-only stream m_txBuf is null, so it returns 0 even before reaching ready().
  • ready() is purely a "do I have an active notification subscriber?" sentinel — which is correct and intentional; it has no bearing on the RX path.

There's nothing to fix here. I'll withdraw the comment.


✏️ Learnings added
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:758-765
Timestamp: 2026-03-15T00:58:38.537Z
Learning: In NimBLEStreamServer (src/NimBLEStream.cpp, NimBLE-Arduino), ready() is intentionally subscriber/notification-centric: m_peerHandle is set only in onSubscribe(). In a write-only (RX-only) server stream (txBufSize==0, rxBufSize>0), ready() correctly returns false permanently — this is by design. Incoming client writes are pushed into m_rxBuf via onWrite() unconditionally and do not require ready() to be true. There is no need to track a peer handle from onWrite() for RX-only mode.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:483-494
Timestamp: 2026-03-14T23:33:16.711Z
Learning: In NimBLEStream (src/NimBLEStream.cpp, NimBLE-Arduino), the author's preference regarding RX buffer overflow in pushRx is that the bigger concern is not partial vs. full drops, but that the application layer has no notification mechanism when data is dropped. A user-settable overflow callback is the preferred direction over silently accepting partial writes.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:776-795
Timestamp: 2026-03-15T00:44:23.085Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino, src/NimBLEStream.cpp), when flushing the TX buffer on peer rollover in NimBLEStreamServer::ChrCallbacks::onSubscribe, the flush should only occur when the new subscriber's connection handle differs from the previous one. If the same peer re-subscribes after a momentary connection loss, the TX buffer should be preserved so transmission can resume seamlessly. Explicit unsubscribes (subValue == 0) should still flush the buffer. Save prevHandle before clearing m_peerHandle so the comparison is possible.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:50-71
Timestamp: 2026-03-14T23:36:28.104Z
Learning: Guideline: In NimBLEStream.cpp, it's acceptable for ble_npl_mutex_deinit to be called on a zero-initialized mutex (e.g., when ble_npl_mutex_init failed). Ensure review confirms that the ByteRingBuffer constructor zeroes the mutex memory before init (memset to 0) so both fresh construction and reinit cases are safe. If applicable, consider adding a defensive memset to 0 after deinitialization in the destructor to guard against memory reuse, but only if this aligns with the project’s memory handling policy. Verify that the NPL layer handles null/zero checks and that the codepaths consistently treat an uninitialized mutex as safe to deinit.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:488-514
Timestamp: 2026-03-15T00:51:54.533Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino), the ble_npl_mutex used in ByteRingBuffer is a recursive mutex (xSemaphoreCreateRecursiveMutex on FreeRTOS). This prevents same-task deadlocks on re-entry but does NOT prevent cross-task interleaving between separate lock/unlock cycles in pushRx (NimBLE task) and read() (user/Arduino task).

Comment on lines +776 to +795
void NimBLEStreamServer::ChrCallbacks::onSubscribe(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo, uint16_t subValue) {
// If we have a stored peer handle, ensure it still refers to an active connection.
// If the connection has gone away without an explicit unsubscribe, clear it so a new
// subscriber can be accepted.
if (m_peerHandle != BLE_HS_CONN_HANDLE_NONE) {
if (ble_gap_conn_find(m_peerHandle, nullptr) != 0) {
m_peerHandle = BLE_HS_CONN_HANDLE_NONE;
}
}

// only one subscriber supported
if (subValue && m_peerHandle != BLE_HS_CONN_HANDLE_NONE) {
NIMBLE_LOGI(LOG_TAG,
"Already have a subscriber, rejecting new subscription from conn handle %d",
connInfo.getConnHandle());
return;
}

m_peerHandle = subValue ? connInfo.getConnHandle() : BLE_HS_CONN_HANDLE_NONE;
if (m_userCallbacks) {
Copy link

@coderabbitai coderabbitai bot Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear queued TX data when the tracked peer rolls over.

send() keeps unsent bytes in m_txBuf when a notification fails. If the previous peer disappears or unsubscribes and a new peer is accepted here, those bytes stay queued and can be delivered to the next subscriber. Dropping the pending TX queue when the old peer is retired avoids cross-session leakage.

Proposed fix
     if (m_peerHandle != BLE_HS_CONN_HANDLE_NONE) {
         if (ble_gap_conn_find(m_peerHandle, nullptr) != 0) {
+            if (m_parent->m_txBuf) {
+                m_parent->m_txBuf->drop(m_parent->m_txBuf->size());
+            }
             m_peerHandle = BLE_HS_CONN_HANDLE_NONE;
         }
     }
@@
-    m_peerHandle = subValue ? connInfo.getConnHandle() : BLE_HS_CONN_HANDLE_NONE;
+    if (!subValue && m_parent->m_txBuf) {
+        m_parent->m_txBuf->drop(m_parent->m_txBuf->size());
+    }
+    m_peerHandle = subValue ? connInfo.getConnHandle() : BLE_HS_CONN_HANDLE_NONE;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEStream.cpp` around lines 776 - 795, When
NimBLEStreamServer::ChrCallbacks::onSubscribe accepts a new subscriber
(m_peerHandle updated) it must drop any queued unsent bytes left from the
previous peer to avoid delivering them to the new connection; update onSubscribe
to clear m_txBuf (or call the method that empties it) whenever you retire the
old peer (i.e., when ble_gap_conn_find fails and you set m_peerHandle =
BLE_HS_CONN_HANDLE_NONE, and also when assigning m_peerHandle =
connInfo.getConnHandle() for a new subscriber), and ensure send() behavior
remains unchanged except that queued data is flushed/cleared on peer rollover.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the new subscriber is the same, then it shouldn't be dropped as we can continue normally if it was a momentary loss

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@h2zero, that's a fair point — if the same connection handle re-subscribes (momentary loss/reconnect), there's no reason to discard data that can still be delivered. The fix should only flush the TX buffer when it's genuinely a different peer taking over the subscription.

Here's a revised proposal that saves the previous handle before clearing it and only drops queued data on a handle mismatch (or on an explicit unsubscribe):

void NimBLEStreamServer::ChrCallbacks::onSubscribe(
    NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo, uint16_t subValue) {

    uint16_t prevHandle = m_peerHandle; // save before any modification

    // If we have a stored peer handle, ensure it still refers to an active connection.
    if (m_peerHandle != BLE_HS_CONN_HANDLE_NONE) {
        if (ble_gap_conn_find(m_peerHandle, nullptr) != 0) {
            m_peerHandle = BLE_HS_CONN_HANDLE_NONE;
        }
    }

    // Only one subscriber supported.
    if (subValue && m_peerHandle != BLE_HS_CONN_HANDLE_NONE) {
        NIMBLE_LOGI(LOG_TAG,
                    "Already have a subscriber, rejecting new subscription from conn handle %d",
                    connInfo.getConnHandle());
        return;
    }

    if (subValue) {
        uint16_t newHandle = connInfo.getConnHandle();
        // Only flush stale TX data when it's a genuinely different peer.
        if (newHandle != prevHandle && m_parent->m_txBuf) {
            m_parent->m_txBuf->drop(m_parent->m_txBuf->size());
        }
        m_peerHandle = newHandle;
    } else {
        // Explicit unsubscribe — peer opted out, flush queued data.
        if (m_parent->m_txBuf) {
            m_parent->m_txBuf->drop(m_parent->m_txBuf->size());
        }
        m_peerHandle = BLE_HS_CONN_HANDLE_NONE;
    }

    if (m_userCallbacks) { ... }
}

Key changes vs. the original proposal:

  • prevHandle is captured before any mutation.
  • TX flush only happens when newHandle != prevHandle (different peer taking over), preserving buffered data for the same peer resuming after a transient drop.
  • Explicit unsubscribes still flush, since the peer has actively opted out of receiving.

✏️ Learnings added
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:776-795
Timestamp: 2026-03-15T00:44:23.085Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino, src/NimBLEStream.cpp), when flushing the TX buffer on peer rollover in NimBLEStreamServer::ChrCallbacks::onSubscribe, the flush should only occur when the new subscriber's connection handle differs from the previous one. If the same peer re-subscribes after a momentary connection loss, the TX buffer should be preserved so transmission can resume seamlessly. Explicit unsubscribes (subValue == 0) should still flush the buffer. Save prevHandle before clearing m_peerHandle so the comparison is possible.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:483-494
Timestamp: 2026-03-14T23:33:16.711Z
Learning: In NimBLEStream (src/NimBLEStream.cpp, NimBLE-Arduino), the author's preference regarding RX buffer overflow in pushRx is that the bigger concern is not partial vs. full drops, but that the application layer has no notification mechanism when data is dropped. A user-settable overflow callback is the preferred direction over silently accepting partial writes.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:50-71
Timestamp: 2026-03-14T23:36:28.104Z
Learning: Guideline: In NimBLEStream.cpp, it's acceptable for ble_npl_mutex_deinit to be called on a zero-initialized mutex (e.g., when ble_npl_mutex_init failed). Ensure review confirms that the ByteRingBuffer constructor zeroes the mutex memory before init (memset to 0) so both fresh construction and reinit cases are safe. If applicable, consider adding a defensive memset to 0 after deinitialization in the destructor to guard against memory reuse, but only if this aligns with the project’s memory handling policy. Verify that the NPL layer handles null/zero checks and that the codepaths consistently treat an uninitialized mutex as safe to deinit.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the handle can change, the address is the only identifier that might be useful

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@h2zero, you're right — the connection handle is re-assigned by the controller on every new connection, so two successive subscriptions from the same physical device will carry different handles. Comparing handles to detect "same peer" is unreliable. Storing and comparing the peer address is the correct approach.

Revised proposal — add a NimBLEAddress m_peerAddress member to ChrCallbacks and compare addresses instead of handles:

// In the header, add to ChrCallbacks:
NimBLEAddress m_peerAddress{};   // address of the current subscriber

// In onSubscribe:
void NimBLEStreamServer::ChrCallbacks::onSubscribe(
    NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo, uint16_t subValue) {

    NimBLEAddress prevAddress = m_peerAddress; // save before any modification

    // If stored handle is no longer valid, clear it.
    if (m_peerHandle != BLE_HS_CONN_HANDLE_NONE) {
        if (ble_gap_conn_find(m_peerHandle, nullptr) != 0) {
            m_peerHandle = BLE_HS_CONN_HANDLE_NONE;
        }
    }

    // Only one subscriber supported.
    if (subValue && m_peerHandle != BLE_HS_CONN_HANDLE_NONE) {
        NIMBLE_LOGI(LOG_TAG,
                    "Already have a subscriber, rejecting new subscription from conn handle %d",
                    connInfo.getConnHandle());
        return;
    }

    if (subValue) {
        // Flush TX data only if this is a genuinely different peer (address differs).
        if (connInfo.getAddress() != prevAddress && m_parent->m_txBuf) {
            m_parent->m_txBuf->drop(m_parent->m_txBuf->size());
        }
        m_peerHandle  = connInfo.getConnHandle();
        m_peerAddress = connInfo.getAddress();
    } else {
        // Explicit unsubscribe — peer opted out, flush queued data.
        if (m_parent->m_txBuf) {
            m_parent->m_txBuf->drop(m_parent->m_txBuf->size());
        }
        m_peerHandle  = BLE_HS_CONN_HANDLE_NONE;
        m_peerAddress = NimBLEAddress{};
    }

    if (m_userCallbacks) {
        m_userCallbacks->onSubscribe(pChr, connInfo, subValue);
    }
}

Key points:

  • m_peerAddress is stored alongside m_peerHandle and cleared on unsubscribe.
  • A re-subscribing peer with the same address (momentary reconnect) won't lose buffered TX data.
  • A different address taking over, or an explicit unsubscribe, still flushes the buffer.

✏️ Learnings added
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:776-795
Timestamp: 2026-03-15T01:06:46.397Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino, src/NimBLEStream.cpp), when deciding whether to flush the TX buffer on peer rollover in NimBLEStreamServer::ChrCallbacks::onSubscribe, compare the peer BLE *address* (NimBLEAddress via connInfo.getAddress()), not the connection handle. Connection handles are re-assigned by the BLE controller on every new connection, even for the same physical device, so handle comparison is unreliable for detecting "same peer". Store a NimBLEAddress member (e.g. m_peerAddress) in ChrCallbacks alongside m_peerHandle, update it on each subscription, and clear it (set to NimBLEAddress{}) on explicit unsubscribe. Only flush TX data when the new subscriber's address differs from the stored address or on an explicit unsubscribe (subValue == 0).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:758-765
Timestamp: 2026-03-15T00:58:38.537Z
Learning: In NimBLEStreamServer (src/NimBLEStream.cpp, NimBLE-Arduino), ready() is intentionally subscriber/notification-centric: m_peerHandle is set only in onSubscribe(). In a write-only (RX-only) server stream (txBufSize==0, rxBufSize>0), ready() correctly returns false permanently — this is by design. Incoming client writes are pushed into m_rxBuf via onWrite() unconditionally and do not require ready() to be true. There is no need to track a peer handle from onWrite() for RX-only mode.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:483-494
Timestamp: 2026-03-14T23:33:16.711Z
Learning: In NimBLEStream (src/NimBLEStream.cpp, NimBLE-Arduino), the author's preference regarding RX buffer overflow in pushRx is that the bigger concern is not partial vs. full drops, but that the application layer has no notification mechanism when data is dropped. A user-settable overflow callback is the preferred direction over silently accepting partial writes.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:776-795
Timestamp: 2026-03-15T00:44:23.085Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino, src/NimBLEStream.cpp), when flushing the TX buffer on peer rollover in NimBLEStreamServer::ChrCallbacks::onSubscribe, the flush should only occur when the new subscriber's connection handle differs from the previous one. If the same peer re-subscribes after a momentary connection loss, the TX buffer should be preserved so transmission can resume seamlessly. Explicit unsubscribes (subValue == 0) should still flush the buffer. Save prevHandle before clearing m_peerHandle so the comparison is possible.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:50-71
Timestamp: 2026-03-14T23:36:28.104Z
Learning: Guideline: In NimBLEStream.cpp, it's acceptable for ble_npl_mutex_deinit to be called on a zero-initialized mutex (e.g., when ble_npl_mutex_init failed). Ensure review confirms that the ByteRingBuffer constructor zeroes the mutex memory before init (memset to 0) so both fresh construction and reinit cases are safe. If applicable, consider adding a defensive memset to 0 after deinitialization in the destructor to guard against memory reuse, but only if this aligns with the project’s memory handling policy. Verify that the NPL layer handles null/zero checks and that the codepaths consistently treat an uninitialized mutex as safe to deinit.

Learnt from: h2zero
Repo: h2zero/NimBLE-Arduino PR: 1068
File: src/NimBLEStream.cpp:488-514
Timestamp: 2026-03-15T00:51:54.533Z
Learning: In NimBLEStream.cpp (NimBLE-Arduino), the ble_npl_mutex used in ByteRingBuffer is a recursive mutex (xSemaphoreCreateRecursiveMutex on FreeRTOS). This prevents same-task deadlocks on re-entry but does NOT prevent cross-task interleaving between separate lock/unlock cycles in pushRx (NimBLE task) and read() (user/Arduino task).

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 12 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +683 to +692
// 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
}
}
}
Comment on lines +725 to +731
if (m_rc == BLE_HS_ENOMEM || os_msys_num_free() <= 2) {
// NimBLE stack out of buffers, likely due to pending notifications/indications
// Don't drop data, but wait for stack to free buffers and try again later
return true;
}

return false; // disconnect or other error don't retry send, preserve data for next attempt
Comment on lines +141 to +146
// Convenience overload to create service/characteristic internally; service will be deleted on end()
bool begin(const NimBLEUUID& svcUuid,
const NimBLEUUID& chrUuid,
uint32_t txBufSize = 1024,
uint32_t rxBufSize = 1024,
bool secure = false);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants