Migrate ESP32 BLE from Bluedroid to NimBLE-Arduino#104
Merged
jonasniesner merged 4 commits intoJul 17, 2026
Conversation
Replace the arduino-esp32 built-in Bluedroid BLE stack (BLEDevice.h family) with h2zero/NimBLE-Arduino (2.5.0) for all ESP32 targets. The nRF52840 (Bluefruit) path is untouched. Primary motivation: Bluedroid's BLEServer::getConnectedCount() returns incorrect values, and every ESP32 power/sleep and advertising decision keys off that count. NimBLEServer::getConnectedCount() is reliable, so all nine call sites are fixed by the stack swap alone with no logic changes. Secondary benefit: ~36 KB flash reduction (esp32-s3-N16R8). Approach keeps the diff minimal: a single NimBLE include plus BLE* -> NimBLE* type aliases in ble_init.h lets the ~30 existing BLE* spellings compile unchanged; only the genuinely different NimBLE 2.x call sites are edited: - NIMBLE_PROPERTY:: characteristic properties (no BLECharacteristic::PROPERTY_*) - 2.x callbacks take NimBLEConnInfo& (onConnect/onDisconnect/onWrite/onSubscribe) - CCCD auto-created for NOTIFY; BLE2902 removed, subscription tracked via onSubscribe - setCallbacks(&cb, false) so static callback objects are not deleted on deinit - advertising: enableScanResponse / setPreferredParams; setManufacturerData(bytes,16) - dropped deprecated no-op pService->start() platformio.ini pulls NimBLE-Arduino into the shared [env] and excludes it from the nRF env via lib_ignore. Verified: all 9 ESP32 envs build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPcsPnhpVnAeA3zHaaQ7eE
NimBLEAdvertising tracks whether a custom advertisement payload was set via an internal m_advDataSet flag. setAdvertisementData() sets it true, but enableScanResponse() and setPreferredParams() reset it to false. The port called those AFTER setAdvertisementData(), so at start() NimBLE discarded the custom payload (name + manufacturer data) and re-advertised the piecemeal builder instead — a 128-bit service UUID with no manufacturer id 9286 and no name. Clients discover tags by manufacturer id 9286, so the device became undiscoverable and could not be connected to. Make setAdvertisementData() the last advertising-data call before start() in both ble_init_esp32() and updatemsdata(). Drop enableScanResponse(false) (scan response is off by default in NimBLE 2.x) and setPreferredParams() (only affects the overridden builder); this matches Bluedroid's prior on-air behavior, which advertised name + flags + 16-byte manufacturer data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPcsPnhpVnAeA3zHaaQ7eE
onWrite read the characteristic via `String value = getValue()`. NimBLE's
NimBLEAttValue -> Arduino String conversion uses the C-string (strlen)
constructor, which truncates at the first 0x00 byte. Pipe-write frames begin
with 0x00 (00 70 / 00 71 / 00 81), so every write was seen as length 0
("Empty data received") and dropped.
Hold the raw NimBLEAttValue instead; its length()/c_str() preserve the full
binary payload including embedded and leading NUL bytes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPcsPnhpVnAeA3zHaaQ7eE
Three reliability fixes surfaced while validating the NimBLE migration (all pre-existing behavior; NimBLE lets us fix the notify ones cleanly): - Idle loop: the non-battery (USB) idle path used idleDelay(2000), which stalled BLE command/response servicing for up to 2 s when a client connected mid-delay — a sluggish, unreliable-feeling first exchange. Use idleDelay(5), matching the battery idle-hold cadence. - Notify payload race: RX and TX are the same characteristic. setValue(data,len) followed by no-arg notify() sends whatever value is currently stored, so a client streaming WRITE_NR pipe frames could overwrite it between the two calls and corrupt the outgoing ACK. notify(data,len) snapshots the payload into an mbuf immediately, eliminating the window. - Dropped ACKs: the drain advanced the response ring even when notify() failed (BLE_HS_ENOMEM under mbuf exhaustion), permanently losing a pipe ACK and stalling the client's window. On failure, break and keep the entry queued to retry next pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPcsPnhpVnAeA3zHaaQ7eE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the arduino-esp32 built-in Bluedroid BLE stack (
BLEDevice.hfamily) with h2zero/NimBLE-Arduino 2.5.0 for all ESP32 targets. The nRF52840 (Adafruit Bluefruit) path is untouched.Primary motivation: Bluedroid's
BLEServer::getConnectedCount()returns incorrect values, and every ESP32 power/sleep and advertising decision keys off that count (9 call sites).NimBLEServer::getConnectedCount()is reliable — verified in the installed source that the peer is added tom_connectedPeersbeforeonConnectfires — so all call sites are fixed by the stack swap alone, with no logic changes.Secondary benefit: ~36 KB flash reduction on
esp32-s3-N16R8(Bluedroid no longer linked).Approach (minimal diff)
A single
#include <NimBLEDevice.h>plususing BLEServer = NimBLEServer;(etc.) alias block inble_init.hlets the ~30 existingBLE*spellings compile unchanged. Only the genuinely-different NimBLE 2.x call sites were edited:NIMBLE_PROPERTY::characteristic properties (noBLECharacteristic::PROPERTY_*)NimBLEConnInfo&(onConnect/onDisconnect/onWrite/onSubscribe)BLE2902removed, subscription tracked viaonSubscribesetCallbacks(&cb, false)so static callback objects aren't deleted ondeinit()enableScanResponse/setPreferredParams;setAdvertisementData()kept last beforestart()pService->start()platformio.inipulls NimBLE-Arduino into the shared[env]and excludes it from the nRF env vialib_ignore.Commits
enableScanResponse()/setPreferredParams()reset NimBLE's custom-data flag; calling them aftersetAdvertisementData()madestart()discard the name + manufacturer-id-9286 payload, leaving the device undiscoverable. Reordered sosetAdvertisementData()is last.onWriteno longer convertsgetValue()to ArduinoString(the strlen-based conversion truncated pipe frames at their leading0x00); holds aNimBLEAttValueinstead.idleDelay(5)instead ofidleDelay(2000)(removes up-to-2s first-exchange stall); response notify usesnotify(data,len)(snapshots into an mbuf, race-free on the shared RX/TX characteristic) and stops advancing the ring onnotify()failure (no dropped pipe ACKs).Validation
NimBLE-Arduino @ 2.5.0and no BluedroidBLElibrary for ESP32; nRF Bluefruit path unaffected.ODB8B185): connect, subscribe, binary write, command processing, and notify response all verified end-to-end; advertising restarts on disconnect.🤖 Generated with Claude Code