From a556f16bfefd148f6c2b8be4330e8d783d09a1e9 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Sat, 28 Mar 2026 20:03:44 -0400 Subject: [PATCH 1/5] Use runtime BLE name based on MAC address Build a runtime advertising name from the device MAC and use it for BLE advertising instead of the hardcoded kAdvertisingName. Add gAdvertisingName buffer and kAdvertisingNameCapacity, plus initAdvertisingName() to construct a per-device name (and return an uppercase unique ID string). Replace earlier setName usages to reference gAdvertisingName and move the advertising name assignment into the payload configuration path. Change include from to to use snprintf, and update NimBLEDevice::init() to initialize NimBLE before the advertising-name initialization (initAdvertisingName() must be called after NimBLEDevice::init()). Also add prototypes for helper functions shouldAdvertiseWhilePowered() and startAdvertising(). --- src/sp140/ble/ble_core.cpp | 47 +++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index 71fcba98..7afc37fa 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -1,8 +1,8 @@ #include "sp140/ble/ble_core.h" #include -#include -#include +#include +#include #include #include #include @@ -18,19 +18,44 @@ namespace { -constexpr const char *kAdvertisingName = "OpenPPG"; +constexpr size_t kAdvertisingNameCapacity = 32; constexpr TickType_t kConnTuneDelayTicks = pdMS_TO_TICKS(1200); constexpr TickType_t kPairingTimeoutTicks = pdMS_TO_TICKS(60000); constexpr TickType_t kAdvertisingWatchdogTicks = pdMS_TO_TICKS(1000); TimerHandle_t gConnTuneTimer = nullptr; TimerHandle_t gPairingTimer = nullptr; TimerHandle_t gAdvertisingWatchdogTimer = nullptr; +char gAdvertisingName[kAdvertisingNameCapacity]; bool pairingModeActive = false; bool pairingModeTransitionActive = false; // Store the active connection handle for conn param updates uint16_t activeConnHandle = 0; +bool shouldAdvertiseWhilePowered(); +bool startAdvertising(NimBLEServer *server); + +// 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 +// NimBLEDevice::init() so the name is ready for the init call. +std::string initAdvertisingName() { + constexpr const char *kBase = "OpenPPG SP140"; + uint8_t mac[6] = {}; + if (esp_read_mac(mac, ESP_MAC_BT) != ESP_OK) { + snprintf(gAdvertisingName, sizeof(gAdvertisingName), "%s", kBase); + USBSerial.println("[BLE] Failed to read BT MAC, using base advertising name"); + return ""; + } + + snprintf(gAdvertisingName, sizeof(gAdvertisingName), "%s %02X%02X%02X", + kBase, mac[3], mac[4], mac[5]); + + char uniqueId[18]; + snprintf(uniqueId, sizeof(uniqueId), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return uniqueId; +} + void stopPairingModeTimer() { if (gPairingTimer != nullptr) { xTimerStop(gPairingTimer, 0); @@ -116,7 +141,7 @@ bool startAdvertising(NimBLEServer *server) { adv.setLegacyAdvertising(true); adv.setConnectable(true); adv.setScannable(true); - adv.setName(kAdvertisingName); + adv.setName(gAdvertisingName); adv.setFlags(BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP); // High-frequency advertising for "instant" connection @@ -161,7 +186,6 @@ bool startAdvertising(NimBLEServer *server) { // Configure payload once — NimBLE accumulates addServiceUUID calls static bool payloadConfigured = false; if (!payloadConfigured) { - advertising->setName(kAdvertisingName); advertising->setDiscoverableMode(BLE_GAP_DISC_MODE_GEN); advertising->setConnectableMode(BLE_GAP_CONN_MODE_UND); advertising->addServiceUUID(NimBLEUUID(CONFIG_SERVICE_UUID)); @@ -170,6 +194,7 @@ bool startAdvertising(NimBLEServer *server) { advertising->setMaxInterval(48); // 30ms (48 * 0.625ms) payloadConfigured = true; } + advertising->setName(gAdvertisingName); // Open advertising only during the explicit pairing window. Normal runtime // advertising only accepts bonded devices from the controller whitelist. @@ -312,8 +337,10 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { } // namespace void setupBLE() { - // Initialize NimBLE with device name - NimBLEDevice::init(kAdvertisingName); + const std::string uniqueId = initAdvertisingName(); + + // Initialize NimBLE with the computed controller-specific device name. + NimBLEDevice::init(gAdvertisingName); // Require bonded LE Secure Connections. The controller has no input/output, // so pairing stays frictionless ("Just Works") while reconnects restore an @@ -349,12 +376,6 @@ void setupBLE() { xTimerStart(gAdvertisingWatchdogTimer, 0); } - NimBLEAddress bleAddress = NimBLEDevice::getAddress(); - std::string uniqueId = bleAddress.toString(); - std::transform( - uniqueId.begin(), uniqueId.end(), uniqueId.begin(), - [](unsigned char c) { return static_cast(std::toupper(c)); }); - initConfigBleService(pServer, uniqueId); initFastLinkBleService(pServer); initOtaBleService(pServer); From 7c812ca62d77f53fe65c014c8628ee39a3f862de Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Wed, 1 Apr 2026 10:26:14 -0500 Subject: [PATCH 2/5] Add BLE pairing icon and flash indicator Display a Bluetooth symbol on the main LVGL screen to indicate BLE pairing mode and add a flashing indicator for it. Declares ble_pairing_icon in the header, creates the label in lvgl_main_screen (font, color, alignment, hidden by default), and adds ble_pairing_flash_timer and isFlashingBLEPairingIcon state in lvgl_updates. Implements ble_pairing_flash_timer_cb, startBLEPairingIconFlash and stopBLEPairingIconFlash with mutex protection, proper timer creation/deletion and error handling. Wire up start/stop calls: start when pairing is entered via button hold, and stop on pairing timeout and on new BLE connections in ble_core. This provides a visible, thread-safe pairing-mode indicator for users. --- inc/sp140/lvgl/lvgl_main_screen.h | 1 + inc/sp140/lvgl/lvgl_updates.h | 7 ++++ src/sp140/ble/ble_core.cpp | 3 ++ src/sp140/lvgl/lvgl_main_screen.cpp | 9 +++++ src/sp140/lvgl/lvgl_updates.cpp | 61 +++++++++++++++++++++++++++++ src/sp140/main.cpp | 1 + 6 files changed, 82 insertions(+) diff --git a/inc/sp140/lvgl/lvgl_main_screen.h b/inc/sp140/lvgl/lvgl_main_screen.h index 2d498bb7..30bb766f 100644 --- a/inc/sp140/lvgl/lvgl_main_screen.h +++ b/inc/sp140/lvgl/lvgl_main_screen.h @@ -53,6 +53,7 @@ extern lv_obj_t* motor_temp_bg; // Background rectangle for Motor temp section extern lv_obj_t* cruise_icon_img; // Cruise control icon image object extern lv_obj_t* charging_icon_img; // Charging icon image object extern lv_obj_t* arm_fail_warning_icon_img; // Arm fail warning icon +extern lv_obj_t* ble_pairing_icon; // BLE pairing mode icon (Bluetooth symbol) // Climb rate indicator objects extern lv_obj_t* climb_rate_divider_lines[13]; diff --git a/inc/sp140/lvgl/lvgl_updates.h b/inc/sp140/lvgl/lvgl_updates.h index 6655e6c5..30ee0f7c 100644 --- a/inc/sp140/lvgl/lvgl_updates.h +++ b/inc/sp140/lvgl/lvgl_updates.h @@ -25,6 +25,9 @@ extern lv_color_t original_arm_fail_icon_color; extern lv_timer_t* critical_border_flash_timer; extern bool isFlashingCriticalBorder; +extern lv_timer_t* ble_pairing_flash_timer; +extern bool isFlashingBLEPairingIcon; + // Main update function void updateLvglMainScreen( const STR_DEVICE_DATA_140_V1& deviceData, @@ -73,4 +76,8 @@ bool isCriticalBorderFlashing(); void startCriticalBorderFlashDirect(); void stopCriticalBorderFlashDirect(); +// BLE pairing icon flash functions +void startBLEPairingIconFlash(); +void stopBLEPairingIconFlash(); + #endif // INC_SP140_LVGL_LVGL_UPDATES_H_ diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index 7afc37fa..d48a8617 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -8,6 +8,7 @@ #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" @@ -88,6 +89,7 @@ size_t syncWhiteListFromBonds() { void onPairingTimeout(TimerHandle_t timer) { (void)timer; pairingModeActive = false; + stopBLEPairingIconFlash(); USBSerial.println("[BLE] Pairing mode expired, re-enabling whitelist"); restartBLEAdvertising(); } @@ -296,6 +298,7 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { if (pairingModeActive) { pairingModeActive = false; stopPairingModeTimer(); + stopBLEPairingIconFlash(); } } diff --git a/src/sp140/lvgl/lvgl_main_screen.cpp b/src/sp140/lvgl/lvgl_main_screen.cpp index 193b574e..1b546ba8 100644 --- a/src/sp140/lvgl/lvgl_main_screen.cpp +++ b/src/sp140/lvgl/lvgl_main_screen.cpp @@ -41,6 +41,7 @@ lv_obj_t* motor_temp_bg = NULL; // Background rectangle for Motor temp section lv_obj_t* cruise_icon_img = NULL; // Cruise control icon image object lv_obj_t* charging_icon_img = NULL; // Charging icon image object lv_obj_t* arm_fail_warning_icon_img = NULL; // Arm fail warning icon +lv_obj_t* ble_pairing_icon = NULL; // BLE pairing mode icon // Climb rate indicator horizontal divider lines (13 lines total) lv_obj_t* climb_rate_divider_lines[13] = {NULL}; @@ -524,6 +525,14 @@ void setupMainScreen(bool darkMode) { lv_obj_move_foreground(arm_fail_warning_icon_img); // Ensure icon is on top lv_obj_add_flag(arm_fail_warning_icon_img, LV_OBJ_FLAG_HIDDEN); // Hide initially + // Create BLE pairing icon (Bluetooth symbol, initially hidden) + ble_pairing_icon = lv_label_create(main_screen); + lv_label_set_text(ble_pairing_icon, LV_SYMBOL_BLUETOOTH); + lv_obj_set_style_text_font(ble_pairing_icon, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(ble_pairing_icon, LVGL_BLUE, 0); + lv_obj_align(ble_pairing_icon, LV_ALIGN_RIGHT_MID, -20, 11); + lv_obj_add_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); // Hide initially + // Create climb rate indicator horizontal divider lines in the far-right section const int climb_section_start_y = 37; const int climb_section_end_y = 128; diff --git a/src/sp140/lvgl/lvgl_updates.cpp b/src/sp140/lvgl/lvgl_updates.cpp index 887db50d..af78e915 100644 --- a/src/sp140/lvgl/lvgl_updates.cpp +++ b/src/sp140/lvgl/lvgl_updates.cpp @@ -20,10 +20,14 @@ lv_color_t original_arm_fail_icon_color; lv_timer_t* critical_border_flash_timer = NULL; bool isFlashingCriticalBorder = false; +lv_timer_t* ble_pairing_flash_timer = NULL; +bool isFlashingBLEPairingIcon = false; + // Timer callback declarations static void cruise_flash_timer_cb(lv_timer_t* timer); static void arm_fail_flash_timer_cb(lv_timer_t* timer); static void critical_border_flash_timer_cb(lv_timer_t* timer); +static void ble_pairing_flash_timer_cb(lv_timer_t* timer); // --- Cruise Icon Flashing Implementation --- static void cruise_flash_timer_cb(lv_timer_t* timer) { @@ -261,6 +265,63 @@ void stopCriticalBorderFlashDirect() { isFlashingCriticalBorder = false; } +// --- BLE Pairing Icon Flashing Implementation --- +static void ble_pairing_flash_timer_cb(lv_timer_t* timer) { + if (ble_pairing_icon == NULL) { + if (ble_pairing_flash_timer != NULL) { + lv_timer_del(ble_pairing_flash_timer); + ble_pairing_flash_timer = NULL; + } + isFlashingBLEPairingIcon = false; + return; + } + + if (lv_obj_has_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_remove_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + } +} + +void startBLEPairingIconFlash() { + if (xSemaphoreTake(lvglMutex, pdMS_TO_TICKS(50)) == pdTRUE) { + if (ble_pairing_icon == NULL) { + xSemaphoreGive(lvglMutex); + return; + } + + if (ble_pairing_flash_timer != NULL) { + lv_timer_del(ble_pairing_flash_timer); + ble_pairing_flash_timer = NULL; + } + + 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!"); + } + + xSemaphoreGive(lvglMutex); + } +} + +void stopBLEPairingIconFlash() { + 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_add_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + } + isFlashingBLEPairingIcon = false; + xSemaphoreGive(lvglMutex); + } +} + // 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 41d762f4..f94f33fb 100644 --- a/src/sp140/main.cpp +++ b/src/sp140/main.cpp @@ -932,6 +932,7 @@ 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 0aea428780a2e52b92af3f63a1a9e42147f18340 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Wed, 1 Apr 2026 10:29:18 -0500 Subject: [PATCH 3/5] Auto-enter BLE pairing on boot Add BLE_PAIR_ON_BOOT build flag and conditional logic to automatically enter BLE pairing mode during setup. platformio.ini now defines -D BLE_PAIR_ON_BOOT, and setupBLE() prints a notice and calls enterBLEPairingMode() and startBLEPairingIconFlash() when the flag is set, enabling firmware builds that start pairing on boot. --- platformio.ini | 1 + src/sp140/ble/ble_core.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/platformio.ini b/platformio.ini index 19cecb91..dc29229a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -42,6 +42,7 @@ build_flags = -D CORE_DEBUG_LEVEL=2 -D CONFIG_ARDUINO_LOOP_STACK_SIZE=8192 -Wno-error=format + -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 d48a8617..b127e813 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -384,6 +384,12 @@ void setupBLE() { initOtaBleService(pServer); USBSerial.println("BLE device ready"); + +#ifdef BLE_PAIR_ON_BOOT + USBSerial.println("[BLE] BLE_PAIR_ON_BOOT: entering pairing mode automatically"); + enterBLEPairingMode(); + startBLEPairingIconFlash(); +#endif } void requestFastConnParams() { From 0312805bb6f574e31ebb4bb4535f50063afcb432 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Wed, 1 Apr 2026 10:43:06 -0500 Subject: [PATCH 4/5] Shorten BLE advertising name to last 2 MAC bytes Change the BLE advertising name format to include only the last two MAC bytes and wrap them in brackets. The snprintf call was updated from "%s %02X%02X%02X" using mac[3],mac[4],mac[5] to "%s [%02X%02X]" using mac[4],mac[5], shortening and reformatting the device name. --- src/sp140/ble/ble_core.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index b127e813..aa962e37 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -48,8 +48,8 @@ std::string initAdvertisingName() { return ""; } - snprintf(gAdvertisingName, sizeof(gAdvertisingName), "%s %02X%02X%02X", - kBase, mac[3], mac[4], mac[5]); + snprintf(gAdvertisingName, sizeof(gAdvertisingName), "%s [%02X%02X]", + kBase, mac[4], mac[5]); char uniqueId[18]; snprintf(uniqueId, sizeof(uniqueId), "%02X:%02X:%02X:%02X:%02X:%02X", From d3cb7164be8599278b4e7cfa33a3351b941593ff Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Wed, 1 Apr 2026 11:02:21 -0500 Subject: [PATCH 5/5] Add BLE pairing screenshot tests and icon pos Adjust BLE pairing icon placement in the main screen (use lv_obj_set_pos(103,72) instead of lv_obj_align) and add test coverage for the BLE pairing indicator. Tests include light and dark screenshot cases plus reference BMPs. Also update the emulator teardown to clear ble_pairing_icon, its flash timer and flash state so tests start from a clean state. --- src/sp140/lvgl/lvgl_main_screen.cpp | 2 +- test/test_screenshots/emulator_display.cpp | 5 +++ .../reference/main_ble_pairing_dark.bmp | Bin 0 -> 61494 bytes .../reference/main_ble_pairing_light.bmp | Bin 0 -> 61494 bytes test/test_screenshots/test_screenshots.cpp | 42 ++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/test_screenshots/reference/main_ble_pairing_dark.bmp create mode 100644 test/test_screenshots/reference/main_ble_pairing_light.bmp diff --git a/src/sp140/lvgl/lvgl_main_screen.cpp b/src/sp140/lvgl/lvgl_main_screen.cpp index 1b546ba8..e0d75ff8 100644 --- a/src/sp140/lvgl/lvgl_main_screen.cpp +++ b/src/sp140/lvgl/lvgl_main_screen.cpp @@ -530,7 +530,7 @@ void setupMainScreen(bool darkMode) { lv_label_set_text(ble_pairing_icon, LV_SYMBOL_BLUETOOTH); lv_obj_set_style_text_font(ble_pairing_icon, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(ble_pairing_icon, LVGL_BLUE, 0); - lv_obj_align(ble_pairing_icon, LV_ALIGN_RIGHT_MID, -20, 11); + lv_obj_set_pos(ble_pairing_icon, 103, 72); lv_obj_add_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); // Hide initially // Create climb rate indicator horizontal divider lines in the far-right section diff --git a/test/test_screenshots/emulator_display.cpp b/test/test_screenshots/emulator_display.cpp index e7ba846c..307385f9 100644 --- a/test/test_screenshots/emulator_display.cpp +++ b/test/test_screenshots/emulator_display.cpp @@ -334,6 +334,7 @@ void emulator_teardown() { cruise_icon_img = NULL; charging_icon_img = NULL; arm_fail_warning_icon_img = NULL; + ble_pairing_icon = NULL; for (int i = 0; i < 13; i++) climb_rate_divider_lines[i] = NULL; for (int i = 0; i < 12; i++) climb_rate_fill_sections[i] = NULL; critical_border = NULL; @@ -358,15 +359,19 @@ void emulator_teardown() { extern lv_timer_t* cruise_flash_timer; extern lv_timer_t* arm_fail_flash_timer; extern lv_timer_t* critical_border_flash_timer; + extern lv_timer_t* ble_pairing_flash_timer; extern bool isFlashingCruiseIcon; extern bool isFlashingArmFailIcon; extern bool isFlashingCriticalBorder; + extern bool isFlashingBLEPairingIcon; cruise_flash_timer = NULL; arm_fail_flash_timer = NULL; critical_border_flash_timer = NULL; + ble_pairing_flash_timer = NULL; isFlashingCruiseIcon = false; isFlashingArmFailIcon = false; isFlashingCriticalBorder = false; + isFlashingBLEPairingIcon = false; memset(framebuffer, 0, sizeof(framebuffer)); } diff --git a/test/test_screenshots/reference/main_ble_pairing_dark.bmp b/test/test_screenshots/reference/main_ble_pairing_dark.bmp new file mode 100644 index 0000000000000000000000000000000000000000..773cea1756de420be667a8c999aab8d0dba64d04 GIT binary patch literal 61494 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 literal 0 HcmV?d00001 diff --git a/test/test_screenshots/reference/main_ble_pairing_light.bmp b/test/test_screenshots/reference/main_ble_pairing_light.bmp new file mode 100644 index 0000000000000000000000000000000000000000..e94209ed60662ad4d435e6f8222e835b0fab21de GIT binary patch literal 61494 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�-Pa*JDHokS+l$e^bi)RT=qqQN}_5@ELM?lsy@%u?w@{A zmj|t-r#wo3FpemRJ05m>oO)ariV+b9CH09vW1k#~pMmAKUEegM%Z z0<{2Isi%>5huW36)arf!(I^7709vW1k#~pMmAKUEegM%Z0<{2Isi%>5huW36)argU zKs^88_;h+R_7y~YP+F$>9puYj^*x>L&eHEMnNi@=>GWf#5rS%Ly`*aav{FyO!re99 zcd`9g3T!7d;ok&brVx{dk_GOMa}xqgfQL>J<|VAIuEeEQciVFX#PiRNPp5Z-QGN0P z^j%0vuo$}f&g-G3Rk3&>OsI#n>Q z+!vEvpv%IsRUH|~OuV2ZN+dSmpd&n&;2k`M0x#Z)1utH|Max&bD z3-(0>b2xD)3gG+0c>-XHGZccNuro=}pwHNGJ^gqdg^z*2UU(!3)T7`|-zv>;51~Q% zjFMs!hsf;cB+k>T{a-p@K0pug(G2v=0evO|Wo>(c>`7_Pa>0k=Wbwx{h@D4Is=ys4 z#9bHvcO55+t)#Vn@;OD%Vu&>gQg81$FDN*UcU-bz*fmFZ0in&bbMj?Z&GPlr2JO5EuwTa!)5w#3lz!%dJD!Tq zjHMLv7fj<+&nldP%aDxb4{9C!AxZOP^OM~J`;Hu>^-Z_(&=)i*KncyDn)zqm(+5?U z9^#USL7q#Ku==eE*nyXS0g>e=>o#`9Q&z@2oa#?Ym~vBIAV5`d_GEydz$EM+gX2lx z%Q$2Vm?Cg@9*d9ZyQ;Go1QT4O4YM&-Ao3|Q5T<y z%fShHh)-)=lbH-!tJVD_yts^FU3iKM(d@_Wyo4C8IYf77GzlFSy{N`s?Pvn7(w~Zj zK_9wa_rXr#9`)cm1z}XF71s)Cb4|u;0YRRE5fFnZTdICxik=~UVTc#AaXBf8+4!r; z%-r^TMuv{?xge4*O{` zcStc61CeOxSGD%ii|L_O=$Ou8-ktSY?fg}GOArfVT{pguLbuy11r4CU?;|UY5|e(j z@|V6!{K;!YA?8~)4aQ+8K3nof^A}@OoPcyuQUipu&o(=GEl<cXXwuiWbVHC_b*9A!Mcl#)ZjkDa8Q1$_#@p+pmgPq#(_0gA_pO_y{b=36%9y|^^> z)nsPj3r5jwR#DOrn4HegaL^gWOh0rBJMxH`-zTQ6(p!R97^~z+-?f7?IRb^4Z`pL< z+b9BCf>;=^pI)G>tfh|ESjMd1yLv11ETQ(g)G>X8MAQr}IvuU`r4SWBnhoVQ`oL literal 0 HcmV?d00001 diff --git a/test/test_screenshots/test_screenshots.cpp b/test/test_screenshots/test_screenshots.cpp index 684895e8..804436cc 100644 --- a/test/test_screenshots/test_screenshots.cpp +++ b/test/test_screenshots/test_screenshots.cpp @@ -554,6 +554,48 @@ TEST_F(ScreenshotTest, MainScreen_CriticalAlerts_Dark) { save_and_compare("main_critical_alerts_dark"); } +// ============================================================ +// BLE pairing icon tests +// ============================================================ + +TEST_F(ScreenshotTest, MainScreen_BLEPairing_Light) { + auto dd = make_default_device_data(false); + auto esc = make_esc_connected(); + auto bms = make_bms_connected(); + auto ubd = make_unified_battery(72.0f, 88.5f, 0.0f); + + emulator_init_display(false); + setupMainScreen(false); + updateLvglMainScreen(dd, esc, bms, ubd, 0.0f, false, false, 0); + + // Show BLE pairing icon (normally toggled by flash timer) + if (ble_pairing_icon != NULL) { + lv_obj_remove_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + } + + emulator_render_frame(); + save_and_compare("main_ble_pairing_light"); +} + +TEST_F(ScreenshotTest, MainScreen_BLEPairing_Dark) { + auto dd = make_default_device_data(true); + auto esc = make_esc_connected(); + auto bms = make_bms_connected(); + auto ubd = make_unified_battery(72.0f, 88.5f, 0.0f); + + emulator_init_display(true); + setupMainScreen(true); + updateLvglMainScreen(dd, esc, bms, ubd, 0.0f, false, false, 0); + + // Show BLE pairing icon (normally toggled by flash timer) + if (ble_pairing_icon != NULL) { + lv_obj_remove_flag(ble_pairing_icon, LV_OBJ_FLAG_HIDDEN); + } + + emulator_render_frame(); + save_and_compare("main_ble_pairing_dark"); +} + // ============================================================ // Splash screen tests (recreated from lvgl_core.cpp displayLvglSplash) // Uses VERSION_MAJOR/VERSION_MINOR from version.h so references