From 87e4aec5a5234871a4c8bf10773aa4f9df85243e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 24 Jul 2026 15:12:38 -0500 Subject: [PATCH 1/4] Fix nRF52 freeze + watchdog reset when saving config over BLE 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. --- src/platform/nrf52/NRF52Bluetooth.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 357c1484c85..6678ac1122a 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -472,11 +472,20 @@ void NRF52Bluetooth::disconnect() for (uint8_t i = 0; i < connection_num; i++) Bluefruit.disconnect(i); - // Wait for disconnection - while (Bluefruit.connected()) - yield(); - - LOG_INFO("Ended BLE connection"); + // 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. + uint32_t start = millis(); + while (Bluefruit.connected() && millis() - start < 1000) + delay(1); + + if (Bluefruit.connected()) + LOG_INFO("BLE disconnect still pending, continuing shutdown"); + else + LOG_INFO("Ended BLE connection"); } } From 1825846dec447d37a4723f8dbb91aabd9012383b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 24 Jul 2026 15:19:50 -0500 Subject: [PATCH 2/4] Address review: use Throttle helper, tighten comment --- src/platform/nrf52/NRF52Bluetooth.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 6678ac1122a..b6e33f62a95 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -7,6 +7,7 @@ #include "error.h" #include "main.h" #include "mesh/PhoneAPI.h" +#include "mesh/Throttle.h" #include "mesh/mesh-pb-constants.h" #include #include @@ -472,14 +473,10 @@ void NRF52Bluetooth::disconnect() for (uint8_t i = 0; i < connection_num; i++) Bluefruit.disconnect(i); - // 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: on Bluefruit's BLE event task the DISCONNECTED event can't be processed + // until this callback returns, so an unbounded wait would deadlock until the watchdog fires. uint32_t start = millis(); - while (Bluefruit.connected() && millis() - start < 1000) + while (Bluefruit.connected() && Throttle::isWithinTimespanMs(start, 1000)) delay(1); if (Bluefruit.connected()) From 49f4a1abffd6e255cc28357cbab1c4f63f529b66 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 24 Jul 2026 15:37:09 -0500 Subject: [PATCH 3/4] Name the disconnect timeout constant --- src/platform/nrf52/NRF52Bluetooth.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index b6e33f62a95..02061b8e325 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -467,6 +467,7 @@ bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passk // Disconnect any BLE connections void NRF52Bluetooth::disconnect() { + static constexpr uint32_t DISCONNECT_TIMEOUT_MSEC = 1000; uint8_t connection_num = Bluefruit.connected(); if (connection_num) { // Close all connections. We're only expecting one. @@ -476,7 +477,7 @@ void NRF52Bluetooth::disconnect() // Best-effort wait: on Bluefruit's BLE event task the DISCONNECTED event can't be processed // until this callback returns, so an unbounded wait would deadlock until the watchdog fires. uint32_t start = millis(); - while (Bluefruit.connected() && Throttle::isWithinTimespanMs(start, 1000)) + while (Bluefruit.connected() && Throttle::isWithinTimespanMs(start, DISCONNECT_TIMEOUT_MSEC)) delay(1); if (Bluefruit.connected()) From 45cb2ca7897ae9d2757b4eebf2dd0f343dee053b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 24 Jul 2026 19:00:30 -0500 Subject: [PATCH 4/4] Log unconfirmed BLE disconnect at WARN with elapsed time --- src/platform/nrf52/NRF52Bluetooth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 02061b8e325..f1d9c6845d7 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -481,7 +481,7 @@ void NRF52Bluetooth::disconnect() delay(1); if (Bluefruit.connected()) - LOG_INFO("BLE disconnect still pending, continuing shutdown"); + LOG_WARN("BLE disconnect unconfirmed after %ums, continuing shutdown", millis() - start); else LOG_INFO("Ended BLE connection"); }