Skip to content

Fix nRF52 freeze + watchdog reset when saving config over BLE - #11202

Merged
thebentern merged 6 commits into
developfrom
fix/nrf52-ble-disconnect-deadlock
Jul 25, 2026
Merged

Fix nRF52 freeze + watchdog reset when saving config over BLE#11202
thebentern merged 6 commits into
developfrom
fix/nrf52-ble-disconnect-deadlock

Conversation

@thebentern

@thebentern thebentern commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Saving any config section that requires a reboot (position, security, role changes, commit_edit_settings, …) from a BLE-connected phone freezes nRF52 devices completely — screen, mesh, and serial all dead — until the 90 s hardware watchdog resets the board. Reproduced on a Seeed Wio Tracker L1; the mechanism affects every nRF52 target.

Root cause

Since #10967, Router::sendLocal delivers self-addressed packets synchronously in the calling task, so phone-originated admin messages run on Bluefruit's BLE event task (the toRadio write callback is registered with useAdaCallback=false).

When the config change keeps requiresReboot=true, AdminModule calls disableBluetooth() while the phone is still connected. NRF52Bluetooth::disconnect() then busy-waits:

while (Bluefruit.connected())
    yield();

The BLE_GAP_EVT_DISCONNECTED event that clears that flag can only be processed by adafruit_ble_task — the very task now spinning. The wait can never complete. Because the BLE task runs at TASK_PRIO_HIGH and yield() never lets lower-priority tasks run, the Arduino loop task starves, the watchdog feed stops, and the WDT resets the device ~90 s later.

Why it wasn't caught earlier: LoRa config applies live (requiresReboot=false) and skips disableBluetooth() entirely, so region/preset saves — the common test case — never hit this path. Position config always takes the reboot path. #11190 (BLE task stack bump) fixed the stack-overflow crash on this same relocated call chain but not this deadlock.

Fix

Make the disconnect wait best-effort with a 1 s bound, and sleep (delay(1)) instead of spinning so lower-priority tasks — including the watchdog feed in the main loop — keep running during the wait:

  • Called from the loop task (on-device menu, serial): behaves as before, disconnect completes in tens of ms and logs Ended BLE connection.
  • Called from the BLE event task (phone-originated admin): times out after 1 s, logs BLE disconnect still pending, continuing shutdown, and proceeds. The SoftDevice completes the link termination on its own once the callback unwinds, and the normal save + scheduled reboot follow.

Testing

On a Wio Tracker L1 (nRF52840):

  • Before: position config save from the app → Disable NRF52 bluetooth is the last log line, device frozen, watchdog reset ~90 s later.
  • After: position config save from the app → ~1 s pause, config saved, clean 5 s scheduled reboot, reconnects normally.
  • Serial-path saves (no BLE connection) verified unaffected: save → reboot cycle completes in the normal ~11 s including USB re-enumeration.
  • seeed_wio_tracker_L1 build green; formatting checked against trunk's clang-format config.

