Skip to content

Stop BLE log streaming from starving the NimBLE notify pool - #11253

Merged
vidplace7 merged 1 commit into
developfrom
fix/ble-log-notify-mbuf-starvation
Jul 27, 2026
Merged

Stop BLE log streaming from starving the NimBLE notify pool#11253
vidplace7 merged 1 commit into
developfrom
fix/ble-log-notify-mbuf-starvation

Conversation

@thebentern

@thebentern thebentern commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #11245.

On 2.8 / ESP32-S3, a connected phone produces a continuous stream of

[E][BLECharacteristic.cpp:1171] notify(): << ble_gatts_notify_custom: rc=6

rc=6 is BLE_HS_ENOMEM: the NimBLE host had no mbuf left to build the notification, so it was dropped. @bruschill measured 694 of these in 479s on a Heltec V4, and none at all on the same workload on nRF52 — so it's the ESP32/NimBLE path specifically.

What's actually failing

It's the log characteristic, not the packet path.

log_to_ble() calls sendLog() once per firmware log line, and sendLog() notified unconditionally. With debug_log_api_enabled set and a client subscribed (the iOS app subscribes to both notify characteristics on every connect), every single log line becomes a GATT notification. In the reporter's captures the errors alternate 1:1 with ordinary log lines — disk-save messages, dedup chatter, module dispatch — and outnumber the fromNum doorbell attempts by 5-15x. They arrive in bursts at ~12ms intervals aligned with logging storms (a config import, the position replay that runs at connect), not at the steady 1.4/s the averages suggest.

Then it feeds itself: each failed notify prints an error line over serial at roughly 12ms apiece, which is enough to visibly throttle the main loop during a burst.

Every GATT notification and ATT response allocates through ble_hs_mbuf_att_pkt()os_msys_get_pkthdr(0, 0), so once that pool is dry the fromNum doorbell and ATT responses fail too. The phone stops being told there is data waiting.

What this changes

Treat a rejected log notify as backpressure. The Arduino BLE wrapper's notify() returns void, but it does invoke onStatus(..., ERROR_GATT, rc) on failure. Registering a callback on the log characteristic gives the host's own verdict, so sendLog() can stand down for 250ms and let the pool refill instead of hammering it once per line. Debug logs are best-effort; the doorbell isn't, and this stops the former from starving the latter. Keying off the failure rather than a free-block count means it stays correct however the pools are sized.

