From 496e2f2d3684b8cbe59f5f2768952f6f386580bb Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Thu, 2 Apr 2026 16:49:09 -0500 Subject: [PATCH 1/4] Add BLE reconnect window and status icon control Introduce a 5-minute reconnect window and UI status control for BLE advertising/connection handling. Adds reconnect window timer/flag and init logic so whitelist advertising after boot or disconnect only runs while the reconnect window is active; advertising logic now checks reconnectWindowActive instead of relying solely on bond count. Add updateBLEStatusIcon(), showBLEStatusIcon(), and hideBLEStatusIcon() and wire status updates across BLE lifecycle events; LVGL changes centralize pairing/status icon show/hide and make stopBLEPairingIconFlash delegate to hideBLEStatusIcon. Remove some legacy scan-response manufacturer payload code paths and stop triggering the pairing-icon flash from the main button handler. Also comment out the BLE_PAIR_ON_BOOT build flag in platformio.ini. --- inc/sp140/lvgl/lvgl_updates.h | 2 + platformio.ini | 2 +- src/sp140/ble/ble_core.cpp | 115 ++++++++++++++++++++++++-------- src/sp140/lvgl/lvgl_updates.cpp | 20 +++++- src/sp140/main.cpp | 1 - 5 files changed, 111 insertions(+), 29 deletions(-) diff --git a/inc/sp140/lvgl/lvgl_updates.h b/inc/sp140/lvgl/lvgl_updates.h index 30ee0f7c..89ac66ee 100644 --- a/inc/sp140/lvgl/lvgl_updates.h +++ b/inc/sp140/lvgl/lvgl_updates.h @@ -79,5 +79,7 @@ void stopCriticalBorderFlashDirect(); // BLE pairing icon flash functions void startBLEPairingIconFlash(); void stopBLEPairingIconFlash(); +void showBLEStatusIcon(); +void hideBLEStatusIcon(); #endif // INC_SP140_LVGL_LVGL_UPDATES_H_ diff --git a/platformio.ini b/platformio.ini index 32a447d7..00f46887 100644 --- a/platformio.ini +++ b/platformio.ini @@ -42,7 +42,7 @@ build_flags = -D CORE_DEBUG_LEVEL=2 -D CONFIG_ARDUINO_LOOP_STACK_SIZE=8192 -Wno-error=format - -D BLE_PAIR_ON_BOOT + ;-D BLE_PAIR_ON_BOOT build_type = debug debug_speed = 12000 debug_tool = esp-builtin diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index aa962e37..90b8d5fb 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -22,19 +22,23 @@ namespace { constexpr size_t kAdvertisingNameCapacity = 32; constexpr TickType_t kConnTuneDelayTicks = pdMS_TO_TICKS(1200); constexpr TickType_t kPairingTimeoutTicks = pdMS_TO_TICKS(60000); +constexpr TickType_t kReconnectWindowTicks = pdMS_TO_TICKS(300000); // 5 min constexpr TickType_t kAdvertisingWatchdogTicks = pdMS_TO_TICKS(1000); TimerHandle_t gConnTuneTimer = nullptr; TimerHandle_t gPairingTimer = nullptr; +TimerHandle_t gReconnectWindowTimer = nullptr; TimerHandle_t gAdvertisingWatchdogTimer = nullptr; char gAdvertisingName[kAdvertisingNameCapacity]; bool pairingModeActive = false; bool pairingModeTransitionActive = false; +bool reconnectWindowActive = false; // Store the active connection handle for conn param updates uint16_t activeConnHandle = 0; bool shouldAdvertiseWhilePowered(); bool startAdvertising(NimBLEServer *server); +void updateBLEStatusIcon(); // Builds gAdvertisingName from the BT MAC and returns the full MAC address // as an uppercase string for use as a unique device ID. Must be called before @@ -63,6 +67,41 @@ void stopPairingModeTimer() { } } +void stopReconnectWindowTimer() { + if (gReconnectWindowTimer != nullptr) { + xTimerStop(gReconnectWindowTimer, 0); + } +} + +void onReconnectWindowTimeout(TimerHandle_t timer) { + (void)timer; + reconnectWindowActive = false; + USBSerial.println("[BLE] Reconnect window expired; stopping advertising"); + restartBLEAdvertising(); +} + +void startReconnectWindowTimer() { + reconnectWindowActive = true; + if (gReconnectWindowTimer == nullptr) { + gReconnectWindowTimer = xTimerCreate("bleReconn", kReconnectWindowTicks, + pdFALSE, nullptr, + onReconnectWindowTimeout); + } + if (gReconnectWindowTimer != nullptr) { + xTimerStop(gReconnectWindowTimer, 0); + xTimerStart(gReconnectWindowTimer, 0); + } +} + +void initReconnectWindowFromBoot() { + if (NimBLEDevice::getNumBonds() > 0) { + startReconnectWindowTimer(); + USBSerial.println("[BLE] Reconnect window active (5 min)"); + } else { + reconnectWindowActive = false; + } +} + size_t syncWhiteListFromBonds() { // Reconcile the whitelist to the current bond store. Advertising must be // stopped before calling this — the BLE controller rejects whitelist changes @@ -89,7 +128,6 @@ size_t syncWhiteListFromBonds() { void onPairingTimeout(TimerHandle_t timer) { (void)timer; pairingModeActive = false; - stopBLEPairingIconFlash(); USBSerial.println("[BLE] Pairing mode expired, re-enabling whitelist"); restartBLEAdvertising(); } @@ -112,11 +150,25 @@ void applyPreferredLinkParams(TimerHandle_t timer) { bool shouldAdvertiseWhilePowered() { return !pairingModeTransitionActive && - (pairingModeActive || NimBLEDevice::getNumBonds() > 0); + (pairingModeActive || reconnectWindowActive); +} + +void updateBLEStatusIcon() { + if (deviceConnected) { + showBLEStatusIcon(); + return; + } + + if (pairingModeActive) { + startBLEPairingIconFlash(); + } else { + hideBLEStatusIcon(); + } } bool startAdvertising(NimBLEServer *server) { if (server == nullptr || pairingModeTransitionActive) { + updateBLEStatusIcon(); return false; } @@ -133,6 +185,14 @@ bool startAdvertising(NimBLEServer *server) { if (!allowOpenAdvertising && bondCount == 0) { USBSerial.println( "[BLE] No bonds present and pairing mode inactive; advertising stopped"); + updateBLEStatusIcon(); + return false; + } + + if (!allowOpenAdvertising && bondCount > 0 && !reconnectWindowActive) { + USBSerial.println( + "[BLE] Reconnect window inactive; whitelist advertising not started"); + updateBLEStatusIcon(); return false; } @@ -161,28 +221,16 @@ bool startAdvertising(NimBLEServer *server) { // Flutter app's `startScan()` filters for CONFIG_SERVICE_UUID. adv.addServiceUUID(NimBLEUUID(CONFIG_SERVICE_UUID)); - // Scan response: manufacturer data with pairing-mode flag so the Flutter app - // can hide non-pairable controllers from the connect list. - // Format: Espressif company ID (0x02E5 LE) + 1 flag byte. - NimBLEExtAdvertisement scanRsp(BLE_HCI_LE_PHY_1M, BLE_HCI_LE_PHY_1M); - scanRsp.setLegacyAdvertising(true); - scanRsp.setScannable(true); - const uint8_t mfrData[] = {0xE5, 0x02, - static_cast(allowOpenAdvertising ? 0x01 : 0x00)}; - scanRsp.setManufacturerData(mfrData, sizeof(mfrData)); - advertising->removeAll(); const bool configured = advertising->setInstanceData(kExtAdvInstance, adv); - const bool scanRspConfigured = - configured ? advertising->setScanResponseData(kExtAdvInstance, scanRsp) - : false; const bool started = - configured && scanRspConfigured && advertising->start(kExtAdvInstance); + configured && advertising->start(kExtAdvInstance); USBSerial.printf( - "[BLE] Ext adv cfg=%d scanRsp=%d start=%d mode=%s bonds=%u wl=%u\n", - configured, scanRspConfigured, started, + "[BLE] Ext adv cfg=%d start=%d mode=%s bonds=%u wl=%u\n", + configured, started, allowOpenAdvertising ? "OPEN" : "BONDED", static_cast(bondCount), static_cast(whiteListCount)); + updateBLEStatusIcon(); return started; #else // Configure payload once — NimBLE accumulates addServiceUUID calls @@ -206,18 +254,13 @@ bool startAdvertising(NimBLEServer *server) { advertising->setScanFilter(false, true); } - // Manufacturer data with pairing-mode flag (updated every restart). - // Espressif company ID (0x02E5 LE) + 1 flag byte. - const std::string mfrPayload = {'\xE5', '\x02', - static_cast(allowOpenAdvertising ? 0x01 : 0x00)}; - advertising->setManufacturerData(mfrPayload); - const bool started = advertising->start(); USBSerial.printf("[BLE] Legacy adv start=%s mode=%s bonds=%u whitelist=%u\n", started ? "OK" : "FAIL", allowOpenAdvertising ? "OPEN" : "BONDED", static_cast(bondCount), static_cast(whiteListCount)); + updateBLEStatusIcon(); return started; #endif } @@ -249,6 +292,9 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { deviceConnected = true; connectedHandle = connInfo.getConnHandle(); + stopReconnectWindowTimer(); + reconnectWindowActive = false; + if (gConnTuneTimer != nullptr) { xTimerStop(gConnTuneTimer, 0); xTimerStart(gConnTuneTimer, 0); @@ -258,6 +304,7 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { connectedHandle, connInfo.getAddress().toString().c_str(), connInfo.isBonded() ? 1 : 0, connInfo.isEncrypted() ? 1 : 0, pairingModeActive ? 1 : 0); + updateBLEStatusIcon(); // During pairing mode, proactively request fresh security negotiation. // This helps recover from stale iOS bonds where iOS tries to restore @@ -287,8 +334,15 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { // Suppress the immediate advertising restart during a pairing transition — // enterBLEPairingMode() issues its own startAdvertising after clearing bonds. if (!pairingModeTransitionActive) { + if (NimBLEDevice::getNumBonds() > 0) { + startReconnectWindowTimer(); + } else { + reconnectWindowActive = false; + stopReconnectWindowTimer(); + } startAdvertising(server); } + updateBLEStatusIcon(); } void onAuthenticationComplete(NimBLEConnInfo &connInfo) override { @@ -298,7 +352,6 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { if (pairingModeActive) { pairingModeActive = false; stopPairingModeTimer(); - stopBLEPairingIconFlash(); } } @@ -327,6 +380,7 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { } } } + updateBLEStatusIcon(); } void onIdentity(NimBLEConnInfo &connInfo) override { @@ -388,8 +442,14 @@ void setupBLE() { #ifdef BLE_PAIR_ON_BOOT USBSerial.println("[BLE] BLE_PAIR_ON_BOOT: entering pairing mode automatically"); enterBLEPairingMode(); - startBLEPairingIconFlash(); #endif + // After optional boot pairing: start reconnect window only if bonds remain. + initReconnectWindowFromBoot(); + if (shouldAdvertiseWhilePowered()) { + restartBLEAdvertising(); + } else { + updateBLEStatusIcon(); + } } void requestFastConnParams() { @@ -410,6 +470,7 @@ void requestNormalConnParams() { void restartBLEAdvertising() { if (pServer == nullptr) { + updateBLEStatusIcon(); return; } @@ -419,6 +480,8 @@ void restartBLEAdvertising() { void enterBLEPairingMode() { // Block advertising restarts (e.g. from onDisconnect) during this transition. pairingModeTransitionActive = true; + stopReconnectWindowTimer(); + reconnectWindowActive = false; // Single-bond model: disconnect the current peer so we can safely clear bonds. if (deviceConnected && pServer != nullptr && diff --git a/src/sp140/lvgl/lvgl_updates.cpp b/src/sp140/lvgl/lvgl_updates.cpp index af78e915..b5963388 100644 --- a/src/sp140/lvgl/lvgl_updates.cpp +++ b/src/sp140/lvgl/lvgl_updates.cpp @@ -308,7 +308,21 @@ void startBLEPairingIconFlash() { } } -void stopBLEPairingIconFlash() { +void showBLEStatusIcon() { + if (xSemaphoreTake(lvglMutex, pdMS_TO_TICKS(50)) == pdTRUE) { + if (ble_pairing_flash_timer != NULL) { + lv_timer_del(ble_pairing_flash_timer); + ble_pairing_flash_timer = NULL; + } + if (ble_pairing_icon != NULL) { + lv_obj_remove_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + } + isFlashingBLEPairingIcon = false; + xSemaphoreGive(lvglMutex); + } +} + +void hideBLEStatusIcon() { if (xSemaphoreTake(lvglMutex, pdMS_TO_TICKS(50)) == pdTRUE) { if (ble_pairing_flash_timer != NULL) { lv_timer_del(ble_pairing_flash_timer); @@ -322,6 +336,10 @@ void stopBLEPairingIconFlash() { } } +void stopBLEPairingIconFlash() { + hideBLEStatusIcon(); +} + // Update the climb rate indicator void updateClimbRateIndicator(float climbRate) { // Clamp climb rate to displayable range (-0.6 to +0.6 m/s) diff --git a/src/sp140/main.cpp b/src/sp140/main.cpp index f94f33fb..41d762f4 100644 --- a/src/sp140/main.cpp +++ b/src/sp140/main.cpp @@ -932,7 +932,6 @@ void buttonHandlerTask(void *parameter) { currentHoldTime >= BLE_PAIRING_HOLD_MS && !pairingHoldHandled) { enterBLEPairingMode(); pulseVibeMotor(); - startBLEPairingIconFlash(); USBSerial.println("[BLE] Pairing mode activated via button hold"); pairingHoldHandled = true; } From 2b32b798911d7dba82f08caf1af3f02cbfcf1554 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Thu, 2 Apr 2026 17:06:46 -0500 Subject: [PATCH 2/4] Decouple BLE UI updates; expose pairing state Remove direct UI calls from BLE core and move BLE pairing icon management into the LVGL UI task. Added isBLEPairingModeActive() to ble_core.h/.cpp so the UI can query pairing state, removed updateBLEStatusIcon() and its calls from ble_core.cpp to decouple BLE logic from display code, and updated lvgl_updates.cpp to include BLE headers and keep the pairing icon in sync (managing the flash timer and visibility). This reduces cross-module coupling and ensures missed callback updates are recovered by the UI task. --- inc/sp140/ble/ble_core.h | 1 + src/sp140/ble/ble_core.cpp | 28 ++-------------------------- src/sp140/lvgl/lvgl_updates.cpp | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/inc/sp140/ble/ble_core.h b/inc/sp140/ble/ble_core.h index bd1d2cdb..124f363f 100644 --- a/inc/sp140/ble/ble_core.h +++ b/inc/sp140/ble/ble_core.h @@ -17,5 +17,6 @@ void requestNormalConnParams(); // Temporarily disable whitelist filtering so a new device can bond. // Advertising reopens for ~60 seconds then whitelisting is restored. void enterBLEPairingMode(); +bool isBLEPairingModeActive(); #endif // INC_SP140_BLE_BLE_CORE_H_ diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index 90b8d5fb..75d5f5fc 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -8,7 +8,6 @@ #include #include "sp140/ble.h" -#include "sp140/lvgl/lvgl_updates.h" #include "sp140/ble/ble_ids.h" #include "sp140/ble/config_service.h" #include "sp140/ble/fastlink_service.h" @@ -38,7 +37,6 @@ uint16_t activeConnHandle = 0; bool shouldAdvertiseWhilePowered(); bool startAdvertising(NimBLEServer *server); -void updateBLEStatusIcon(); // Builds gAdvertisingName from the BT MAC and returns the full MAC address // as an uppercase string for use as a unique device ID. Must be called before @@ -153,22 +151,8 @@ bool shouldAdvertiseWhilePowered() { (pairingModeActive || reconnectWindowActive); } -void updateBLEStatusIcon() { - if (deviceConnected) { - showBLEStatusIcon(); - return; - } - - if (pairingModeActive) { - startBLEPairingIconFlash(); - } else { - hideBLEStatusIcon(); - } -} - bool startAdvertising(NimBLEServer *server) { if (server == nullptr || pairingModeTransitionActive) { - updateBLEStatusIcon(); return false; } @@ -185,14 +169,12 @@ bool startAdvertising(NimBLEServer *server) { if (!allowOpenAdvertising && bondCount == 0) { USBSerial.println( "[BLE] No bonds present and pairing mode inactive; advertising stopped"); - updateBLEStatusIcon(); return false; } if (!allowOpenAdvertising && bondCount > 0 && !reconnectWindowActive) { USBSerial.println( "[BLE] Reconnect window inactive; whitelist advertising not started"); - updateBLEStatusIcon(); return false; } @@ -230,7 +212,6 @@ bool startAdvertising(NimBLEServer *server) { configured, started, allowOpenAdvertising ? "OPEN" : "BONDED", static_cast(bondCount), static_cast(whiteListCount)); - updateBLEStatusIcon(); return started; #else // Configure payload once — NimBLE accumulates addServiceUUID calls @@ -260,7 +241,6 @@ bool startAdvertising(NimBLEServer *server) { allowOpenAdvertising ? "OPEN" : "BONDED", static_cast(bondCount), static_cast(whiteListCount)); - updateBLEStatusIcon(); return started; #endif } @@ -304,7 +284,6 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { connectedHandle, connInfo.getAddress().toString().c_str(), connInfo.isBonded() ? 1 : 0, connInfo.isEncrypted() ? 1 : 0, pairingModeActive ? 1 : 0); - updateBLEStatusIcon(); // During pairing mode, proactively request fresh security negotiation. // This helps recover from stale iOS bonds where iOS tries to restore @@ -342,7 +321,6 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { } startAdvertising(server); } - updateBLEStatusIcon(); } void onAuthenticationComplete(NimBLEConnInfo &connInfo) override { @@ -380,7 +358,6 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { } } } - updateBLEStatusIcon(); } void onIdentity(NimBLEConnInfo &connInfo) override { @@ -447,8 +424,6 @@ void setupBLE() { initReconnectWindowFromBoot(); if (shouldAdvertiseWhilePowered()) { restartBLEAdvertising(); - } else { - updateBLEStatusIcon(); } } @@ -470,7 +445,6 @@ void requestNormalConnParams() { void restartBLEAdvertising() { if (pServer == nullptr) { - updateBLEStatusIcon(); return; } @@ -531,3 +505,5 @@ void enterBLEPairingMode() { USBSerial.println("[BLE] Pairing mode active for 60s"); restartBLEAdvertising(); } + +bool isBLEPairingModeActive() { return pairingModeActive; } diff --git a/src/sp140/lvgl/lvgl_updates.cpp b/src/sp140/lvgl/lvgl_updates.cpp index b5963388..a5ebb199 100644 --- a/src/sp140/lvgl/lvgl_updates.cpp +++ b/src/sp140/lvgl/lvgl_updates.cpp @@ -4,6 +4,8 @@ #include "../../../inc/sp140/globals.h" #include "../../../inc/sp140/vibration_pwm.h" #include "../../../inc/sp140/shared-config.h" +#include "../../../inc/sp140/ble.h" +#include "../../../inc/sp140/ble/ble_core.h" #include // Flash timer globals - definitions @@ -851,6 +853,36 @@ void updateLvglMainScreen( lv_obj_set_style_image_recolor(arm_fail_warning_icon_img, original_arm_fail_icon_color, LV_PART_MAIN); } + // Keep the BLE icon synced from the UI task so missed callback updates recover. + if (ble_pairing_icon != NULL) { + if (deviceConnected) { + if (ble_pairing_flash_timer != NULL) { + lv_timer_del(ble_pairing_flash_timer); + ble_pairing_flash_timer = NULL; + } + lv_obj_remove_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + isFlashingBLEPairingIcon = false; + } else if (isBLEPairingModeActive()) { + if (!isFlashingBLEPairingIcon) { + isFlashingBLEPairingIcon = true; + lv_obj_remove_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + ble_pairing_flash_timer = lv_timer_create(ble_pairing_flash_timer_cb, 500, NULL); + if (ble_pairing_flash_timer == NULL) { + isFlashingBLEPairingIcon = false; + lv_obj_add_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + USBSerial.println("Error: Failed to create BLE pairing flash timer!"); + } + } + } else { + if (ble_pairing_flash_timer != NULL) { + lv_timer_del(ble_pairing_flash_timer); + ble_pairing_flash_timer = NULL; + } + lv_obj_add_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + isFlashingBLEPairingIcon = false; + } + } + // Update climb rate indicator static float lastAltitude = 0.0f; static uint32_t lastAltitudeTime = 0; From b1c0d9b13231bd6165c8a2d4e31df81aab935f3f Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Tue, 7 Apr 2026 20:35:59 -0500 Subject: [PATCH 3/4] Remove BLE reconnect-window logic Remove the reconnect-window feature and related state/timers from the BLE core (kReconnectWindowTicks, gReconnectWindowTimer, reconnectWindowActive and associated functions). Advertising logic is simplified so bonded devices may always reconnect via whitelist advertising (shouldAdvertiseWhilePowered now checks NimBLEDevice::getNumBonds()). Cleanup removes timer management and reconnect-window handling from connection callbacks, setup, and pairing transition, reducing complexity and reliance on a 5-minute reconnect timer. --- src/sp140/ble/ble_core.cpp | 61 ++++---------------------------------- 1 file changed, 5 insertions(+), 56 deletions(-) diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index 75d5f5fc..2a9345c6 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -21,16 +21,13 @@ namespace { constexpr size_t kAdvertisingNameCapacity = 32; constexpr TickType_t kConnTuneDelayTicks = pdMS_TO_TICKS(1200); constexpr TickType_t kPairingTimeoutTicks = pdMS_TO_TICKS(60000); -constexpr TickType_t kReconnectWindowTicks = pdMS_TO_TICKS(300000); // 5 min constexpr TickType_t kAdvertisingWatchdogTicks = pdMS_TO_TICKS(1000); TimerHandle_t gConnTuneTimer = nullptr; TimerHandle_t gPairingTimer = nullptr; -TimerHandle_t gReconnectWindowTimer = nullptr; TimerHandle_t gAdvertisingWatchdogTimer = nullptr; char gAdvertisingName[kAdvertisingNameCapacity]; bool pairingModeActive = false; bool pairingModeTransitionActive = false; -bool reconnectWindowActive = false; // Store the active connection handle for conn param updates uint16_t activeConnHandle = 0; @@ -65,40 +62,6 @@ void stopPairingModeTimer() { } } -void stopReconnectWindowTimer() { - if (gReconnectWindowTimer != nullptr) { - xTimerStop(gReconnectWindowTimer, 0); - } -} - -void onReconnectWindowTimeout(TimerHandle_t timer) { - (void)timer; - reconnectWindowActive = false; - USBSerial.println("[BLE] Reconnect window expired; stopping advertising"); - restartBLEAdvertising(); -} - -void startReconnectWindowTimer() { - reconnectWindowActive = true; - if (gReconnectWindowTimer == nullptr) { - gReconnectWindowTimer = xTimerCreate("bleReconn", kReconnectWindowTicks, - pdFALSE, nullptr, - onReconnectWindowTimeout); - } - if (gReconnectWindowTimer != nullptr) { - xTimerStop(gReconnectWindowTimer, 0); - xTimerStart(gReconnectWindowTimer, 0); - } -} - -void initReconnectWindowFromBoot() { - if (NimBLEDevice::getNumBonds() > 0) { - startReconnectWindowTimer(); - USBSerial.println("[BLE] Reconnect window active (5 min)"); - } else { - reconnectWindowActive = false; - } -} size_t syncWhiteListFromBonds() { // Reconcile the whitelist to the current bond store. Advertising must be @@ -148,7 +111,8 @@ void applyPreferredLinkParams(TimerHandle_t timer) { bool shouldAdvertiseWhilePowered() { return !pairingModeTransitionActive && - (pairingModeActive || reconnectWindowActive); + (pairingModeActive || + NimBLEDevice::getNumBonds() > 0); } bool startAdvertising(NimBLEServer *server) { @@ -172,11 +136,9 @@ bool startAdvertising(NimBLEServer *server) { return false; } - if (!allowOpenAdvertising && bondCount > 0 && !reconnectWindowActive) { - USBSerial.println( - "[BLE] Reconnect window inactive; whitelist advertising not started"); - return false; - } + // Bonded devices can always reconnect via whitelist advertising — + // no reconnect window gating. Power draw is negligible for + // whitelist-only advertising. #if CONFIG_BT_NIMBLE_EXT_ADV // Legacy connectable undirected advertising via the extended API. @@ -272,9 +234,6 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { deviceConnected = true; connectedHandle = connInfo.getConnHandle(); - stopReconnectWindowTimer(); - reconnectWindowActive = false; - if (gConnTuneTimer != nullptr) { xTimerStop(gConnTuneTimer, 0); xTimerStart(gConnTuneTimer, 0); @@ -313,12 +272,6 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { // Suppress the immediate advertising restart during a pairing transition — // enterBLEPairingMode() issues its own startAdvertising after clearing bonds. if (!pairingModeTransitionActive) { - if (NimBLEDevice::getNumBonds() > 0) { - startReconnectWindowTimer(); - } else { - reconnectWindowActive = false; - stopReconnectWindowTimer(); - } startAdvertising(server); } } @@ -420,8 +373,6 @@ void setupBLE() { USBSerial.println("[BLE] BLE_PAIR_ON_BOOT: entering pairing mode automatically"); enterBLEPairingMode(); #endif - // After optional boot pairing: start reconnect window only if bonds remain. - initReconnectWindowFromBoot(); if (shouldAdvertiseWhilePowered()) { restartBLEAdvertising(); } @@ -454,8 +405,6 @@ void restartBLEAdvertising() { void enterBLEPairingMode() { // Block advertising restarts (e.g. from onDisconnect) during this transition. pairingModeTransitionActive = true; - stopReconnectWindowTimer(); - reconnectWindowActive = false; // Single-bond model: disconnect the current peer so we can safely clear bonds. if (deviceConnected && pServer != nullptr && From 37e9292a4422af56dbe909601ef59e7bc835d73f Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Wed, 8 Apr 2026 16:29:32 -0500 Subject: [PATCH 4/4] Handle splash screen and force main repaint Capture and remove the LVGL splash screen to avoid leftover objects and force an initial repaint of the main screen before other tasks contend for SPI. Added a splash_screen pointer (lv_screen_active()), invalidate and refresh the main_display immediately (lv_obj_invalidate + lv_refr_now), and delete the splash screen if it differs from the main screen. Also cleaned up .vscode/extensions.json by removing a duplicate PlatformIO recommendation. --- .vscode/extensions.json | 3 +-- src/sp140/main.cpp | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index dc42e991..f45a9d62 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,8 +3,7 @@ // for the documentation about the extensions.json format "recommendations": [ "ms-vscode.cpptools-extension-pack", - "pioarduino.pioarduino-ide", - "platformio.platformio-ide" + "pioarduino.pioarduino-ide" ], "unwantedRecommendations": [ "ms-vscode.cpptools-extension-pack" diff --git a/src/sp140/main.cpp b/src/sp140/main.cpp index 41d762f4..174e77ff 100644 --- a/src/sp140/main.cpp +++ b/src/sp140/main.cpp @@ -769,8 +769,10 @@ void setup() { setLEDColor(LED_GREEN); // Show splash screen (blocking) + lv_obj_t* splash_screen = NULL; if (xSemaphoreTake(lvglMutex, portMAX_DELAY) == pdTRUE) { displayLvglSplash(deviceData, 2000); + splash_screen = lv_screen_active(); // Keep mutex held through main screen setup } else { USBSerial.println("Failed to acquire LVGL mutex for splash"); @@ -782,9 +784,14 @@ void setup() { setupMainScreen(deviceData.theme == 1); } - // Load main screen + // Force the first main-screen repaint before other tasks can contend for SPI. if (main_screen != NULL) { - lv_screen_load(main_screen); + lv_obj_invalidate(main_screen); + lv_refr_now(main_display); + if (splash_screen != NULL && splash_screen != main_screen) { + lv_obj_delete(splash_screen); + splash_screen = NULL; + } USBSerial.println("Main screen loaded"); } else { USBSerial.println("Error: Main screen object is NULL after setup attempt");