Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions inc/sp140/lvgl/lvgl_main_screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
7 changes: 7 additions & 0 deletions inc/sp140/lvgl/lvgl_updates.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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_
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 43 additions & 13 deletions src/sp140/ble/ble_core.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#include "sp140/ble/ble_core.h"

#include <Arduino.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <esp_mac.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/timers.h>

#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"
Expand All @@ -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);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand All @@ -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.
Expand Down Expand Up @@ -271,6 +298,7 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks {
if (pairingModeActive) {
pairingModeActive = false;
stopPairingModeTimer();
stopBLEPairingIconFlash();
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<char>(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() {
Expand Down
9 changes: 9 additions & 0 deletions src/sp140/lvgl/lvgl_main_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand Down
61 changes: 61 additions & 0 deletions src/sp140/lvgl/lvgl_updates.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/sp140/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions test/test_screenshots/emulator_display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
}
Binary file not shown.
Binary file not shown.
42 changes: 42 additions & 0 deletions test/test_screenshots/test_screenshots.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down