Add keepsBluetoothAlive hook to Activity base class

Introduces a virtual method allowing activities to indicate whether they need the BLE stack to remain active. This enables selective teardown of Bluetooth to free heap memory, with the Bluetooth settings screen being able to override this for pairing and scanning functionality.
This commit is contained in:
Justin Mitchell
2026-06-24 17:00:52 -04:00
parent d30fde2f4d
commit d6f5be6b7a
15 changed files with 112 additions and 39 deletions
+25 -9
View File
@@ -483,14 +483,29 @@ void setup() {
// Ensure we're not still holding the power button before leaving setup
waitForPowerRelease();
allowSleepAt = millis() + 2000;
// Bluetooth is started lazily by the lifecycle check in loop() once a reader or the
// Bluetooth settings screen is on the stack — not here at boot — so home/browser and
// WiFi activities keep the ~50 KB the BLE stack would otherwise hold.
}
// Restore Bluetooth if it was enabled before sleep/reboot. Deep-sleep wake is a
// full chip reset, so NimBLE starts fresh here and auto-reconnects to the bonded
// page-turner. Left uninitialised (zero radio/RAM cost) when the setting is off.
// ensureStarted() forces normal CPU frequency internally (NimBLE controller init
// hangs at the 10 MHz low-power state).
if (SETTINGS.bluetoothEnabled) {
// Bring the BLE stack up or down to match the current context. BLE is only resident
// while a reader (page-turner input) or the Bluetooth settings screen (pairing) is on
// the stack AND WiFi is off — WiFi and BLE share the one C3 radio and can't both fit in
// heap. Called every loop; ensureStarted()/stop() are no-ops when already in the right
// state, so this only does work on a transition.
void updateBluetoothLifecycle() {
const bool wanted =
SETTINGS.bluetoothEnabled && activityManager.bluetoothShouldBeActive() && WiFi.getMode() == WIFI_MODE_NULL;
if (wanted && !BleHid.isRunning()) {
bleinput::ensureStarted();
// Single place BLE starts for normal use (reader entry, BT toggled on, etc.), so the
// "BT Connecting..." popup shows uniformly. Then clear it with a ghost-cleanup (HALF)
// refresh so a grayscale reader page doesn't ghost over the popup.
bleinput::showConnectingUntilLinked(renderer, mappedInputManager);
activityManager.requestGhostCleanup();
activityManager.requestUpdate();
} else if (!wanted && BleHid.isRunning()) {
bleinput::stop();
}
}
@@ -500,6 +515,7 @@ void loop() {
static unsigned long lastMemPrint = 0;
gpio.update();
updateBluetoothLifecycle(); // bring BLE up/down for the current activity context
BleHid.poll(); // drive BLE auto-reconnect + key auto-repeat (no-op when BT off)
mappedInputManager.pollBle(); // drain BLE keys -> logical-button overlay for this frame
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());
@@ -616,13 +632,13 @@ void loop() {
// The BLE controller cannot run at the 10 MHz low-power frequency — NimBLE's
// controller reset/maintenance hangs the radio and trips the interrupt WDT (the
// same reason WiFi force-disables power saving in HalPowerManager). Keep full CPU
// speed whenever Bluetooth is enabled, regardless of input idleness.
if (!SETTINGS.bluetoothEnabled && millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
// speed whenever the BLE stack is actually resident, regardless of input idleness.
if (!BleHid.isRunning() && millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
// If we've been inactive for a while, increase the delay to save power
powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity
delay(50);
} else {
if (SETTINGS.bluetoothEnabled) powerManager.setPowerSaving(false); // keep the BLE radio stable
if (BleHid.isRunning()) powerManager.setPowerSaving(false); // keep the BLE radio stable
// Short delay to prevent tight loop while still being responsive
delay(10);
}