Restore CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT to the IDF default of 12 (from the 8 set in #10741). _os_msys_find_pool() selects by block size, and a notify request always resolves to the smallest pool, so msys_1 is the only pool GATT ever draws from — 8 blocks is thin once anything chains or fragments. This costs 1KB.

I deliberately did not touch MSYS_2, TRANSPORT_ACL_FROM_LL_COUNT or TRANSPORT_EVT_COUNT. Raising msys_2 would do nothing for this bug (nothing on the notify path allocates from it), and the ACL/EVT pools are separate controller→host transport pools, also not involved. Restoring the full IDF defaults would re-spend ~6KB, most of it as one large contiguous allocation at NimBLE init — which is the exact allocation profile #10741 was fixing on heap-tight boards like heltec-v3, so I'd rather not.

Null-check logRadioCharacteristic in sendLog(). deinit() nulls it, and onNowHasData() already guards its characteristic the same way — sendLog() was the one path that didn't, so a log line arriving during BLE teardown would dereference a freed pointer. Latent, unrelated to the ENOMEM, but it's two lines away.

A note on the 2.7 → 2.8 comparison

The issue reports zero occurrences on 2.7.27. Worth being precise about that: 2.7 used NimBLE-Arduino 1.4.3, whose pool was 12 blocks of 292B — actually smaller in bytes than 2.8's trimmed 8+8 — and its notify() discarded the return code without logging. So "zero errors on 2.7.27" is partly that the old stack couldn't report the failure. The reporter's own 2.7.27 data point supports this: they saw an ATT error 17 (insufficient resources) on a 104-byte write, which is the same pool exhaustion surfacing on the write path, at default sizing.

What genuinely changed in 2.8 is demand — noticeably more log lines, plus the position replay burst at every connect — and a BLE wrapper that now reports each drop. That's why the primary fix here is backpressure rather than just buying more buffers.

Testing

Builds clean for heltec-v4 (the reporter's board). The config change forces a framework rebuild, so expect a slow first build.

I don't have a device that reproduces this in front of me, so this is not hardware-verified yet — if anyone with an S3 and a phone can run with debug_log_api_enabled on, connect, and do a config import, the rc=6 count during that window is the number to watch.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Bluetooth log notification reliability by handling backpressure when the host can’t accept more data.
    • Added a cooldown to pause further notify attempts after ERROR_GATT failures to reduce repeated delivery failures.
    • Prevented log notifications from being sent when the log characteristic is no longer available.
    • Increased NimBLE memory pool capacity for GATT allocations to better handle notify bursts and responses.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (31)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Meshnology W10 meshnology_w10 esp32-s3
Meshnology W12 meshnology_w12 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Pocket V3 rak_wismesh_pocket nrf52840
RAK WisMesh Pod rak_wismesh_pod nrf52840
RAK WisMesh Repeater Mini V2 rak_wismesh_repeater_mini nrf52840
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed SenseCAP Mesh-Tracker-X1 seeed_mesh_tracker_X1 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LILYGO T-Impulse Plus t-impulse-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-08-26. Updated for bfac789.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 221c583b-f16a-4a7a-a7cb-9ce21b421442

📥 Commits

Reviewing files that changed from the base of the PR and between 06441a2 and bfac789.

📒 Files selected for processing (2)
  • src/nimble/NimbleBluetooth.cpp
  • variants/esp32/esp32-common.ini
🚧 Files skipped from review as they are similar to previous changes (2)
  • variants/esp32/esp32-common.ini
  • src/nimble/NimbleBluetooth.cpp

📝 Walkthrough

Walkthrough

BLE log notifications now detect GATT allocation failures, suppress retries during a cooldown, avoid notifying torn-down characteristics, and use a larger ESP32 NimBLE allocation pool.

Changes

BLE notification backpressure

Layer / File(s) Summary
Notification capacity and cooldown handling
src/nimble/NimbleBluetooth.cpp, variants/esp32/esp32-common.ini
The ESP32 NimBLE pool increases from 8 to 12 blocks. Log characteristic callbacks record ERROR_GATT failures, and sendLog skips notifications during cooldowns or when the characteristic is unavailable.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bugfix

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preventing BLE log streaming from exhausting the NimBLE notify pool.
Description check ✅ Passed The description is substantive and covers the fix, impact, testing, and issue reference, though it omits the optional attestation checklist.
Linked Issues check ✅ Passed The code adds log-notify backpressure, restores msys_1 to 12, and null-checks teardown, matching #11245's buffer-exhaustion fix.
Out of Scope Changes check ✅ Passed The changes stay within the BLE notify/backpressure and NimBLE pool sizing scope described in the issue.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ble-log-notify-mbuf-starvation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@thebentern thebentern added bugfix Pull request that fixes bugs and removed bugfix Pull request that fixes bugs labels Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/nimble/NimbleBluetooth.cpp (2)

640-649: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Shorten the explanatory comment block.

This is a multi-paragraph explanation; retain only the concise rationale needed to explain the backoff.

As per coding guidelines, C++ comments should be one or two lines maximum and should explain only non-obvious reasoning.

Proposed simplification
-// Debug-log streaming is best-effort, but it competes for the same resources as the traffic that
-// isn't. Every GATT notification allocates from the NimBLE host's msys_1 mbuf pool, and log_to_ble()
-// attempts one notification per log line, so a logging burst (a config import, the position replay
-// that runs at every connect) drains the pool faster than the link gives it back. Once it is empty
-// the fromNum doorbell and ATT responses fail too, and each failed notify costs an error print on
-// the serial console -- which produces more log lines, at ~12ms apiece.
-//
-// So treat a rejected log notify as backpressure: stop offering log lines to the pool for a moment
-// and let it refill. This keys off the host's own verdict rather than a free-block count, so it
-// stays correct no matter how the pools are sized.
+// Failed log notifications can exhaust NimBLE's msys_1 pool; back off after rejection
+// to preserve FromRadio and ATT traffic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nimble/NimbleBluetooth.cpp` around lines 640 - 649, Shorten the comment
immediately above the debug-log backpressure logic to one or two lines,
retaining only that rejected BLE log notifications trigger a temporary pause to
allow NimBLE resources to recover. Remove the detailed pool, timing, and
connection-flow explanation while preserving the implementation unchanged.

Source: Coding guidelines


650-650: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the prescribed constant naming.

kLogNotifyBackoffMs is a constant but does not follow UPPER_SNAKE_CASE; rename it and its references.

As per coding guidelines, constants and defines must use UPPER_SNAKE_CASE.

-static constexpr uint32_t kLogNotifyBackoffMs = 250;
+static constexpr uint32_t LOG_NOTIFY_BACKOFF_MS = 250;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nimble/NimbleBluetooth.cpp` at line 650, Rename the constant
kLogNotifyBackoffMs to the prescribed UPPER_SNAKE_CASE form and update every
reference to it throughout NimbleBluetooth.cpp, preserving its value and
behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/nimble/NimbleBluetooth.cpp`:
- Around line 640-649: Shorten the comment immediately above the debug-log
backpressure logic to one or two lines, retaining only that rejected BLE log
notifications trigger a temporary pause to allow NimBLE resources to recover.
Remove the detailed pool, timing, and connection-flow explanation while
preserving the implementation unchanged.
- Line 650: Rename the constant kLogNotifyBackoffMs to the prescribed
UPPER_SNAKE_CASE form and update every reference to it throughout
NimbleBluetooth.cpp, preserving its value and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f98e676-21f1-4157-9f74-32831ac2f8e4

📥 Commits

Reviewing files that changed from the base of the PR and between 1e982fa and 06441a2.

📒 Files selected for processing (2)
  • src/nimble/NimbleBluetooth.cpp
  • variants/esp32/esp32-common.ini

@thebentern

Copy link
Copy Markdown
Contributor Author

@bruschill can you test?

sendLog() notified once per firmware log line with no backpressure. With
debug_log_api_enabled and a subscribed client, a logging burst drains the
msys_1 mbuf pool that every GATT notification and ATT response allocates
from, so ble_gatts_notify_custom starts returning BLE_HS_ENOMEM (rc=6) --
for the fromNum doorbell too, not just the log line that exhausted it.
Each failure then prints an error over serial at ~12ms apiece, which is
itself enough to throttle the main loop during a burst.

Register a callback on the log characteristic so the host's own
ERROR_GATT verdict pauses log notifies for 250ms and lets the pool
refill. Keying off the failure rather than a free-block count keeps this
correct however the pools are sized.

Also restore CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT to the IDF default of
12. Pool selection is by block size and a notify request always resolves
to the smallest pool, so msys_1 is the only pool GATT draws from; 8 was
thin once anything chains or fragments. msys_2 and the ACL/EVT transport
pools are left trimmed -- neither is on the notify path, and restoring
the full defaults would re-spend the contiguous allocation that #10741
was fixing on heap-tight boards.

sendLog() also lacked the null check onNowHasData() already has, so a log
line arriving during BLE teardown dereferenced a freed characteristic.

Fixes #11245
@thebentern
thebentern force-pushed the fix/ble-log-notify-mbuf-starvation branch from 06441a2 to bfac789 Compare July 27, 2026 12:15
@thebentern

Copy link
Copy Markdown
Contributor Author

Addressed both CodeRabbit nitpicks: trimmed the block comment to two lines and renamed the constant to LOG_NOTIFY_BACKOFF_MS, per the comment-length and UPPER_SNAKE_CASE rules in .github/copilot-instructions.md. The full rationale lives in the commit message and the PR description instead. No behavior change; rebuilt heltec-v4 clean.

Same treatment applied to #11254, which had the same two problems.

@thebentern
thebentern added this pull request to the merge queue Jul 27, 2026
@bruschill

Copy link
Copy Markdown

@bruschill can you test?

Definitely, will just have to be this evening 👍🏻

@vidplace7
vidplace7 removed this pull request from the merge queue due to a manual request Jul 27, 2026
@vidplace7
vidplace7 merged commit 134fe5e into develop Jul 27, 2026
101 checks passed
@vidplace7
vidplace7 deleted the fix/ble-log-notify-mbuf-starvation branch July 27, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BLE notify fails continuously with BLE_HS_ENOMEM (rc=6) on 2.8.0 / ESP32-S3

3 participants