The durable follow-up remains moving phone-originated admin handling off the BLE event task entirely (#11155).

Summary by CodeRabbit

  • Bug Fixes
    • Improved Bluetooth disconnection handling to avoid indefinite shutdown waits by using a timeout-bounded, best-effort completion check.
    • Added clearer status logging: reports when disconnection remains unconfirmed after the timeout, or confirms when the BLE connection has ended.

Since #10967 phone-originated admin messages are handled synchronously on
Bluefruit's BLE event task. NRF52Bluetooth::disconnect() busy-waited for
BLE_GAP_EVT_DISCONNECTED, which only that same task can process, so any
config save that requires a reboot (e.g. position) deadlocked the device
until the 90s watchdog fired. Bound the wait to 1s and sleep instead of
spinning so lower-priority tasks (including the watchdog feed) keep
running; the SoftDevice completes the link termination on its own.
@coderabbitai

coderabbitai Bot commented Jul 24, 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: 366b878b-3e6d-485c-90a8-ebe7bede827f

📥 Commits

Reviewing files that changed from the base of the PR and between 49f4a1a and 45cb2ca.

📒 Files selected for processing (1)
  • src/platform/nrf52/NRF52Bluetooth.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/platform/nrf52/NRF52Bluetooth.cpp

📝 Walkthrough

Walkthrough

NRF52Bluetooth::disconnect() now waits up to approximately one second for BLE termination, uses delay(1) during polling, and logs whether disconnection completed or remains pending after the timeout.

Changes

NRF52 BLE disconnect handling

Layer / File(s) Summary
Bounded disconnect wait and logging
src/platform/nrf52/NRF52Bluetooth.cpp
NRF52Bluetooth::disconnect() replaces the unbounded wait with timeout-bounded polling and logs both completed and pending disconnection outcomes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: bugfix

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the nRF52 BLE disconnect fix and watchdog-reset issue.
Description check ✅ Passed The PR description is well structured with problem, root cause, fix, and testing details, and it aligns with the template overall.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/nrf52-ble-disconnect-deadlock

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 requested a review from Copilot July 24, 2026 20:15

@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/platform/nrf52/NRF52Bluetooth.cpp (2)

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

Shorten the disconnect rationale comment.

This six-line block exceeds the project’s one- or two-line comment limit. Preserve only the non-obvious reason for the best-effort wait.

Proposed wording
-        // Wait for disconnection, but only best-effort with a timeout: phone-originated admin
-        // messages run on Bluefruit's own BLE event task (sendLocal delivers synchronously), and
-        // there the DISCONNECTED event that clears the connected flag can't be processed until we
-        // return - an unbounded wait deadlocks until the watchdog fires. The SoftDevice completes
-        // the link termination on its own regardless. delay() rather than yield() so lower-priority
-        // tasks (including the watchdog feed in the main loop) keep running while we wait.
+        // Best-effort wait: BLE callbacks may be blocked on the calling event task.
+        // The SoftDevice completes link termination asynchronously.

As per coding guidelines, comments must be minimal and limited to non-obvious rationale.

🤖 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/platform/nrf52/NRF52Bluetooth.cpp` around lines 475 - 480, Shorten the
comment above the disconnect wait to one or two lines, retaining only the
non-obvious rationale that an unbounded wait can deadlock because the
DISCONNECTED event cannot be processed until the current BLE event-task handler
returns. Remove implementation details about SoftDevice termination, delay
versus yield, and watchdog behavior.

Source: Coding guidelines


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

Use Throttle::isWithinTimespanMs() for the BLE disconnect timeout.

This one-shot bounded wait follows the repo’s elapsed-time convention; include mesh/Throttle.h and change the loop to while (Bluefruit.connected() && Throttle::isWithinTimespanMs(start, 1000)).

🤖 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/platform/nrf52/NRF52Bluetooth.cpp` around lines 481 - 483, Include
mesh/Throttle.h in NRF52Bluetooth.cpp and update the BLE disconnect wait loop to
use Throttle::isWithinTimespanMs(start, 1000) instead of manually comparing
millis() against the timeout, while preserving the existing connection check and
delay.

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/platform/nrf52/NRF52Bluetooth.cpp`:
- Around line 475-480: Shorten the comment above the disconnect wait to one or
two lines, retaining only the non-obvious rationale that an unbounded wait can
deadlock because the DISCONNECTED event cannot be processed until the current
BLE event-task handler returns. Remove implementation details about SoftDevice
termination, delay versus yield, and watchdog behavior.
- Around line 481-483: Include mesh/Throttle.h in NRF52Bluetooth.cpp and update
the BLE disconnect wait loop to use Throttle::isWithinTimespanMs(start, 1000)
instead of manually comparing millis() against the timeout, while preserving the
existing connection check and delay.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 79f015c7-f0a5-403a-aaf5-6340bb5df257

📥 Commits

Reviewing files that changed from the base of the PR and between 36fdbdd and 87e4aec.

📒 Files selected for processing (1)
  • src/platform/nrf52/NRF52Bluetooth.cpp

@github-actions

github-actions Bot commented Jul 24, 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-24. Updated for 45cb2ca.

Copilot AI 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.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Prevents nRF52 devices from freezing (and eventually watchdog-resetting) when a BLE-originated config save triggers a reboot-required path and attempts to disconnect while running on Bluefruit’s BLE event task.

Changes:

  • Adds a bounded (1s) best-effort wait for BLE disconnect to avoid deadlock on the BLE event task.
  • Replaces a tight yield() spin with delay(1) to allow lower-priority tasks (including watchdog feeding) to run.
  • Logs whether the disconnect completed or timed out before continuing shutdown.

Comment thread src/platform/nrf52/NRF52Bluetooth.cpp
Comment thread src/platform/nrf52/NRF52Bluetooth.cpp Outdated
Comment on lines +478 to +480
// return - an unbounded wait deadlocks until the watchdog fires. The SoftDevice completes
// the link termination on its own regardless. delay() rather than yield() so lower-priority
// tasks (including the watchdog feed in the main loop) keep running while we wait.
Comment thread src/platform/nrf52/NRF52Bluetooth.cpp Outdated
Comment thread src/platform/nrf52/NRF52Bluetooth.cpp Outdated
@thebentern thebentern added the bugfix Pull request that fixes bugs label Jul 24, 2026

@NomDeTom NomDeTom left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Works for me

@thebentern
thebentern added this pull request to the merge queue Jul 25, 2026
Merged via the queue into develop with commit 3f4e7cc Jul 25, 2026
101 checks passed
madeofstown pushed a commit to madeofstown/meshtastic-firmware that referenced this pull request Jul 26, 2026
…stic#11202)

* Fix nRF52 freeze + watchdog reset when saving config over BLE

Since meshtastic#10967 phone-originated admin messages are handled synchronously on
Bluefruit's BLE event task. NRF52Bluetooth::disconnect() busy-waited for
BLE_GAP_EVT_DISCONNECTED, which only that same task can process, so any
config save that requires a reboot (e.g. position) deadlocked the device
until the 90s watchdog fired. Bound the wait to 1s and sleep instead of
spinning so lower-priority tasks (including the watchdog feed) keep
running; the SoftDevice completes the link termination on its own.

* Address review: use Throttle helper, tighten comment

* Name the disconnect timeout constant

* Log unconfirmed BLE disconnect at WARN with elapsed time
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.

3 participants