fix(ble): stop BLE from coming back up during the pre-reboot window (#11650)

* fix(ble): stop BLE from coming back up during the pre-reboot window

Saving a reboot-requiring config over BLE (e.g. screen timeout) made the
node disconnect, re-advertise, let the phone reconnect, and then drop it
again at the reset. Two causes:

nRF52: admin messages from the phone run synchronously on Bluefruit's BLE
event task, so the BLE_GAP_EVT_DISCONNECTED caused by shutdown() is only
processed after we return - and that handler restarts advertising because
restartOnDisconnect(true) was never cleared. Stopping advertising first is
a no-op while a connection is live (the SoftDevice isn't advertising), so
the deferred event brought it straight back. Clear the restart flag and
stop advertising before dropping the link, mirroring nRF54L15's ble_enabled
gate. This also closes a main-thread race on the shutdown path where
Advertising.stop() could land between connection teardown and Bluefruit's
auto-restart within the same event dispatch.

PowerFSM (all platforms): darkEnter/onEnter/powerEnter/powerExit/serialExit
unconditionally re-enable BLE, so any state transition inside the reboot
window - a button press while the banner is up, the screen timeout, USB
plug/unplug - turned BLE back on after AdminModule had deliberately torn it
down. Route them through a helper that skips the re-enable while
rebootAtMsec/shutdownAtMsec is armed; every writer of those deadlines is an
imminent restart.

* style: trim rationale comments to house 1-2 line limit

The full mechanism is in the original commit message and PR description.
This commit is contained in:
Ben Meadors authored and GitHub committed 2026-08-28 22:32:01 +00:00
1 parent db84bdf3b4
commit f8a8d12477
2 files changed
+21 -6

No files matched your search

+16 -5
View File
@@ -195,6 +195,17 @@ static void lsExit()
t5BacklightWakeFromSleep();
}
/// Skip the BLE re-enable while a reboot/shutdown is armed: AdminModule tears BLE down before
/// scheduling the restart, and a state transition in that window would otherwise bring it back up.
static void setBluetoothEnableUnlessRestarting()
{
if (rebootAtMsec || shutdownAtMsec) {
LOG_POWERFSM("Skip BLE enable, restart pending");
return;
}
setBluetoothEnable(true);
}
static void nbEnter()
{
LOG_POWERFSM("State: nbEnter");
@@ -211,7 +222,7 @@ static void nbEnter()
static void darkEnter()
{
LOG_POWERFSM("State: darkEnter");
setBluetoothEnable(true);
setBluetoothEnableUnlessRestarting();
if (screen)
screen->setOn(false);
// Screen timeout enters DARK; ensure backlight also turns off.
@@ -235,7 +246,7 @@ static void serialExit()
{
LOG_POWERFSM("State: serialExit");
// Turn bluetooth back on when we leave serial stream API
setBluetoothEnable(true);
setBluetoothEnableUnlessRestarting();
}
static void powerEnter()
@@ -248,7 +259,7 @@ static void powerEnter()
} else {
if (screen)
screen->setOn(true);
setBluetoothEnable(true);
setBluetoothEnableUnlessRestarting();
// within enter() the function getState() returns the state we came from
}
}
@@ -266,7 +277,7 @@ static void powerIdle()
static void powerExit()
{
LOG_POWERFSM("State: powerExit");
setBluetoothEnable(true);
setBluetoothEnableUnlessRestarting();
}
static void onEnter()
@@ -274,7 +285,7 @@ static void onEnter()
LOG_POWERFSM("State: onEnter");
if (screen)
screen->setOn(true);
setBluetoothEnable(true);
setBluetoothEnableUnlessRestarting();
}
static void onIdle()
+5 -1
View File
@@ -245,8 +245,12 @@ void NRF52Bluetooth::shutdown()
// Shutdown bluetooth for minimum power draw
LOG_INFO("Disable NRF52 bluetooth");
Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset)
disconnect();
// Clear the auto-restart flag before dropping the link: our DISCONNECTED event is only processed
// after this callback returns and would re-start advertising. startAdv()/resumeAdvertising() re-set it.
Bluefruit.Advertising.restartOnDisconnect(false);
Bluefruit.Advertising.stop();
disconnect();
}
void NRF52Bluetooth::startDisabled()
{