Skip to content
Open
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
4 changes: 1 addition & 3 deletions inc/sp140/lvgl/lvgl_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,20 @@
// 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;

// Core function declarations
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);

Expand Down
4 changes: 4 additions & 0 deletions inc/sp140/lvgl/lvgl_updates.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion sdkconfig.OpenPPG-CESP32S3-CAN-SP140
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/sp140/ble/ble_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
25 changes: 24 additions & 1 deletion src/sp140/ble/fastlink_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 13 additions & 8 deletions src/sp140/bms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -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() {
Expand Down
46 changes: 25 additions & 21 deletions src/sp140/lvgl/lvgl_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/sp140/lvgl/lvgl_main_screen.cpp
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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);

Expand Down
Loading