From 559fb85bed088e0eb368faf8c4dd3472ecc87c44 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Sat, 16 Aug 2025 11:36:15 -0500 Subject: [PATCH 1/4] Update for Arduino-ESP32 3.x API and improve ESC init Refactor buzzer and vibration motor initialization to use the new ledcAttach API from Arduino-ESP32 3.x. Update BLE characteristic callbacks to use String instead of std::string for compatibility. Improve ESC initialization by deferring throttle setup until after the first CAN process and add a readiness check. Update watchdog initialization to use the new esp_task_wdt_init config structure. Minor fix to pin initialization order in setup. --- platformio.ini | 3 ++- src/sp140/buzzer.cpp | 12 ++++++------ src/sp140/esc.cpp | 14 +++++++++----- src/sp140/extra-data.ino | 16 ++++++++-------- src/sp140/sp140.ino | 12 ++++++++++-- src/sp140/vibration_pwm.cpp | 4 ++-- 6 files changed, 37 insertions(+), 24 deletions(-) diff --git a/platformio.ini b/platformio.ini index 30f4421..274acb3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,7 +21,7 @@ lib_ignore = [env:OpenPPG-CESP32S3-CAN-SP140] -platform = espressif32@6.11.0 +platform = https://github.com/pioarduino/platform-espressif32.git#develop board = m5stack-stamps3 framework = arduino src_folder = sp140 @@ -35,6 +35,7 @@ build_flags = -D LV_CONF_INCLUDE_SIMPLE -I inc/sp140/lvgl -D LV_LVGL_H_INCLUDE_SIMPLE + -D USBSerial=Serial build_type = debug debug_speed = 12000 diff --git a/src/sp140/buzzer.cpp b/src/sp140/buzzer.cpp index de65343..28a2447 100644 --- a/src/sp140/buzzer.cpp +++ b/src/sp140/buzzer.cpp @@ -29,9 +29,9 @@ static bool buzzerInitialized = false; * @return Returns true if initialization was successful, false otherwise */ bool initBuzz() { - // Setup LEDC channel for buzzer - ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQUENCY, BUZZER_PWM_RESOLUTION); - ledcAttachPin(board_config.buzzer_pin, BUZZER_PWM_CHANNEL); + // Setup LEDC channel for buzzer (Arduino-ESP32 3.x API) + // Channels are implicitly allocated; attach pin to channel and set frequency/resolution + ledcAttach(board_config.buzzer_pin, BUZZER_PWM_FREQUENCY, BUZZER_PWM_RESOLUTION); buzzerInitialized = true; return true; } @@ -43,9 +43,9 @@ void startTone(uint16_t frequency) { if (!buzzerInitialized || !ENABLE_BUZZ) return; // Change the frequency for this channel - ledcChangeFrequency(BUZZER_PWM_CHANNEL, frequency, BUZZER_PWM_RESOLUTION); + ledcWriteTone(board_config.buzzer_pin, frequency); // Set 50% duty cycle (square wave) - ledcWrite(BUZZER_PWM_CHANNEL, 128); + ledcWrite(board_config.buzzer_pin, 128); } /** @@ -55,7 +55,7 @@ void stopTone() { if (!buzzerInitialized) return; // Set duty cycle to 0 to stop the tone - ledcWrite(BUZZER_PWM_CHANNEL, 0); + ledcWrite(board_config.buzzer_pin, 0); } /** diff --git a/src/sp140/esc.cpp b/src/sp140/esc.cpp index 1a435c8..9fa39f1 100644 --- a/src/sp140/esc.cpp +++ b/src/sp140/esc.cpp @@ -15,6 +15,7 @@ static CanardAdapter adapter; static uint8_t memory_pool[1024] __attribute__((aligned(8))); static SineEsc esc(adapter); static unsigned long lastSuccessfulCommTimeMs = 0; // Store millis() time of last successful ESC comm +static bool escReady = false; STR_ESC_TELEMETRY_140 escTelemetryData = { @@ -36,14 +37,13 @@ void initESC() { } adapter.begin(memory_pool, sizeof(memory_pool)); - adapter.setLocalNodeId(LOCAL_NODE_ID); esc.begin(0x20); // Default ID for the ESC + adapter.setLocalNodeId(LOCAL_NODE_ID); - // Set idle throttle only if ESC is found - const uint16_t IdleThrottle_us = 10000; // 1000us (0.1us resolution) - esc.setThrottleSettings2(IdleThrottle_us); + // Defer sending throttle until after first adapter process to avoid null pointer in CANARD adapter.processTxRxOnce(); - vTaskDelay(pdMS_TO_TICKS(20)); // Wait for ESC to process the command + vTaskDelay(pdMS_TO_TICKS(20)); // Give ESC time to be ready + escReady = true; } /** @@ -54,6 +54,10 @@ void initESC() { * Important: The ESC requires messages at least every 300ms or it will reset */ void setESCThrottle(int throttlePWM) { + // Ensure TWAI/ESC subsystem is initialized + if (!escTwaiInitialized || !escReady) { + return; + } // Input validation if (throttlePWM < 1000 || throttlePWM > 2000) { return; // Ignore invalid throttle values diff --git a/src/sp140/extra-data.ino b/src/sp140/extra-data.ino index 6cf9805..c223619 100644 --- a/src/sp140/extra-data.ino +++ b/src/sp140/extra-data.ino @@ -224,7 +224,7 @@ void updateBMSTelemetry(const STR_BMS_TELEMETRY_140& telemetry) { class MetricAltCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { - std::string value = pCharacteristic->getValue(); + String value = pCharacteristic->getValue(); if (value.length() == 1) { // Ensure we only get a single byte USBSerial.print("New: "); @@ -243,7 +243,7 @@ class MetricAltCallbacks: public BLECharacteristicCallbacks { class PerformanceModeCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { - std::string value = pCharacteristic->getValue(); + String value = pCharacteristic->getValue(); if (value.length() == 1) { uint8_t mode = value[0]; @@ -262,7 +262,7 @@ class PerformanceModeCallbacks: public BLECharacteristicCallbacks { class ScreenRotationCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { - std::string value = pCharacteristic->getValue(); + String value = pCharacteristic->getValue(); if (value.length() == 1) { uint8_t rotation = value[0]; @@ -293,7 +293,7 @@ class ThrottleValueCallbacks: public BLECharacteristicCallbacks { return; // Only allow updates while in cruise mode } - std::string value = pCharacteristic->getValue(); + String value = pCharacteristic->getValue(); if (value.length() == 2) { // Expecting 2 bytes for PWM value uint16_t newPWM = (value[0] << 8) | value[1]; @@ -325,14 +325,14 @@ class MyServerCallbacks: public BLEServerCallbacks { class TimeCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { - std::string value = pCharacteristic->getValue(); + String value = pCharacteristic->getValue(); if (value.length() == sizeof(time_t)) { // Expecting just a unix timestamp struct timeval tv; time_t timestamp; // Copy the incoming timestamp - memcpy(×tamp, value.data(), sizeof(timestamp)); + memcpy(×tamp, value.c_str(), sizeof(timestamp)); // Apply timezone offset timestamp += deviceData.timezone_offset; @@ -360,11 +360,11 @@ class TimeCallbacks: public BLECharacteristicCallbacks { class TimezoneCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { - std::string value = pCharacteristic->getValue(); + String value = pCharacteristic->getValue(); if (value.length() == 4) { // Expecting 4 bytes for timezone offset int32_t offset; - memcpy(&offset, value.data(), sizeof(offset)); + memcpy(&offset, value.c_str(), sizeof(offset)); deviceData.timezone_offset = offset; writeDeviceData(); diff --git a/src/sp140/sp140.ino b/src/sp140/sp140.ino index be6a333..7699f04 100644 --- a/src/sp140/sp140.ino +++ b/src/sp140/sp140.ino @@ -587,7 +587,15 @@ void setupAnalogRead() { void setupWatchdog() { #ifndef OPENPPG_DEBUG // Initialize Task Watchdog - ESP_ERROR_CHECK(esp_task_wdt_init(3000, true)); // 3 second timeout, panic on timeout + esp_task_wdt_config_t twdt_config = { + .timeout_ms = 3000, + .idle_core_mask = 0, // do not subscribe idle tasks + .trigger_panic = true, + }; + esp_err_t wdt_init_result = esp_task_wdt_init(&twdt_config); + if (wdt_init_result != ESP_OK && wdt_init_result != ESP_ERR_INVALID_STATE) { + ESP_ERROR_CHECK(wdt_init_result); + } #endif // OPENPPG_DEBUG } @@ -604,8 +612,8 @@ void setup() { // Pull CSB (pin 42) high to activate I2C mode // temporary fix TODO remove - digitalWrite(42, HIGH); pinMode(42, OUTPUT); + digitalWrite(42, HIGH); // Initialize LVGL mutex before anything else lvglMutex = xSemaphoreCreateMutex(); diff --git a/src/sp140/vibration_pwm.cpp b/src/sp140/vibration_pwm.cpp index 85d8720..c78fcae 100644 --- a/src/sp140/vibration_pwm.cpp +++ b/src/sp140/vibration_pwm.cpp @@ -61,8 +61,8 @@ void vibeTask(void* parameter) { */ bool initVibeMotor() { extern HardwareConfig board_config; - ledcSetup(VIBE_PWM_CHANNEL, VIBE_PWM_FREQ, VIBE_PWM_RESOLUTION); - ledcAttachPin(board_config.vibe_pwm, VIBE_PWM_CHANNEL); + // Arduino-ESP32 3.x LEDC API: use ledcAttach(pin, freq, resolution) + ledcAttach(board_config.vibe_pwm, VIBE_PWM_FREQ, VIBE_PWM_RESOLUTION); // Create vibration queue vibeQueue = xQueueCreate(5, sizeof(VibeRequest)); From 8efb518626967a4b1bf8d493dc614b6f2af24a47 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Sat, 16 Aug 2025 11:42:18 -0500 Subject: [PATCH 2/4] Fix vibration --- src/sp140/vibration_pwm.cpp | 49 ++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/sp140/vibration_pwm.cpp b/src/sp140/vibration_pwm.cpp index c78fcae..2c46b03 100644 --- a/src/sp140/vibration_pwm.cpp +++ b/src/sp140/vibration_pwm.cpp @@ -2,6 +2,8 @@ #include "sp140/esp32s3-config.h" #include "sp140/lvgl/lvgl_updates.h" +extern HardwareConfig board_config; + const int VIBE_PWM_PIN = 46; // TODO: move to config const int VIBE_PWM_FREQ = 1000; // Adjust as needed const int VIBE_PWM_RESOLUTION = 8; // 8-bit resolution @@ -22,9 +24,9 @@ void criticalVibeTask(void* parameter) { for (;;) { if (criticalVibrationActive && ENABLE_VIBE) { // Pulse every 1 second for critical alerts - ledcWrite(VIBE_PWM_CHANNEL, 200); // Medium intensity for continuous + ledcWrite(board_config.vibe_pwm, 200); // Medium intensity for continuous vTaskDelay(pdMS_TO_TICKS(300)); // 300ms on - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); vTaskDelay(pdMS_TO_TICKS(700)); // 700ms off (total 1 second cycle) } else { // If not active, suspend task to save resources @@ -43,13 +45,13 @@ void vibeTask(void* parameter) { if (xQueueReceive(vibeQueue, &request, portMAX_DELAY) == pdTRUE) { if (ENABLE_VIBE) { // Turn on vibration with specified intensity - ledcWrite(VIBE_PWM_CHANNEL, request.intensity); + ledcWrite(board_config.vibe_pwm, request.intensity); // Wait for specified duration vTaskDelay(pdMS_TO_TICKS(request.duration_ms)); // Turn off vibration - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); } } } @@ -60,9 +62,10 @@ void vibeTask(void* parameter) { * @return Returns true if initialization was successful, false otherwise */ bool initVibeMotor() { - extern HardwareConfig board_config; // Arduino-ESP32 3.x LEDC API: use ledcAttach(pin, freq, resolution) + pinMode(board_config.vibe_pwm, OUTPUT); ledcAttach(board_config.vibe_pwm, VIBE_PWM_FREQ, VIBE_PWM_RESOLUTION); + ledcWrite(board_config.vibe_pwm, 0); // Create vibration queue vibeQueue = xQueueCreate(5, sizeof(VibeRequest)); @@ -112,7 +115,7 @@ void stopVibration() { if (vibeQueue != NULL) { xQueueReset(vibeQueue); } - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); } /** @@ -125,10 +128,10 @@ bool runVibePattern(const unsigned int pattern[], int patternSize) { if (!ENABLE_VIBE) return false; for (int i = 0; i < patternSize; i++) { - ledcWrite(VIBE_PWM_CHANNEL, pattern[i]); + ledcWrite(board_config.vibe_pwm, pattern[i]); vTaskDelay(pdMS_TO_TICKS(200)); } - ledcWrite(VIBE_PWM_CHANNEL, 0); // Turn off vibration + ledcWrite(board_config.vibe_pwm, 0); // Turn off vibration return true; } @@ -141,46 +144,46 @@ void executeVibePattern(VibePattern pattern) { switch (pattern) { case VIBE_SHORT_PULSE: - ledcWrite(VIBE_PWM_CHANNEL, 255); + ledcWrite(board_config.vibe_pwm, 255); vTaskDelay(pdMS_TO_TICKS(100)); - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); break; case VIBE_LONG_PULSE: - ledcWrite(VIBE_PWM_CHANNEL, 255); + ledcWrite(board_config.vibe_pwm, 255); vTaskDelay(pdMS_TO_TICKS(500)); - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); break; case VIBE_DOUBLE_PULSE: for (int i = 0; i < 2; i++) { - ledcWrite(VIBE_PWM_CHANNEL, 255); + ledcWrite(board_config.vibe_pwm, 255); vTaskDelay(pdMS_TO_TICKS(150)); - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); vTaskDelay(pdMS_TO_TICKS(150)); } break; case VIBE_TRIPLE_PULSE: for (int i = 0; i < 3; i++) { - ledcWrite(VIBE_PWM_CHANNEL, 255); + ledcWrite(board_config.vibe_pwm, 255); vTaskDelay(pdMS_TO_TICKS(100)); - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); vTaskDelay(pdMS_TO_TICKS(100)); } break; case VIBE_RAMP_UP: for (int i = 0; i <= 255; i += 5) { - ledcWrite(VIBE_PWM_CHANNEL, i); + ledcWrite(board_config.vibe_pwm, i); vTaskDelay(pdMS_TO_TICKS(25)); } - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); break; case VIBE_RAMP_DOWN: for (int i = 255; i >= 0; i -= 5) { - ledcWrite(VIBE_PWM_CHANNEL, i); + ledcWrite(board_config.vibe_pwm, i); vTaskDelay(pdMS_TO_TICKS(25)); } break; @@ -188,10 +191,10 @@ void executeVibePattern(VibePattern pattern) { case VIBE_WAVE: for (int i = 0; i <= 180; i += 5) { int intensity = (sin(i * PI / 180.0) + 1) * 127.5; - ledcWrite(VIBE_PWM_CHANNEL, intensity); + ledcWrite(board_config.vibe_pwm, intensity); vTaskDelay(pdMS_TO_TICKS(20)); } - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); break; } } @@ -206,10 +209,10 @@ void customVibePattern(const uint8_t intensities[], const uint16_t durations[], if (!ENABLE_VIBE) return; for (int i = 0; i < steps; i++) { - ledcWrite(VIBE_PWM_CHANNEL, intensities[i]); + ledcWrite(board_config.vibe_pwm, intensities[i]); vTaskDelay(pdMS_TO_TICKS(durations[i])); } - ledcWrite(VIBE_PWM_CHANNEL, 0); + ledcWrite(board_config.vibe_pwm, 0); } // Service state for critical alerts From 92f2f4ceb28e6771d1e1fbc73e21bdb266d2f26f Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Sat, 16 Aug 2025 12:01:55 -0500 Subject: [PATCH 3/4] Improve critical border flashing and LVGL buffer usage Switch critical border visibility logic from hidden flag to border opacity for smoother flashing and more reliable redraws. Increase LVGL buffer size to full screen to reduce partial update issues. Adjust UI task refresh rate from 25 Hz to 20 Hz for better performance. --- inc/sp140/lvgl/lvgl_core.h | 4 ++-- src/sp140/lvgl/lvgl_main_screen.cpp | 2 +- src/sp140/lvgl/lvgl_updates.cpp | 30 +++++++++++++++-------------- src/sp140/sp140.ino | 2 +- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/inc/sp140/lvgl/lvgl_core.h b/inc/sp140/lvgl/lvgl_core.h index 148e14a..d1097dd 100644 --- a/inc/sp140/lvgl/lvgl_core.h +++ b/inc/sp140/lvgl/lvgl_core.h @@ -13,8 +13,8 @@ #define SCREEN_HEIGHT 128 // LVGL buffer size - optimize for our display -// Use 1/4 of the screen size to balance memory usage and performance -#define LVGL_BUFFER_SIZE (SCREEN_WIDTH * (SCREEN_HEIGHT / 4)) +// Use half screen size for single flush to balance performance and memory usage +#define LVGL_BUFFER_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 2) // LVGL refresh time in ms - match the config file setting #define LVGL_REFRESH_TIME 40 diff --git a/src/sp140/lvgl/lvgl_main_screen.cpp b/src/sp140/lvgl/lvgl_main_screen.cpp index a797143..8ed184b 100644 --- a/src/sp140/lvgl/lvgl_main_screen.cpp +++ b/src/sp140/lvgl/lvgl_main_screen.cpp @@ -624,7 +624,7 @@ void setupMainScreen(bool darkMode) { lv_obj_set_style_border_color(critical_border, LVGL_RED, LV_PART_MAIN); lv_obj_set_style_bg_opa(critical_border, LV_OPA_0, LV_PART_MAIN); // Transparent background lv_obj_set_style_radius(critical_border, 0, LV_PART_MAIN); // Sharp corners - lv_obj_add_flag(critical_border, LV_OBJ_FLAG_HIDDEN); // Initially hidden + lv_obj_set_style_border_opa(critical_border, LV_OPA_0, LV_PART_MAIN); // Initially invisible border // Move border to front so it's visible over all other elements lv_obj_move_foreground(critical_border); } diff --git a/src/sp140/lvgl/lvgl_updates.cpp b/src/sp140/lvgl/lvgl_updates.cpp index e5828ae..a65043d 100644 --- a/src/sp140/lvgl/lvgl_updates.cpp +++ b/src/sp140/lvgl/lvgl_updates.cpp @@ -179,23 +179,23 @@ void startArmFailIconFlash() { static void critical_border_flash_timer_cb(lv_timer_t* timer) { // This callback runs within the LVGL task handler, so no mutex needed here. if (critical_border != NULL) { - // Toggle visibility: 300ms on, 700ms off - bool is_on = !lv_obj_has_flag(critical_border, LV_OBJ_FLAG_HIDDEN); - if (is_on) { - lv_obj_add_flag(critical_border, LV_OBJ_FLAG_HIDDEN); - // Invalidate the border area to ensure clean redraw - lv_obj_invalidate(critical_border); + // Toggle opacity: 300ms on (opaque), 700ms off (transparent) + uint8_t current_opa = lv_obj_get_style_border_opa(critical_border, LV_PART_MAIN); + if (current_opa == LV_OPA_100) { + lv_obj_set_style_border_opa(critical_border, LV_OPA_0, LV_PART_MAIN); lv_timer_set_period(timer, 700); // Off duration + // Invalidate entire screen when hiding to ensure clean removal of border pixels + lv_obj_invalidate(lv_scr_act()); } else { - lv_obj_clear_flag(critical_border, LV_OBJ_FLAG_HIDDEN); - // Invalidate the border area to ensure clean redraw - lv_obj_invalidate(critical_border); + lv_obj_set_style_border_opa(critical_border, LV_OPA_100, LV_PART_MAIN); lv_timer_set_period(timer, 300); // On duration // Trigger vibration pulse in sync with border "on" if (ENABLE_VIBE) { pulseVibration(300, 200); // 300ms pulse, intensity 200 } + // Invalidate the border area when showing + lv_obj_invalidate(critical_border); } // Force immediate refresh to minimize tearing lv_refr_now(lv_disp_get_default()); @@ -206,7 +206,7 @@ void startCriticalBorderFlash() { if (xSemaphoreTake(lvglMutex, pdMS_TO_TICKS(50)) == pdTRUE) { if (critical_border != NULL && !isFlashingCriticalBorder) { isFlashingCriticalBorder = true; - lv_obj_clear_flag(critical_border, LV_OBJ_FLAG_HIDDEN); // Start visible + lv_obj_set_style_border_opa(critical_border, LV_OPA_100, LV_PART_MAIN); // Start visible critical_border_flash_timer = lv_timer_create(critical_border_flash_timer_cb, 300, NULL); } xSemaphoreGive(lvglMutex); @@ -220,7 +220,9 @@ void stopCriticalBorderFlash() { critical_border_flash_timer = NULL; } if (critical_border != NULL) { - lv_obj_add_flag(critical_border, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_style_border_opa(critical_border, LV_OPA_0, LV_PART_MAIN); + // Invalidate entire screen to ensure clean removal + lv_obj_invalidate(lv_scr_act()); } isFlashingCriticalBorder = false; xSemaphoreGive(lvglMutex); @@ -235,7 +237,7 @@ bool isCriticalBorderFlashing() { void startCriticalBorderFlashDirect() { if (critical_border != NULL && !isFlashingCriticalBorder) { isFlashingCriticalBorder = true; - lv_obj_clear_flag(critical_border, LV_OBJ_FLAG_HIDDEN); // Start visible + lv_obj_set_style_border_opa(critical_border, LV_OPA_100, LV_PART_MAIN); // Start visible lv_obj_invalidate(critical_border); // Ensure clean initial draw critical_border_flash_timer = lv_timer_create(critical_border_flash_timer_cb, 300, NULL); // Force immediate refresh for clean start @@ -249,8 +251,8 @@ void stopCriticalBorderFlashDirect() { critical_border_flash_timer = NULL; } if (critical_border != NULL) { - lv_obj_add_flag(critical_border, LV_OBJ_FLAG_HIDDEN); - lv_obj_invalidate(critical_border); // Ensure clean removal + lv_obj_set_style_border_opa(critical_border, LV_OPA_0, LV_PART_MAIN); + lv_obj_invalidate(lv_scr_act()); // Ensure clean removal of border // Force immediate refresh for clean stop lv_refr_now(lv_disp_get_default()); } diff --git a/src/sp140/sp140.ino b/src/sp140/sp140.ino index 7699f04..03920bd 100644 --- a/src/sp140/sp140.ino +++ b/src/sp140/sp140.ino @@ -497,7 +497,7 @@ void monitoringTask(void *pvParameters) { // UI task: fixed 25 Hz refresh and snapshot publish void uiTask(void *pvParameters) { TickType_t lastWake = xTaskGetTickCount(); - const TickType_t uiTicks = pdMS_TO_TICKS(40); // 25 Hz + const TickType_t uiTicks = pdMS_TO_TICKS(50); // 20 Hz for (;;) { refreshDisplay(); pushTelemetrySnapshot(); From 1b03760a0bf1466a1a43b62692adee5d80df4c32 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Mon, 18 Aug 2025 09:55:19 -0500 Subject: [PATCH 4/4] Update SIMPLE_MONITOR_README.md --- SIMPLE_MONITOR_README.md | 153 ++++++++++++++++++++++++++------------- 1 file changed, 104 insertions(+), 49 deletions(-) diff --git a/SIMPLE_MONITOR_README.md b/SIMPLE_MONITOR_README.md index 7289903..dd0a2f7 100644 --- a/SIMPLE_MONITOR_README.md +++ b/SIMPLE_MONITOR_README.md @@ -1,36 +1,66 @@ # Simple Sensor Monitoring System ## Overview -A lightweight, elegant monitoring system for ESP32 electric ultralight sensors. Based on modern C++ patterns with easy extensibility. +A lightweight, category-based monitoring system for ESP32 electric ultralight sensors. Features intelligent alert suppression when controllers disconnect and modern OOP design with device categories. ## Design Philosophy -- **Simple**: ~50 lines of core code vs complex alerting systems -- **Flexible**: Easy to add new sensors and output methods -- **Modern**: Uses `std::function` and lambdas for clean sensor definitions -- **Extensible**: Interface-based design for multiple output channels +- **Safety-First**: Automatically suppresses ESC alerts when ESC disconnected, BMS alerts when BMS disconnected +- **Category-Based**: Organizes sensors by device type (ESC, BMS, ALTIMETER, INTERNAL) for grouped management +- **Modern OOP**: Interface-based design with virtual methods and clean inheritance +- **Thread-Safe**: Queue-based telemetry snapshots for safe multi-task operation ## Usage -### Current ESC Temperature Monitoring +### Device Categories & Alert Suppression ```cpp -// Automatically monitors: +enum class SensorCategory { + ESC, // ESC temps, motor temp, ESC errors + BMS, // Battery voltage, current, temps, SOC + ALTIMETER, // Barometric sensors + INTERNAL // CPU temp, system sensors +}; + +// When ESC disconnects: All ESC alerts automatically cleared +// When BMS disconnects: All BMS alerts automatically cleared +// ALTIMETER/INTERNAL: Always monitored (no connection dependency) +``` + +### Current Monitoring Coverage +```cpp +// ESC Category (suppressed when ESC disconnected): // - ESC MOS: Warning 90°C, Critical 110°C // - ESC MCU: Warning 80°C, Critical 95°C // - ESC CAP: Warning 85°C, Critical 100°C // - Motor: Warning 90°C, Critical 110°C +// - ESC Error Conditions (overcurrent, overtemp, etc.) + +// BMS Category (suppressed when BMS disconnected): +// - Battery voltage, current, SOC, cell voltages +// - BMS temperatures, charge/discharge MOS status ``` ### Adding New Sensors ```cpp -// Example: Battery voltage monitoring -static SensorMonitor batteryVoltage = { - "BatteryVolt", - {.warnLow = 70, .warnHigh = 105, .critLow = 65, .critHigh = 110}, - []() { return bmsTelemetryData.battery_voltage; }, - AlertLevel::OK, - &serialLogger -}; -sensors.push_back(&batteryVoltage); +// Example: New ESC sensor +static SensorMonitor* newEscSensor = new SensorMonitor( + SensorID::ESC_New_Sensor, // Unique ID from enum + SensorCategory::ESC, // Category determines suppression behavior + {.warnLow = 10, .warnHigh = 80, .critLow = 5, .critHigh = 90}, + []() { return escTelemetryData.new_value; }, // Data source + &multiLogger // Fan-out to multiple outputs +); +monitors.push_back(newEscSensor); + +// Example: Boolean error monitor +static BooleanMonitor* newError = new BooleanMonitor( + SensorID::ESC_New_Error, + SensorCategory::ESC, + []() { return checkErrorCondition(); }, + true, // Alert when condition is true + AlertLevel::CRIT_HIGH, + &multiLogger +); +monitors.push_back(newError); ``` ### Example Output @@ -38,6 +68,8 @@ sensors.push_back(&batteryVoltage); [15234] [WARN_HIGH] ESC_MOS_Temp = 92.50 [15467] [CRIT_HIGH] Motor_Temp = 112.30 [15890] [OK] ESC_MOS_Temp = 88.20 +[16123] ESC disconnected - clearing all ESC alerts +[16450] [WARN_LOW] BMS_SOC = 12.3 ``` ## Easy Extensions @@ -45,61 +77,84 @@ sensors.push_back(&batteryVoltage); ### SD Card Logging ```cpp struct SDLogger : ILogger { - void log(const char* name, AlertLevel lvl, float v) override { - // Write timestamp, name, level, value to SD card + void log(SensorID id, AlertLevel lvl, float v) override { File logFile = SD.open("/alerts.log", FILE_APPEND); - logFile.printf("%lu,%s,%d,%.2f\n", millis(), name, (int)lvl, v); + logFile.printf("%lu,%s,%d,%.2f\n", millis(), sensorIDToString(id), (int)lvl, v); logFile.close(); } }; ``` -### Display Alerts +### Custom Alert Processing ```cpp -struct DisplayLogger : ILogger { - void log(const char* name, AlertLevel lvl, float v) override { - // Show alert on LVGL display - if (lvl >= AlertLevel::WARN_HIGH) { - showAlertPopup(name, lvl, v); +struct CustomLogger : ILogger { + void log(SensorID id, AlertLevel lvl, float v) override { + // Custom processing based on sensor category + SensorCategory cat = getSensorCategory(id); + if (cat == SensorCategory::ESC && lvl >= AlertLevel::CRIT_HIGH) { + triggerEmergencyShutdown(); } } }; ``` -### Multiple Outputs +### Multiple Outputs (Built-in) ```cpp -// Log to both serial and SD card -static SerialLogger serialLog; -static SDLogger sdLog; +// MultiLogger automatically fans out to all registered sinks +multiLogger.addSink(&serialLogger); // Console output +multiLogger.addSink(&uiLogger); // LVGL alerts +multiLogger.addSink(&customLogger); // Your custom handler -sensorMonitor.logger = &serialLog; // Or use both with composite pattern +// All monitors automatically use multiLogger ``` ## Architecture ```cpp +IMonitor (interface) +├── virtual SensorID getSensorID() = 0 +├── virtual SensorCategory getCategory() = 0 +└── virtual void check() = 0 + +SensorMonitor : IMonitor BooleanMonitor : IMonitor +├── SensorID id ├── SensorID id +├── SensorCategory category ├── SensorCategory category +├── Thresholds thr ├── std::function read +├── std::function read ├── bool alertOnTrue +└── ILogger* logger └── AlertLevel level + ILogger (interface) -├── SerialLogger (serial console) -├── SDLogger (SD card - future) -└── DisplayLogger (LVGL display - future) - -SensorMonitor -├── name (string identifier) -├── thresholds (warn/crit levels) -├── read (lambda function) -└── logger (output interface) +├── SerialLogger (debug output) +├── AlertUILogger (LVGL display) +└── MultiLogger (fan-out to multiple sinks) + +Categories & Connection Logic: +├── ESC sensors → suppressed when escState != CONNECTED +├── BMS sensors → suppressed when bmsState != CONNECTED +├── ALTIMETER sensors → always active +└── INTERNAL sensors → always active ``` ## Integration -- Monitors check every 40ms in main SPI communication task -- Only logs when alert level changes (no spam) -- Uses existing telemetry data (no additional sensor reads) -- Zero overhead when sensors are in OK state +- **Queue-Based**: Dedicated monitoring task receives telemetry snapshots via FreeRTOS queue +- **Thread-Safe**: No direct access to volatile telemetry data from monitoring task +- **Connection-Aware**: Automatically clears alerts when devices disconnect +- **Smart Suppression**: Only runs ESC monitors when ESC connected, BMS monitors when BMS connected +- **Change Detection**: Only logs when alert level changes (no spam) +- **Zero Overhead**: Minimal CPU usage when all sensors in OK state ## Memory Usage -- ~1KB total code size -- ~200 bytes per monitored sensor -- Minimal RAM footprint -- No dynamic allocation during runtime - -This approach perfectly balances simplicity with extensibility - start with basic serial logging, easily expand to SD cards, displays, or remote monitoring as needed. +- **Code Size**: ~3KB total (includes OOP infrastructure, queue handling, UI integration) +- **Per Sensor**: ~150 bytes (SensorMonitor) or ~120 bytes (BooleanMonitor) +- **Static Allocation**: All monitors allocated at compile-time (embedded-friendly) +- **Queue Memory**: ~200 bytes for telemetry snapshot queue +- **Category Lookup**: O(1) via virtual methods (no external mapping tables) + +## Key Benefits +1. **Safety**: ESC alerts disappear when ESC disconnects (no false warnings during connection issues) +2. **Maintainability**: Add new sensors by category, automatic suppression behavior +3. **Robustness**: Thread-safe design prevents race conditions between telemetry and monitoring +4. **Extensibility**: Clean OOP interfaces for new monitor types and output methods +5. **Performance**: Smart suppression reduces unnecessary checks when devices offline + +This design balances safety, maintainability, and performance - essential for flight-critical embedded systems.