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/platformio.ini b/platformio.ini index af0eb745..32a447d7 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 71fcba98..aa962e37 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -1,13 +1,14 @@ #include "sp140/ble/ble_core.h" #include -#include -#include +#include +#include #include #include #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" @@ -18,19 +19,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]", + kBase, 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); @@ -63,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(); } @@ -116,7 +143,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 +188,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 +196,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. @@ -271,6 +298,7 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { if (pairingModeActive) { pairingModeActive = false; stopPairingModeTimer(); + stopBLEPairingIconFlash(); } } @@ -312,8 +340,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,17 +379,17 @@ 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); 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() { diff --git a/src/sp140/lvgl/lvgl_main_screen.cpp b/src/sp140/lvgl/lvgl_main_screen.cpp index 193b574e..e0d75ff8 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_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 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; } 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 00000000..773cea17 Binary files /dev/null and b/test/test_screenshots/reference/main_ble_pairing_dark.bmp differ 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 00000000..e94209ed Binary files /dev/null and b/test/test_screenshots/reference/main_ble_pairing_light.bmp differ 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