diff --git a/inc/sp140/lvgl/lvgl_core.h b/inc/sp140/lvgl/lvgl_core.h index 0b3be14..bb0d0aa 100644 --- a/inc/sp140/lvgl/lvgl_core.h +++ b/inc/sp140/lvgl/lvgl_core.h @@ -16,14 +16,13 @@ // v9 uses byte-based buffers: width * height * bytes_per_pixel / 2 #define LVGL_BUF_BYTES (SCREEN_WIDTH * SCREEN_HEIGHT * 2 / 2) -// LVGL refresh time in ms - match the config file setting +// Poll period for the blocking splash-screen loop in ms #define LVGL_REFRESH_TIME 40 // Core LVGL globals extern int8_t displayCS; // Display chip select pin extern lv_display_t* main_display; extern Adafruit_ST7735* tft_driver; -extern uint32_t lvgl_last_update; // Shared SPI bus mutex (guards TFT + MCP2515 access) extern SemaphoreHandle_t spiBusMutex; @@ -31,7 +30,6 @@ extern SemaphoreHandle_t spiBusMutex; void setupLvglBuffer(); void setupLvglDisplay(const STR_DEVICE_DATA_140_V1& deviceData, int8_t dc_pin, int8_t rst_pin, SPIClass* spi); void lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map); -void lv_tick_handler(); void updateLvgl(); void displayLvglSplash(const STR_DEVICE_DATA_140_V1& deviceData, int duration); diff --git a/inc/sp140/lvgl/lvgl_updates.h b/inc/sp140/lvgl/lvgl_updates.h index 89ac66e..5c68b85 100644 --- a/inc/sp140/lvgl/lvgl_updates.h +++ b/inc/sp140/lvgl/lvgl_updates.h @@ -28,6 +28,10 @@ extern bool isFlashingCriticalBorder; extern lv_timer_t* ble_pairing_flash_timer; extern bool isFlashingBLEPairingIcon; +// Forget cached last-applied UI state (alignment modes, attached tile +// styles). Must be called whenever the main screen widgets are (re)created. +void resetLvglUpdateCache(); + // Main update function void updateLvglMainScreen( const STR_DEVICE_DATA_140_V1& deviceData, diff --git a/sdkconfig.OpenPPG-CESP32S3-CAN-SP140 b/sdkconfig.OpenPPG-CESP32S3-CAN-SP140 index 745f540..07fc5ee 100644 --- a/sdkconfig.OpenPPG-CESP32S3-CAN-SP140 +++ b/sdkconfig.OpenPPG-CESP32S3-CAN-SP140 @@ -1068,7 +1068,7 @@ CONFIG_FREERTOS_ASSERT_FAIL_ABORT=y # CONFIG_FREERTOS_ASSERT_FAIL_PRINT_CONTINUE is not set # CONFIG_FREERTOS_ASSERT_DISABLE is not set CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536 -CONFIG_FREERTOS_ISR_STACKSIZE=1536 +CONFIG_FREERTOS_ISR_STACKSIZE=2096 # CONFIG_FREERTOS_LEGACY_HOOKS is not set CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index f7afbdf..4fde647 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -402,8 +402,12 @@ void requestFastConnParams() { if (pServer == nullptr || !deviceConnected) { return; } - // Tighten to 15ms interval for OTA throughput - pServer->updateConnParams(activeConnHandle, 12, 12, 0, 200); + // Tighten to 15ms interval for OTA throughput, but lengthen the supervision + // timeout to 8s (was 2s). A multi-second flash-erase stall or a sluggish phone + // must not drop the link at the link-layer level mid-flash. Slower recovery of + // a genuinely dead link is acceptable (OTA_TIMEOUT_MS=30s is the backstop); + // reliability of the flash matters more than fast dead-link detection. + pServer->updateConnParams(activeConnHandle, 12, 12, 0, 800); } void requestNormalConnParams() { diff --git a/src/sp140/ble/fastlink_service.cpp b/src/sp140/ble/fastlink_service.cpp index cb43521..b89a775 100644 --- a/src/sp140/ble/fastlink_service.cpp +++ b/src/sp140/ble/fastlink_service.cpp @@ -19,6 +19,12 @@ uint32_t gFastLinkSkippedNoConnCount = 0; uint32_t gFastLinkSkippedOtaCount = 0; uint32_t gFastLinkLastStatsMs = 0; uint32_t gFastLinkPacketId = 0; +// During OTA the normal ~50Hz telemetry stream is suppressed, but we still emit +// a low-rate keepalive so the phone's connection-health watchdog sees the link +// as alive (otherwise the central tears it down mid-flash -> HCI 0x13 / +// reason=531, aborting OTA ~20% in). +uint32_t gFastLinkLastOtaKeepaliveMs = 0; +constexpr uint32_t kOtaKeepaliveIntervalMs = 1000; constexpr uint32_t kFastLinkTelemetryProperties = NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | @@ -140,7 +146,24 @@ void updateFastLinkTelemetry(const BLE_FastLink_Telemetry &data) { pFastLinkCharacteristic->setValue((uint8_t *)&data, sizeof(BLE_FastLink_Telemetry)); if (isOtaInProgress()) { - ++gFastLinkSkippedOtaCount; + // Suppress the full ~50Hz stream during OTA to give the flash bandwidth, + // but emit a ~1Hz keepalive notify so the central keeps the link up. The + // keepalive ships the packet just setValue()'d above, whose advancing + // packet_id/uptime_ms the app counts as telemetry progress. Wrap-safe + // unsigned subtraction. At 1Hz vs the 15ms OTA interval it does not + // meaningfully slow the flash. + const uint32_t nowMs = millis(); + if (deviceConnected && + (nowMs - gFastLinkLastOtaKeepaliveMs >= kOtaKeepaliveIntervalMs)) { + gFastLinkLastOtaKeepaliveMs = nowMs; + if (pFastLinkCharacteristic->notify()) { + ++gFastLinkNotifyOkCount; + } else { + ++gFastLinkNotifyFailCount; + } + } else { + ++gFastLinkSkippedOtaCount; + } } else if (deviceConnected) { const bool sent = pFastLinkCharacteristic->notify(); if (sent) { diff --git a/src/sp140/bms.cpp b/src/sp140/bms.cpp index 77664fc..ce552c3 100644 --- a/src/sp140/bms.cpp +++ b/src/sp140/bms.cpp @@ -50,8 +50,6 @@ void updateBMSData() { if (bms_can == nullptr || !bmsCanInitialized) return; // TODO track bms incrementing cycle count - // Ensure display CS is deselected and BMS CS is selected - digitalWrite(displayCS, HIGH); // Take the shared SPI mutex to prevent contention with TFT flush if (spiBusMutex != NULL) { if (xSemaphoreTake(spiBusMutex, pdMS_TO_TICKS(150)) != pdTRUE) { @@ -60,12 +58,25 @@ void updateBMSData() { return; // Use stale BMS data this cycle rather than hang } } + // Ensure display CS is deselected and BMS CS is selected. This must happen + // only AFTER the bus mutex is held: writing displayCS while a TFT flush is + // streaming pixels deselects the panel mid-transfer and the rest of that + // flush is silently lost, leaving stale bands on screen. + digitalWrite(displayCS, HIGH); digitalWrite(bmsCS, LOW); // USBSerial.println("Updating BMS Data"); unsigned long tStart = millis(); bms_can->update(); + // All SPI traffic happens inside update() — the getters below only read + // values the library cached in RAM. Release the bus first so a pending + // display flush waits only for the CAN drain, not the whole copy-out. + digitalWrite(bmsCS, HIGH); + if (spiBusMutex != NULL) { + xSemaphoreGive(spiBusMutex); + } + // Basic measurements bmsTelemetryData.battery_voltage = bms_can->getBatteryVoltage(); bmsTelemetryData.battery_current = bms_can->getBatteryCurrent(); @@ -139,12 +150,6 @@ void updateBMSData() { USBSerial.println("ms"); } // printBMSData(); - - // Deselect BMS CS when done and release mutex - digitalWrite(bmsCS, HIGH); - if (spiBusMutex != NULL) { - xSemaphoreGive(spiBusMutex); - } } void printBMSData() { diff --git a/src/sp140/lvgl/lvgl_core.cpp b/src/sp140/lvgl/lvgl_core.cpp index b4df9d3..e115cba 100644 --- a/src/sp140/lvgl/lvgl_core.cpp +++ b/src/sp140/lvgl/lvgl_core.cpp @@ -8,13 +8,20 @@ lv_display_t* main_display = nullptr; static uint8_t buf[LVGL_BUF_BYTES]; static uint8_t buf2[LVGL_BUF_BYTES]; Adafruit_ST7735* tft_driver = nullptr; -uint32_t lvgl_last_update = 0; // Define the shared SPI bus mutex SemaphoreHandle_t spiBusMutex = NULL; +// Set when a flush had to be skipped because the SPI bus was busy. LVGL is +// told the flush succeeded (required to keep rendering), so the skipped area +// would otherwise never be repainted — updateLvgl() checks this flag and +// forces a full repaint to recover. +static volatile bool flushSkipped = false; void setupLvglBuffer() { // Initialize LVGL library lv_init(); + // Let LVGL read time directly so animation/refresh pacing stays accurate + // regardless of how often lv_timer_handler() gets called. + lv_tick_set_cb([]() -> uint32_t { return millis(); }); } void setupLvglDisplay( @@ -77,7 +84,10 @@ void lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { if (xSemaphoreTake(spiBusMutex, pdMS_TO_TICKS(200)) != pdTRUE) { // SPI bus timeout - BMS might be doing long operation, skip display flush USBSerial.println("[DISPLAY] SPI bus timeout - skipping display flush"); - // Must still signal LVGL that flush is done to avoid deadlock + // Must still signal LVGL that flush is done to avoid deadlock. That + // marks the area clean without it ever reaching the panel, so flag it + // for a recovery repaint (handled in updateLvgl). + flushSkipped = true; lv_display_flush_ready(disp); return; } @@ -103,26 +113,20 @@ void lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { lv_display_flush_ready(disp); } -// LVGL tick handler - to be called from timer or in main loop -void lv_tick_handler() { - static uint32_t last_tick = 0; - uint32_t current_ms = millis(); - - if (current_ms - last_tick > 5) { // 5ms tick rate for LVGL - lv_tick_inc(current_ms - last_tick); - last_tick = current_ms; - } -} - -// Update LVGL - call this regularly +// Update LVGL - call this regularly with lvglMutex held. LVGL paces actual +// redraws internally (LV_DEF_REFR_PERIOD), so no extra gating is needed here. void updateLvgl() { - uint32_t current_ms = millis(); - - // Update LVGL at the defined refresh rate - if (current_ms - lvgl_last_update > LVGL_REFRESH_TIME) { - lv_tick_handler(); - lv_timer_handler(); - lvgl_last_update = current_ms; + lv_timer_handler(); + + // A flush was dropped because the SPI bus was busy: LVGL believes those + // pixels reached the panel, so invalidate everything once to repaint any + // stale content on the next refresh cycle. + if (flushSkipped) { + flushSkipped = false; + lv_obj_t* screen = lv_screen_active(); + if (screen != NULL) { + lv_obj_invalidate(screen); + } } } diff --git a/src/sp140/lvgl/lvgl_main_screen.cpp b/src/sp140/lvgl/lvgl_main_screen.cpp index e0d75ff..e0d2dca 100644 --- a/src/sp140/lvgl/lvgl_main_screen.cpp +++ b/src/sp140/lvgl/lvgl_main_screen.cpp @@ -1,5 +1,6 @@ #include "../../../inc/sp140/lvgl/lvgl_main_screen.h" #include "../../../inc/sp140/lvgl/lvgl_alerts.h" +#include "../../../inc/sp140/lvgl/lvgl_updates.h" #include "../../../inc/sp140/esp32s3-config.h" #include "../../assets/img/cruise-control-340255-30.c" // Cruise control icon // NOLINT(build/include) @@ -106,6 +107,10 @@ void setupMainScreen(bool darkMode) { lv_obj_delete(main_screen); } + // Fresh widgets have no cached update state — forget the last-applied values + // so updateLvglMainScreen() re-applies styles/positions to the new objects. + resetLvglUpdateCache(); + // Create main screen main_screen = lv_obj_create(NULL); diff --git a/src/sp140/lvgl/lvgl_updates.cpp b/src/sp140/lvgl/lvgl_updates.cpp index 350740f..43f7195 100644 --- a/src/sp140/lvgl/lvgl_updates.cpp +++ b/src/sp140/lvgl/lvgl_updates.cpp @@ -7,6 +7,63 @@ #include "../../../inc/sp140/ble.h" #include "../../../inc/sp140/ble/ble_core.h" #include +#include + +// --------------------------------------------------------------------------- +// Change-detecting setters for the per-frame update path. +// LVGL invalidates (and re-flushes over SPI) any object touched by +// lv_label_set_text() or a style setter even when the new value is identical +// to the current one. updateLvglMainScreen() runs every UI frame, so +// unguarded setters redraw nearly the whole screen at the full frame rate. +// These helpers skip the LVGL call when nothing actually changed, so only +// areas with new content get invalidated and flushed. + +static void setLabelText(lv_obj_t* label, const char* text) { + if (label == NULL || text == NULL) return; + const char* current = lv_label_get_text(label); + if (current != NULL && strcmp(current, text) == 0) return; + lv_label_set_text(label, text); +} + +static void setLabelTextColor(lv_obj_t* label, lv_color_t color) { + if (label == NULL) return; + if (lv_color_eq(lv_obj_get_style_text_color(label, LV_PART_MAIN), color)) return; + lv_obj_set_style_text_color(label, color, 0); +} + +static void setBgColor(lv_obj_t* obj, lv_color_t color, lv_part_t part) { + if (obj == NULL) return; + if (lv_color_eq(lv_obj_get_style_bg_color(obj, part), color)) return; + lv_obj_set_style_bg_color(obj, color, part); +} + +static void setBgOpa(lv_obj_t* obj, lv_opa_t opa) { + if (obj == NULL) return; + if (lv_obj_get_style_bg_opa(obj, LV_PART_MAIN) == opa) return; + lv_obj_set_style_bg_opa(obj, opa, LV_PART_MAIN); +} + +static void setImageRecolor(lv_obj_t* obj, lv_color_t color) { + if (obj == NULL) return; + if (lv_color_eq(lv_obj_get_style_image_recolor(obj, LV_PART_MAIN), color)) return; + lv_obj_set_style_image_recolor(obj, color, LV_PART_MAIN); +} + +// Last-applied UI state that cannot be read back from the widgets (label +// alignment mode, which bg style is attached). Kept at file scope so +// resetLvglUpdateCache() can invalidate it whenever the screen widgets are +// (re)created - e.g. setupMainScreen() on boot or between screenshot tests. +static int8_t lastVoltLeftMode = -1; // 0 = BMS, 1 = "NO BMS", 2 = none +static uint8_t lastBattTempLevel = 0xFF; // 0 = normal, 1 = warning, 2 = critical +static uint8_t lastEscTempLevel = 0xFF; +static uint8_t lastMotorTempLevel = 0xFF; + +void resetLvglUpdateCache() { + lastVoltLeftMode = -1; + lastBattTempLevel = 0xFF; + lastEscTempLevel = 0xFF; + lastMotorTempLevel = 0xFF; +} // Flash timer globals - definitions lv_timer_t* cruise_flash_timer = NULL; @@ -350,13 +407,6 @@ void updateClimbRateIndicator(float climbRate) { const float kVarioSegment = 3.0f / 6.0f; // m/s per segment (0.5) const float kVarioDeadzone = kVarioSegment / 2.0f; // neutral zone near 0 (0.25 m/s) - // Reset all sections to transparent - for (int i = 0; i < 12; i++) { - if (climb_rate_fill_sections[i] != NULL) { - lv_obj_set_style_bg_opa(climb_rate_fill_sections[i], LV_OPA_0, LV_PART_MAIN); - } - } - // Define colors for positive climb rates (from center upward) lv_color_t positive_colors[6] = { lv_color_make(0, 255, 0), // Bright green @@ -377,6 +427,15 @@ void updateClimbRateIndicator(float climbRate) { lv_color_make(75, 0, 130) // Dark purple (again) }; + // Compute the desired state for all 12 sections, then diff-apply so only + // sections that actually changed are invalidated and redrawn. + lv_opa_t section_opa[12]; + lv_color_t section_color[12]; + for (int i = 0; i < 12; i++) { + section_opa[i] = LV_OPA_0; + section_color[i] = lv_color_black(); // unused while transparent + } + if (climbRate >= kVarioDeadzone) { // Positive climb rate - fill sections above center line int sectionsToFill = (int)(climbRate / kVarioSegment); @@ -384,10 +443,8 @@ void updateClimbRateIndicator(float climbRate) { for (int i = 0; i < sectionsToFill; i++) { int sectionIndex = 5 - i; // Section 5 is closest to center line, going up to 0 - if (climb_rate_fill_sections[sectionIndex] != NULL) { - lv_obj_set_style_bg_opa(climb_rate_fill_sections[sectionIndex], LV_OPA_100, LV_PART_MAIN); - lv_obj_set_style_bg_color(climb_rate_fill_sections[sectionIndex], positive_colors[i], LV_PART_MAIN); - } + section_opa[sectionIndex] = LV_OPA_100; + section_color[sectionIndex] = positive_colors[i]; } } else if (climbRate <= -kVarioDeadzone) { // Negative climb rate - fill sections below center line @@ -396,13 +453,20 @@ void updateClimbRateIndicator(float climbRate) { for (int i = 0; i < sectionsToFill; i++) { int sectionIndex = 6 + i; // Section 6 is closest to center line, going down to 11 - if (climb_rate_fill_sections[sectionIndex] != NULL) { - lv_obj_set_style_bg_opa(climb_rate_fill_sections[sectionIndex], LV_OPA_100, LV_PART_MAIN); - lv_obj_set_style_bg_color(climb_rate_fill_sections[sectionIndex], negative_colors[i], LV_PART_MAIN); - } + section_opa[sectionIndex] = LV_OPA_100; + section_color[sectionIndex] = negative_colors[i]; } } // If |climb rate| is within kVarioDeadzone, no sections are filled (neutral) + + for (int i = 0; i < 12; i++) { + if (climb_rate_fill_sections[i] == NULL) continue; + // Only recolor visible sections; color is irrelevant while transparent + if (section_opa[i] == LV_OPA_100) { + setBgColor(climb_rate_fill_sections[i], section_color[i], LV_PART_MAIN); + } + setBgOpa(climb_rate_fill_sections[i], section_opa[i]); + } } void updateLvglMainScreen( @@ -436,7 +500,7 @@ void updateLvglMainScreen( hasValidBatteryTemp = true; } } - // Show whichever ESC temperature is hotter — capacitor or MOSFET — and color + // Show whichever ESC temperature is hotter - capacitor or MOSFET - and color // it against that sensor's own thresholds (cap and MOSFET have different // limits). Skip a sensor that reads NaN (invalid); if both are invalid // escTemp stays NaN and the tile shows "-". @@ -468,40 +532,49 @@ void updateLvglMainScreen( batteryColor = LVGL_YELLOW; } - lv_obj_set_style_bg_color(battery_bar, batteryColor, LV_PART_INDICATOR); + setBgColor(battery_bar, batteryColor, LV_PART_INDICATOR); // Update battery percentage text char buffer[10]; snprintf(buffer, sizeof(buffer), "%d%%", (int)batteryPercent); - lv_label_set_text(battery_label, buffer); - lv_obj_set_style_text_color(battery_label, LVGL_BLACK, 0); + setLabelText(battery_label, buffer); + setLabelTextColor(battery_label, LVGL_BLACK); } else if (escConnected) { // clear the battery bar, we handle voltage later lv_bar_set_value(battery_bar, 0, LV_ANIM_OFF); } else { lv_bar_set_value(battery_bar, 0, LV_ANIM_OFF); - lv_label_set_text(battery_label, "NO DATA"); - lv_obj_set_style_text_color(battery_label, LVGL_RED, 0); + setLabelText(battery_label, "NO DATA"); + setLabelTextColor(battery_label, LVGL_RED); } // Update left voltage (cell voltage) if (voltage_left_label != NULL) { // Check object exists + // Re-align only on source change: lv_obj_align() invalidates the label + // every call even when the resulting position is identical. if (bmsConnected) { char buffer[10]; snprintf(buffer, sizeof(buffer), "%2.2fv", lowestCellV); - lv_label_set_text(voltage_left_label, buffer); + setLabelText(voltage_left_label, buffer); // Always use black text for better readability - lv_obj_set_style_text_color(voltage_left_label, LVGL_BLACK, 0); - // Restore default position when BMS is connected - lv_obj_align(voltage_left_label, LV_ALIGN_TOP_LEFT, 3, 12); + setLabelTextColor(voltage_left_label, LVGL_BLACK); + if (lastVoltLeftMode != 0) { + // Restore default position when BMS is connected + lv_obj_align(voltage_left_label, LV_ALIGN_TOP_LEFT, 3, 12); + lastVoltLeftMode = 0; + } } else if (escConnected) { - lv_obj_set_style_text_color(voltage_left_label, LVGL_BLACK, 0); - lv_label_set_text(voltage_left_label, "NO\nBMS"); - // Move up by 10 pixels when showing NO BMS - lv_obj_align(voltage_left_label, LV_ALIGN_TOP_LEFT, 3, 2); + setLabelTextColor(voltage_left_label, LVGL_BLACK); + setLabelText(voltage_left_label, "NO\nBMS"); + if (lastVoltLeftMode != 1) { + // Move up by 10 pixels when showing NO BMS + lv_obj_align(voltage_left_label, LV_ALIGN_TOP_LEFT, 3, 2); + lastVoltLeftMode = 1; + } } else { - lv_label_set_text(voltage_left_label, ""); + setLabelText(voltage_left_label, ""); + lastVoltLeftMode = 2; } } @@ -511,29 +584,36 @@ void updateLvglMainScreen( if (bmsConnected) { char buffer[10]; snprintf(buffer, sizeof(buffer), "%2.0fv", totalVolts); - lv_label_set_text(voltage_right_label, buffer); + setLabelText(voltage_right_label, buffer); // Ensure battery label shows percentage if BMS is connected if (batteryPercent >= 0) { char batt_buffer[10]; snprintf(batt_buffer, sizeof(batt_buffer), "%d%%", (int)batteryPercent); - lv_label_set_text(battery_label, batt_buffer); - lv_obj_set_style_text_color(battery_label, LVGL_BLACK, 0); + setLabelText(battery_label, batt_buffer); + setLabelTextColor(battery_label, LVGL_BLACK); } } else if (escConnected) { - lv_label_set_text(voltage_right_label, ""); + setLabelText(voltage_right_label, ""); char buffer[10]; snprintf(buffer, sizeof(buffer), "%2.1fv", totalVolts); - lv_label_set_text(battery_label, buffer); - lv_obj_set_style_text_color(battery_label, LVGL_BLACK, 0); + setLabelText(battery_label, buffer); + setLabelTextColor(battery_label, LVGL_BLACK); } else { - lv_label_set_text(voltage_right_label, ""); + setLabelText(voltage_right_label, ""); } } - // Update power display with individual character positions + // Update power display with individual character positions. + // Compute the desired text for every position first, then diff-apply: the + // old clear-then-set pattern invalidated all 4 labels twice per frame. if (power_char_labels[0] != NULL) { // Check if power display is initialized + const char* power_texts[4] = {"", "", "", ""}; + char power_tens_buf[12]; + char power_ones_buf[12]; + char power_tenths_buf[12]; + if (bmsConnected || escConnected) { float kWatts = unifiedBatteryData.power; @@ -545,11 +625,6 @@ void updateLvglMainScreen( kWatts = 0.0f; } - // Clear all positions first - for (int i = 0; i < 4; i++) { - lv_label_set_text(power_char_labels[i], ""); - } - // Carry-safe rounding to one decimal place (e.g., 9.95 -> 10.0) int tenths_total = static_cast(kWatts * 10.0f + 0.5f); int whole_part = tenths_total / 10; @@ -559,39 +634,35 @@ void updateLvglMainScreen( int tens = whole_part / 10; int ones = whole_part % 10; - // Populate character positions using static buffers - static char power_digit_buffers[4][12]; - // Tens digit (only show if >= 10kW) if (tens > 0) { - snprintf(power_digit_buffers[0], sizeof(power_digit_buffers[0]), "%d", tens); - lv_label_set_text(power_char_labels[0], power_digit_buffers[0]); + snprintf(power_tens_buf, sizeof(power_tens_buf), "%d", tens); + power_texts[0] = power_tens_buf; } // Ones digit (always show) - snprintf(power_digit_buffers[1], sizeof(power_digit_buffers[1]), "%d", ones); - lv_label_set_text(power_char_labels[1], power_digit_buffers[1]); + snprintf(power_ones_buf, sizeof(power_ones_buf), "%d", ones); + power_texts[1] = power_ones_buf; // Decimal point - lv_label_set_text(power_char_labels[2], "."); + power_texts[2] = "."; // Tenths digit - snprintf(power_digit_buffers[3], sizeof(power_digit_buffers[3]), "%d", decimal_part); - lv_label_set_text(power_char_labels[3], power_digit_buffers[3]); + snprintf(power_tenths_buf, sizeof(power_tenths_buf), "%d", decimal_part); + power_texts[3] = power_tenths_buf; // Unit label is static and already set to "kW" - } else { - // Clear all positions when no data - for (int i = 0; i < 4; i++) { - lv_label_set_text(power_char_labels[i], ""); - } - // Keep unit label visible even when no data + } + // else: all positions clear when no data; unit label stays visible + + for (int i = 0; i < 4; i++) { + setLabelText(power_char_labels[i], power_texts[i]); } } // Update performance mode - Re-added this section if (perf_mode_label != NULL) { // Check object exists - lv_label_set_text(perf_mode_label, deviceData.performance_mode == 0 ? "CHILL " : "SPORT"); + setLabelText(perf_mode_label, deviceData.performance_mode == 0 ? "CHILL " : "SPORT"); } // Update armed time @@ -603,19 +674,20 @@ void updateLvglMainScreen( const int sessionSeconds = (armedStartMillis > 0) ? ((_lastArmedMillis - armedStartMillis) / 1000) : 0; char timeBuffer[10]; snprintf(timeBuffer, sizeof(timeBuffer), "%02d:%02d", sessionSeconds / 60, sessionSeconds % 60); - lv_label_set_text(armed_time_label, timeBuffer); + setLabelText(armed_time_label, timeBuffer); } - // Update altitude - populate individual character positions + // Update altitude - compute the desired text for all 7 character positions + // first, then diff-apply so unchanged digits are not redrawn. (The old + // clear-then-set pattern invalidated all 7 labels twice per frame.) + const char* altitude_texts[7] = {"", "", "", "", "", "", ""}; + char altitude_digit_buffers[7][12]; + if (altitude == __FLT_MIN__) { // Show error in first few positions - lv_label_set_text(altitude_char_labels[0], "E"); - lv_label_set_text(altitude_char_labels[1], "R"); - lv_label_set_text(altitude_char_labels[2], "R"); - lv_label_set_text(altitude_char_labels[3], ""); - lv_label_set_text(altitude_char_labels[4], ""); - lv_label_set_text(altitude_char_labels[5], ""); - lv_label_set_text(altitude_char_labels[6], ""); + altitude_texts[0] = "E"; + altitude_texts[1] = "R"; + altitude_texts[2] = "R"; } else { if (deviceData.metric_alt) { // Display meters if metric altitude is enabled // Explicitly handle small negative values to avoid "-0.0" @@ -638,14 +710,6 @@ void updateLvglMainScreen( int tens = (whole_part / 10) % 10; int ones = whole_part % 10; - // Populate each character position using static character buffers - static char digit_buffers[7][12]; // Static buffers for single digits - - // Clear all positions first - for (int i = 0; i < 7; i++) { - lv_label_set_text(altitude_char_labels[i], ""); - } - // Determine the leftmost digit position needed int leftmost_pos = 3; // ones position (always needed) if (whole_part >= 10) leftmost_pos = 2; // tens @@ -654,43 +718,45 @@ void updateLvglMainScreen( // Place negative sign if needed (one position left of leftmost digit) if (isNegative && leftmost_pos > 0) { - lv_label_set_text(altitude_char_labels[leftmost_pos - 1], "-"); + altitude_texts[leftmost_pos - 1] = "-"; } // Thousands digit if (thousands > 0) { - snprintf(digit_buffers[0], sizeof(digit_buffers[0]), "%d", thousands); - lv_label_set_text(altitude_char_labels[0], digit_buffers[0]); + snprintf(altitude_digit_buffers[0], sizeof(altitude_digit_buffers[0]), "%d", thousands); + altitude_texts[0] = altitude_digit_buffers[0]; } // Hundreds digit if (thousands > 0 || hundreds > 0) { - snprintf(digit_buffers[1], sizeof(digit_buffers[1]), "%d", hundreds); - lv_label_set_text(altitude_char_labels[1], digit_buffers[1]); + snprintf(altitude_digit_buffers[1], sizeof(altitude_digit_buffers[1]), "%d", hundreds); + altitude_texts[1] = altitude_digit_buffers[1]; } // Tens digit if (whole_part >= 10) { - snprintf(digit_buffers[2], sizeof(digit_buffers[2]), "%d", tens); - lv_label_set_text(altitude_char_labels[2], digit_buffers[2]); + snprintf(altitude_digit_buffers[2], sizeof(altitude_digit_buffers[2]), "%d", tens); + altitude_texts[2] = altitude_digit_buffers[2]; } // Ones digit (always show) - snprintf(digit_buffers[3], sizeof(digit_buffers[3]), "%d", ones); - lv_label_set_text(altitude_char_labels[3], digit_buffers[3]); + snprintf(altitude_digit_buffers[3], sizeof(altitude_digit_buffers[3]), "%d", ones); + altitude_texts[3] = altitude_digit_buffers[3]; // Decimal point - lv_label_set_text(altitude_char_labels[4], "."); + altitude_texts[4] = "."; // Tenths digit - snprintf(digit_buffers[5], sizeof(digit_buffers[5]), "%d", decimal_part); - lv_label_set_text(altitude_char_labels[5], digit_buffers[5]); + snprintf(altitude_digit_buffers[5], sizeof(altitude_digit_buffers[5]), "%d", decimal_part); + altitude_texts[5] = altitude_digit_buffers[5]; - // Adjust width of position 4 back to narrow for meters (decimal point) + // Adjust width of position 4 back to narrow for meters (decimal point). + // lv_obj_set_size is internally change-detected, so this is free when + // the unit mode hasn't changed. lv_obj_set_size(altitude_char_labels[4], 8, 30); // decimal_width=8, char_height=30 // Unit - lv_label_set_text(altitude_char_labels[6], "m"); + altitude_texts[6] = "m"; } else { // For feet - no decimal point needed float feet_float = altitude * 3.28084f; @@ -704,16 +770,9 @@ void updateLvglMainScreen( int tens = (feet / 10) % 10; int ones = feet % 10; - static char digit_buffers_ft[7][12]; // Static buffers for feet - // Adjust width of position 4 for feet (should be normal width, not narrow) lv_obj_set_size(altitude_char_labels[4], 17, 30); // char_width=19, char_height=30 - // Clear all positions first - for (int i = 0; i < 7; i++) { - lv_label_set_text(altitude_char_labels[i], ""); - } - // Determine the leftmost digit position needed int leftmost_pos = 4; // ones position (always needed for feet) if (feet >= 10) leftmost_pos = 3; // tens @@ -723,117 +782,152 @@ void updateLvglMainScreen( // Place negative sign if needed (one position left of leftmost digit) if (isNegative && leftmost_pos > 0) { - lv_label_set_text(altitude_char_labels[leftmost_pos - 1], "-"); + altitude_texts[leftmost_pos - 1] = "-"; } // Ten-thousands digit in position 0 if (ten_thousands > 0) { - snprintf(digit_buffers_ft[0], sizeof(digit_buffers_ft[0]), "%d", ten_thousands); - lv_label_set_text(altitude_char_labels[0], digit_buffers_ft[0]); + snprintf(altitude_digit_buffers[0], sizeof(altitude_digit_buffers[0]), "%d", ten_thousands); + altitude_texts[0] = altitude_digit_buffers[0]; } // Thousands digit in position 1 if (ten_thousands > 0 || thousands > 0) { - snprintf(digit_buffers_ft[1], sizeof(digit_buffers_ft[1]), "%d", thousands); - lv_label_set_text(altitude_char_labels[1], digit_buffers_ft[1]); + snprintf(altitude_digit_buffers[1], sizeof(altitude_digit_buffers[1]), "%d", thousands); + altitude_texts[1] = altitude_digit_buffers[1]; } // Hundreds digit in position 2 if (ten_thousands > 0 || thousands > 0 || hundreds > 0) { - snprintf(digit_buffers_ft[2], sizeof(digit_buffers_ft[2]), "%d", hundreds); - lv_label_set_text(altitude_char_labels[2], digit_buffers_ft[2]); + snprintf(altitude_digit_buffers[2], sizeof(altitude_digit_buffers[2]), "%d", hundreds); + altitude_texts[2] = altitude_digit_buffers[2]; } // Tens digit in position 3 if (feet >= 10) { - snprintf(digit_buffers_ft[3], sizeof(digit_buffers_ft[3]), "%d", tens); - lv_label_set_text(altitude_char_labels[3], digit_buffers_ft[3]); + snprintf(altitude_digit_buffers[3], sizeof(altitude_digit_buffers[3]), "%d", tens); + altitude_texts[3] = altitude_digit_buffers[3]; } // Ones digit in position 4 (always show) - now has normal width - snprintf(digit_buffers_ft[4], sizeof(digit_buffers_ft[4]), "%d", ones); - lv_label_set_text(altitude_char_labels[4], digit_buffers_ft[4]); + snprintf(altitude_digit_buffers[4], sizeof(altitude_digit_buffers[4]), "%d", ones); + altitude_texts[4] = altitude_digit_buffers[4]; // Feet unit in position 6 (same as meters, with proper spacing) - lv_label_set_text(altitude_char_labels[6], "f"); + altitude_texts[6] = "f"; } } - // Update temperature labels + for (int i = 0; i < 7; i++) { + setLabelText(altitude_char_labels[i], altitude_texts[i]); + } + + // Update temperature labels. + // Tile background styles (normal/warning/critical) are applied only on + // level transitions: add/remove_style invalidates the tile on every call, + // which used to redraw active warning tiles every single frame. // -- Battery Temperature -- if (batt_temp_label != NULL && batt_letter_label != NULL) { // Check labels exist - lv_obj_remove_style(batt_temp_bg, &style_warning, 0); - lv_obj_remove_style(batt_temp_bg, &style_critical, 0); - + uint8_t battTempLevel = 0; // 0 = normal/no data, 1 = warning, 2 = critical if (bmsTelemetry.bmsState == TelemetryState::CONNECTED && hasValidBatteryTemp) { - lv_label_set_text_fmt(batt_temp_label, "%d", static_cast(batteryTemp)); + char buffer[12]; + snprintf(buffer, sizeof(buffer), "%d", static_cast(batteryTemp)); + setLabelText(batt_temp_label, buffer); if (batteryTemp >= bmsCellTempThresholds.critHigh) { + battTempLevel = 2; + } else if (batteryTemp >= bmsCellTempThresholds.warnHigh) { + battTempLevel = 1; + } + } else { + // No valid cell probe connected: show "-" instead of a fake low reading. + setLabelText(batt_temp_label, "-"); + } + + if (battTempLevel != lastBattTempLevel) { + lv_obj_remove_style(batt_temp_bg, &style_warning, 0); + lv_obj_remove_style(batt_temp_bg, &style_critical, 0); + if (battTempLevel == 2) { lv_obj_add_style(batt_temp_bg, &style_critical, 0); lv_obj_remove_flag(batt_temp_bg, LV_OBJ_FLAG_HIDDEN); - } else if (batteryTemp >= bmsCellTempThresholds.warnHigh) { + } else if (battTempLevel == 1) { lv_obj_add_style(batt_temp_bg, &style_warning, 0); lv_obj_remove_flag(batt_temp_bg, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(batt_temp_bg, LV_OBJ_FLAG_HIDDEN); } - } else { - // No valid cell probe connected: show "-" instead of a fake low reading. - lv_label_set_text(batt_temp_label, "-"); - lv_obj_add_flag(batt_temp_bg, LV_OBJ_FLAG_HIDDEN); + lastBattTempLevel = battTempLevel; } } // -- ESC Temperature -- if (esc_temp_label != NULL && esc_letter_label != NULL) { // Check labels exist - lv_obj_remove_style(esc_temp_bg, &style_warning, 0); - lv_obj_remove_style(esc_temp_bg, &style_critical, 0); - + uint8_t escTempLevel = 0; if (escTelemetry.escState == TelemetryState::CONNECTED && !isnan(escTemp)) { - lv_label_set_text_fmt(esc_temp_label, "%d", static_cast(escTemp)); - + char buffer[12]; + snprintf(buffer, sizeof(buffer), "%d", static_cast(escTemp)); + setLabelText(esc_temp_label, buffer); if (escTemp >= escTempThresholds->critHigh) { + escTempLevel = 2; + } else if (escTemp >= escTempThresholds->warnHigh) { + escTempLevel = 1; + } + } else { + setLabelText(esc_temp_label, "-"); + } + + if (escTempLevel != lastEscTempLevel) { + lv_obj_remove_style(esc_temp_bg, &style_warning, 0); + lv_obj_remove_style(esc_temp_bg, &style_critical, 0); + if (escTempLevel == 2) { lv_obj_add_style(esc_temp_bg, &style_critical, 0); lv_obj_remove_flag(esc_temp_bg, LV_OBJ_FLAG_HIDDEN); - } else if (escTemp >= escTempThresholds->warnHigh) { + } else if (escTempLevel == 1) { lv_obj_add_style(esc_temp_bg, &style_warning, 0); lv_obj_remove_flag(esc_temp_bg, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(esc_temp_bg, LV_OBJ_FLAG_HIDDEN); } - } else { - lv_label_set_text(esc_temp_label, "-"); - lv_obj_add_flag(esc_temp_bg, LV_OBJ_FLAG_HIDDEN); + lastEscTempLevel = escTempLevel; } } // -- Motor Temperature -- if (motor_temp_label != NULL && motor_letter_label != NULL) { // Check labels exist - lv_obj_remove_style(motor_temp_bg, &style_warning, 0); - lv_obj_remove_style(motor_temp_bg, &style_critical, 0); - + uint8_t motorTempLevel = 0; // Show motor temp only for a valid numeric reading while ESC is connected. if (escTelemetry.escState == TelemetryState::CONNECTED && !isnan(motorTemp)) { - lv_label_set_text_fmt(motor_temp_label, "%d", static_cast(motorTemp)); - + char buffer[12]; + snprintf(buffer, sizeof(buffer), "%d", static_cast(motorTemp)); + setLabelText(motor_temp_label, buffer); if (motorTemp >= motorTempThresholds.critHigh) { + motorTempLevel = 2; + } else if (motorTemp >= motorTempThresholds.warnHigh) { + motorTempLevel = 1; + } + } else { + setLabelText(motor_temp_label, "-"); + } + + if (motorTempLevel != lastMotorTempLevel) { + lv_obj_remove_style(motor_temp_bg, &style_warning, 0); + lv_obj_remove_style(motor_temp_bg, &style_critical, 0); + if (motorTempLevel == 2) { lv_obj_add_style(motor_temp_bg, &style_critical, 0); lv_obj_remove_flag(motor_temp_bg, LV_OBJ_FLAG_HIDDEN); - } else if (motorTemp >= motorTempThresholds.warnHigh) { + } else if (motorTempLevel == 1) { lv_obj_add_style(motor_temp_bg, &style_warning, 0); lv_obj_remove_flag(motor_temp_bg, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(motor_temp_bg, LV_OBJ_FLAG_HIDDEN); } - } else { - lv_label_set_text(motor_temp_label, "-"); - lv_obj_add_flag(motor_temp_bg, LV_OBJ_FLAG_HIDDEN); + lastMotorTempLevel = motorTempLevel; } } // Update armed indicator if (armed) { // Set background to CYAN when armed, regardless of cruise state - lv_obj_set_style_bg_color(arm_indicator, LVGL_CYAN, LV_PART_MAIN); + setBgColor(arm_indicator, LVGL_CYAN, LV_PART_MAIN); lv_obj_remove_flag(arm_indicator, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(arm_indicator, LV_OBJ_FLAG_HIDDEN); @@ -848,7 +942,7 @@ void updateLvglMainScreen( lv_obj_add_flag(cruise_icon_img, LV_OBJ_FLAG_HIDDEN); } // Restore original color after flashing is done - lv_obj_set_style_image_recolor(cruise_icon_img, original_cruise_icon_color, LV_PART_MAIN); + setImageRecolor(cruise_icon_img, original_cruise_icon_color); } // Update Charging Icon Visibility - only when BMS is connected and reports charging @@ -866,11 +960,11 @@ void updateLvglMainScreen( if (!isFlashingArmFailIcon && arm_fail_warning_icon_img != NULL) { lv_obj_add_flag(arm_fail_warning_icon_img, LV_OBJ_FLAG_HIDDEN); // Ensure color is reset if flashing ended abruptly elsewhere (though cb should handle it) - lv_obj_set_style_image_recolor(arm_fail_warning_icon_img, original_arm_fail_icon_color, LV_PART_MAIN); + setImageRecolor(arm_fail_warning_icon_img, original_arm_fail_icon_color); } // Keep the BLE icon synced from the UI task so missed callback updates recover. - // Pairing mode flash takes priority over the solid-connected state — the + // Pairing mode flash takes priority over the solid-connected state - the // flashing Bluetooth symbol communicates "ready to pair" for the entire 60s // window, even after the phone has connected. Once the window ends the icon // settles solid (if still connected) or hides. diff --git a/src/sp140/main.cpp b/src/sp140/main.cpp index 4c64584..dcebf02 100644 --- a/src/sp140/main.cpp +++ b/src/sp140/main.cpp @@ -469,10 +469,10 @@ void monitoringTask(void *pvParameters) { } } -// UI task: fixed 20 Hz refresh and snapshot publish +// UI task: fixed ~30 Hz refresh and snapshot publish void uiTask(void *pvParameters) { TickType_t lastWake = xTaskGetTickCount(); - const TickType_t uiTicks = pdMS_TO_TICKS(50); // 20 Hz + const TickType_t uiTicks = pdMS_TO_TICKS(33); // ~30 Hz, matches LV_DEF_REFR_PERIOD for (;;) { refreshDisplay(); lastUiRunMs = millis(); diff --git a/test/test_screenshots/emulator_display.cpp b/test/test_screenshots/emulator_display.cpp index 307385f..b0fce4a 100644 --- a/test/test_screenshots/emulator_display.cpp +++ b/test/test_screenshots/emulator_display.cpp @@ -20,7 +20,6 @@ static uint8_t lvgl_buf1[SCREEN_WIDTH * SCREEN_HEIGHT * 2] __attribute__((aligne lv_display_t* main_display = nullptr; int8_t displayCS = -1; Adafruit_ST7735* tft_driver = nullptr; -uint32_t lvgl_last_update = 0; SemaphoreHandle_t spiBusMutex = nullptr; // Define lvglMutex from lvgl_updates.h