diff --git a/inc/sp140/altimeter.h b/inc/sp140/altimeter.h index aee7c86..b78ea0d 100644 --- a/inc/sp140/altimeter.h +++ b/inc/sp140/altimeter.h @@ -10,8 +10,8 @@ #define VARIO_BUFFER_SIZE 25 // Number of samples to average for vertical speed #define MAX_VERTICAL_SPEED 250.0f // Maximum vertical speed to display (m/s) -// Set up the barometer -bool setupAltimeter(bool alt_wire = false); +// Set up the barometer +bool setupAltimeter(); // Get the altitude (in meters) float getAltitude(const STR_DEVICE_DATA_140_V1& deviceData); diff --git a/inc/sp140/esc.h b/inc/sp140/esc.h index bcba449..10962c0 100644 --- a/inc/sp140/esc.h +++ b/inc/sp140/esc.h @@ -2,21 +2,26 @@ #define INC_SP140_ESC_H_ #include + +// Motor temp validity range (disconnected/invalid readings are represented as NaN) +constexpr float MOTOR_TEMP_VALID_MIN_C = -20.0f; +constexpr float MOTOR_TEMP_VALID_MAX_C = 140.0f; + +inline bool isMotorTempValidC(float tempC) { + return tempC > MOTOR_TEMP_VALID_MIN_C && tempC <= MOTOR_TEMP_VALID_MAX_C; +} #include "sp140/structs.h" #include "../../inc/sp140/esp32s3-config.h" #include #include -void initESC(); +void initESC(); void setESCThrottle(int throttlePWM); void readESCTelemetry(); bool setupTWAI(); -// Get the highest temperature from all ESC sensors -float getHighestTemp(const STR_ESC_TELEMETRY_140& telemetry); - // ESC Error Decoding Functions -String decodeRunningError(uint16_t errorCode); +String decodeRunningError(uint16_t errorCode); String decodeSelfCheckError(uint16_t errorCode); bool hasRunningError(uint16_t errorCode); bool hasSelfCheckError(uint16_t errorCode); diff --git a/inc/sp140/simple_monitor.h b/inc/sp140/simple_monitor.h index 3795cde..a24f1a4 100644 --- a/inc/sp140/simple_monitor.h +++ b/inc/sp140/simple_monitor.h @@ -3,6 +3,7 @@ #include #include +#include #include #include "sp140/structs.h" @@ -148,6 +149,14 @@ struct SensorMonitor : public IMonitor { void check() override { float v = read(); + if (isnan(v)) { + if (last != AlertLevel::OK) { + logger->log(id, AlertLevel::OK, v); + last = AlertLevel::OK; + } + return; + } + AlertLevel now = AlertLevel::OK; if (v <= thr.critLow) now = AlertLevel::CRIT_LOW; @@ -182,6 +191,14 @@ struct HysteresisSensorMonitor : public SensorMonitor { void check() override { float v = read(); + if (isnan(v)) { + if (last != AlertLevel::OK) { + logger->log(id, AlertLevel::OK, v); + last = AlertLevel::OK; + } + return; + } + AlertLevel now = last; // Start with current state // Hysteresis logic to prevent bouncing diff --git a/inc/sp140/structs.h b/inc/sp140/structs.h index a2031f3..29715c0 100644 --- a/inc/sp140/structs.h +++ b/inc/sp140/structs.h @@ -147,7 +147,7 @@ typedef struct { float mos_temp; // MOSFET temperature (°C) float cap_temp; // Capacitor temperature (°C) float mcu_temp; // MCU temperature (°C) - float motor_temp; // Motor temperature (°C) + float motor_temp; // Motor temperature (°C), NaN if sensor invalid/disconnected int32_t eRPM; // Electrical RPM uint16_t inPWM; // Input PWM value uint16_t running_error; // Runtime error bitmask diff --git a/inc/sp140/throttle.h b/inc/sp140/throttle.h index 6b8f6a6..01b9997 100644 --- a/inc/sp140/throttle.h +++ b/inc/sp140/throttle.h @@ -44,9 +44,22 @@ uint16_t readThrottleRaw(); /** Get the most recently sampled raw throttle value (0..4095). */ uint16_t getLastThrottleRaw(); -/** Convert raw pot reading (0..4095) to PWM microseconds. */ +/** Convert raw pot reading (0..4095) to PWM microseconds (full range). */ int potRawToPwm(uint16_t raw); +/** + * Convert raw pot reading (0..4095) to PWM microseconds using the + * mode-specific maximum. In chill mode the full physical range maps to + * ESC_MIN_PWM..CHILL_MODE_MAX_PWM; in sport mode it maps to the full + * ESC_MIN_PWM..ESC_MAX_PWM range. This gives chill mode full granular + * control over its limited output range instead of a hard clamp. + * + * @param raw Raw ADC value (0..4095) + * @param performance_mode 0 = CHILL, 1 = SPORT + * @return PWM microseconds in the mode's range + */ +int potRawToModePwm(uint16_t raw, uint8_t performance_mode); + /** * Apply mode-based ramp (us/tick) and clamp to the mode's max PWM. * Updates `prevPwm` with the final value. @@ -86,10 +99,13 @@ bool throttleEngaged(); /** * Read throttle input and return smoothed PWM value. * This is the core throttle processing pipeline without any state logic. + * Uses mode-aware mapping so the full physical range covers the mode's + * PWM output range. * + * @param performance_mode 0 = CHILL, 1 = SPORT * @return Smoothed PWM value from throttle input */ -int getSmoothedThrottlePwm(); +int getSmoothedThrottlePwm(uint8_t performance_mode); /** * Reset throttle state for clean startup/disarm. @@ -107,8 +123,8 @@ void handleThrottle(); /** * Calculate the cruise control PWM value from a raw pot reading. - * Uses the same mapping as normal throttle (full range then clamp to mode max). - * Also applies the absolute cruise max cap. + * Uses the same mode-aware mapping as normal throttle, then applies + * the absolute cruise max cap. * * @param potVal Raw potentiometer value (0..4095) * @param performance_mode 0 = CHILL, 1 = SPORT diff --git a/inc/sp140/utilities.h b/inc/sp140/utilities.h index ef1b1ad..3ab1696 100644 --- a/inc/sp140/utilities.h +++ b/inc/sp140/utilities.h @@ -1,15 +1,10 @@ -#ifndef INC_SP140_UTILITIES_H_ -#define INC_SP140_UTILITIES_H_ - -#include - -double mapd(double x, double in_min, double in_max, double out_min, double out_max); - -// Function for digital time display - adds leading zeros -String convertToDigits(byte digits); - -// Function to get unique chip ID -String chipId(); +#ifndef INC_SP140_UTILITIES_H_ +#define INC_SP140_UTILITIES_H_ + +#include + +// Function to get unique chip ID +String chipId(); // Definitions for main rainbow colors in WRGB format for NeoPixel. // The 32-bit color value is WRGB. W (White) is ignored for RGB pixels. diff --git a/inc/sp140/vibration_pwm.h b/inc/sp140/vibration_pwm.h index 2a93198..89be351 100644 --- a/inc/sp140/vibration_pwm.h +++ b/inc/sp140/vibration_pwm.h @@ -74,35 +74,4 @@ void pulseVibration(uint16_t duration_ms, uint8_t intensity); */ void stopVibration(); -/** - * @brief Initializes the critical alert service. - * - * This function sets up the necessary resources for handling synchronized critical alerts. - * It should be called once during system initialization. - */ -void initCriticalAlertService(); - -/** - * @brief Starts the critical alert notifications. - * - * Activates the synchronized vibration and border flashing. If alerts are - * already running, this function has no effect. - */ -void startCriticalAlerts(); - -/** - * @brief Stops the critical alert notifications. - * - * Deactivates the vibration and border flashing. If alerts are not currently - * running, this function has no effect. - */ -void stopCriticalAlerts(); - -/** - * @brief Checks if the critical alert system is currently active. - * - * @return true if critical alerts are active, false otherwise. - */ -bool isCriticalAlertActive(); - #endif // INC_SP140_VIBRATION_PWM_H_ diff --git a/src/sp140/alert_display.cpp b/src/sp140/alert_display.cpp index 20d7cef..ed0c324 100644 --- a/src/sp140/alert_display.cpp +++ b/src/sp140/alert_display.cpp @@ -43,9 +43,6 @@ void initAlertDisplay() { return; } - // Init the new alert service - initCriticalAlertService(); - // Create aggregation task (low priority) xTaskCreate(alertAggregationTask, "AlertAgg", 4096, NULL, 1, &alertAggregationTaskHandle); USBSerial.println("[AlertDisplay] Init complete"); diff --git a/src/sp140/altimeter.cpp b/src/sp140/altimeter.cpp index 5fa7f2a..8b70a0c 100644 --- a/src/sp140/altimeter.cpp +++ b/src/sp140/altimeter.cpp @@ -75,13 +75,11 @@ float getBaroPressure() { return __FLT_MIN__; // Return a very small number if BMP is not present } -// Start the bmp3XX sensor -bool setupAltimeter(bool altWire) { - TwoWire* wire = &Wire; - - // pull down pin 40 to high to set the address - pinMode(40, OUTPUT); - digitalWrite(40, HIGH); +// Start the bmp3XX sensor +bool setupAltimeter() { + // pull down pin 40 to high to set the address + pinMode(40, OUTPUT); + digitalWrite(40, HIGH); if (!bmp.begin_I2C(BMP3XX_DEFAULT_ADDRESS, &Wire)) return false; bmp.setOutputDataRate(BMP3_ODR_25_HZ); diff --git a/src/sp140/ble/esc_service.cpp b/src/sp140/ble/esc_service.cpp index 74a666e..ca6272c 100644 --- a/src/sp140/ble/esc_service.cpp +++ b/src/sp140/ble/esc_service.cpp @@ -92,6 +92,7 @@ void updateESCPackedTelemetry(const STR_ESC_TELEMETRY_140& telemetry) { packet.mos_temp = telemetry.mos_temp; packet.cap_temp = telemetry.cap_temp; packet.mcu_temp = telemetry.mcu_temp; + // motor_temp is NaN when sensor is disconnected/invalid. packet.motor_temp = telemetry.motor_temp; packet.eRPM = static_cast(telemetry.eRPM); packet.inPWM = static_cast(telemetry.inPWM); @@ -118,6 +119,7 @@ void updateESCTelemetryBLE(const STR_ESC_TELEMETRY_140& telemetry) { float voltage = telemetry.volts; float current = telemetry.amps; int32_t rpm = static_cast(telemetry.eRPM); + // motor_temp is NaN when sensor is disconnected/invalid. EscTempsPacket temps = { telemetry.mos_temp, telemetry.cap_temp, diff --git a/src/sp140/esc.cpp b/src/sp140/esc.cpp index 3de2aa1..ed74f25 100644 --- a/src/sp140/esc.cpp +++ b/src/sp140/esc.cpp @@ -94,23 +94,25 @@ void readESCTelemetry() { escTelemetryData.amps = res->bus_current / 10.0f; escTelemetryData.mos_temp = res->mos_temp / 10.0f; escTelemetryData.cap_temp = res->cap_temp / 10.0f; - escTelemetryData.mcu_temp = res->mcu_temp / 10.0f; + escTelemetryData.mcu_temp = res->mcu_temp / 10.0f; // Filter motor temp - only update if sensor is connected (valid range: -20°C to 140°C) // Disconnected sensor reads ~149°C (thermistor pulled high) float rawMotorTemp = res->motor_temp / 10.0f; - if (rawMotorTemp > -20.0f && rawMotorTemp <= 140.0f) { + if (isMotorTempValidC(rawMotorTemp)) { escTelemetryData.motor_temp = rawMotorTemp; + } else { + // Store invalid motor temp as NaN. Downstream consumers can skip on isnan(). + escTelemetryData.motor_temp = NAN; } - // else: keep previous value (sensor disconnected or invalid) escTelemetryData.eRPM = res->speed; escTelemetryData.inPWM = res->recv_pwm / 10.0f; watts = escTelemetryData.amps * escTelemetryData.volts; - // Store error bitmasks - escTelemetryData.running_error = res->running_error; - escTelemetryData.selfcheck_error = res->selfcheck_error; - - // Record the time of this successful communication using the local clock + // Store error bitmasks + escTelemetryData.running_error = res->running_error; + escTelemetryData.selfcheck_error = res->selfcheck_error; + + // Record the time of this successful communication using the local clock lastSuccessfulCommTimeMs = millis(); } // else: Timestamp hasn't changed, treat as stale data, don't update local timer or telemetry @@ -299,19 +301,10 @@ double mapDouble(double x, double in_min, double in_max, double out_min, double return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } -/** - * Get the highest temperature reading from all ESC sensors - * @param telemetry ESC telemetry data structure - * @return The highest temperature value among motor, MOSFET, and capacitor temps - */ -float getHighestTemp(const STR_ESC_TELEMETRY_140& telemetry) { - return max(telemetry.motor_temp, max(telemetry.mos_temp, telemetry.cap_temp)); -} - /** * Decode running error bitmask into human-readable string - * @param errorCode 16-bit running error code from ESC - * @return String containing decoded error messages + * @param errorCode 16-bit running error code from ESC + * @return String containing decoded error messages */ String decodeRunningError(uint16_t errorCode) { if (errorCode == 0) { diff --git a/src/sp140/esc_monitors.cpp b/src/sp140/esc_monitors.cpp index 82ce713..c5c9dd5 100644 --- a/src/sp140/esc_monitors.cpp +++ b/src/sp140/esc_monitors.cpp @@ -51,7 +51,7 @@ void addESCMonitors() { monitors.push_back(escCapTemp); // Motor Temperature Monitor - with hysteresis - // Note: Invalid readings (>140°C = disconnected) are filtered in esc.cpp + // Return NaN for disconnected/invalid sensor so monitor logic skips it. static HysteresisSensorMonitor* motorTemp = new HysteresisSensorMonitor( SensorID::Motor_Temp, SensorCategory::ESC, diff --git a/src/sp140/lvgl/lvgl_updates.cpp b/src/sp140/lvgl/lvgl_updates.cpp index afe8ad8..8897c67 100644 --- a/src/sp140/lvgl/lvgl_updates.cpp +++ b/src/sp140/lvgl/lvgl_updates.cpp @@ -4,6 +4,7 @@ #include "../../../inc/sp140/globals.h" #include "../../../inc/sp140/vibration_pwm.h" #include "../../../inc/sp140/shared-config.h" +#include // Flash timer globals - definitions lv_timer_t* cruise_flash_timer = NULL; @@ -704,9 +705,8 @@ void updateLvglMainScreen( lv_obj_remove_style(motor_temp_bg, &style_warning, 0); lv_obj_remove_style(motor_temp_bg, &style_critical, 0); - // Show motor temp if ESC connected and reading is valid (not disconnected) - // Disconnected sensor reads ~149°C, so treat > 140°C as invalid - if (escTelemetry.escState == TelemetryState::CONNECTED && motorTemp > -20.0f && motorTemp <= 140.0f) { + // 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)); if (motorTemp >= motorTempThresholds.critHigh) { diff --git a/src/sp140/sp140.ino b/src/sp140/sp140.ino index 1a2b3e2..7d44402 100644 --- a/src/sp140/sp140.ino +++ b/src/sp140/sp140.ino @@ -799,7 +799,7 @@ void setup140() { const int SDA_PIN = 44; const int SCL_PIN = 41; Wire.setPins(SDA_PIN, SCL_PIN); - if (!setupAltimeter(board_config.alt_wire)) { + if (!setupAltimeter()) { USBSerial.println("Error initializing BMP3xx barometer"); } if (ENABLE_VIBE) { @@ -1102,7 +1102,7 @@ void handleThrottle() { break; case ARMED: - int smoothedPwm = getSmoothedThrottlePwm(); + int smoothedPwm = getSmoothedThrottlePwm(deviceData.performance_mode); finalPwm = applyModeRampClamp(smoothedPwm, prevPwm, deviceData.performance_mode); break; } @@ -1178,7 +1178,7 @@ void afterCruiseEnd() { // Instead of clearing the buffer which causes throttle to drop to 0, // pre-populate it with the current throttle position to ensure smooth transition int currentPotVal = readThrottleRaw(); - int currentPwmVal = potRawToPwm(currentPotVal); + int currentPwmVal = potRawToModePwm(currentPotVal, deviceData.performance_mode); // Pre-fill the buffer with current pot value for smooth transition throttleFilterReset(currentPwmVal); diff --git a/src/sp140/throttle.cpp b/src/sp140/throttle.cpp index 8166e8c..5c01faf 100644 --- a/src/sp140/throttle.cpp +++ b/src/sp140/throttle.cpp @@ -61,6 +61,15 @@ int potRawToPwm(uint16_t raw) { return map(raw, POT_MIN_VALUE, POT_MAX_VALUE, ESC_MIN_PWM, ESC_MAX_PWM); } +/** Map raw ADC (0..4095) to mode-specific PWM range. + * Chill mode: full physical range -> ESC_MIN_PWM..CHILL_MODE_MAX_PWM + * Sport mode: full physical range -> ESC_MIN_PWM..ESC_MAX_PWM + */ +int potRawToModePwm(uint16_t raw, uint8_t performance_mode) { + int maxPwm = (performance_mode == 0) ? CHILL_MODE_MAX_PWM : ESC_MAX_PWM; + return map(raw, POT_MIN_VALUE, POT_MAX_VALUE, ESC_MIN_PWM, maxPwm); +} + /** * Apply mode-specific ramp limiting (in microseconds per tick) and clamp * the result to the current mode's max PWM. Updates prevPwm to the final value. @@ -110,13 +119,16 @@ int throttleFilterAverage() { /** * Read throttle input and return smoothed PWM value. * This is the core throttle processing pipeline without any state logic. + * Uses mode-aware mapping so the full physical range covers the mode's + * PWM output range (chill: 1035-1600, sport: 1035-1950). * + * @param performance_mode 0 = CHILL, 1 = SPORT * @return Smoothed PWM value from throttle input */ -int getSmoothedThrottlePwm() { - // Read and convert raw pot to PWM +int getSmoothedThrottlePwm(uint8_t performance_mode) { + // Read and convert raw pot to mode-specific PWM range int potValRaw = readThrottleRaw(); - int targetPwm = potRawToPwm(potValRaw); + int targetPwm = potRawToModePwm(potValRaw, performance_mode); // Smooth in PWM domain using ring buffer throttleFilterPush(targetPwm); @@ -134,21 +146,15 @@ void resetThrottleState(int& prevPwm) { /** * Calculate the cruise control PWM value from a raw pot reading. - * Uses the same mapping as normal throttle: map to full range first, - * then clamp to the mode maximum and cruise maximum. - * - * This ensures cruise maintains the actual throttle output level, - * not a re-mapped value that would cause throttle drop in chill mode. + * Uses the same mode-aware mapping as normal throttle so the full + * physical range maps to the mode's output range, then applies the + * absolute cruise max cap. */ uint16_t calculateCruisePwm(uint16_t potVal, uint8_t performance_mode, float cruiseMaxPct) { - // Step 1: Map to full PWM range (same as normal throttle) - uint16_t pwm = potRawToPwm(potVal); - - // Step 2: Clamp to mode maximum (chill mode caps at lower PWM) - int maxPwmForMode = (performance_mode == 0) ? CHILL_MODE_MAX_PWM : ESC_MAX_PWM; - pwm = min(pwm, (uint16_t)maxPwmForMode); + // Step 1: Map to mode-specific PWM range (same mapping as normal throttle) + uint16_t pwm = potRawToModePwm(potVal, performance_mode); - // Step 3: Apply absolute cruise max cap + // Step 2: Apply absolute cruise max cap uint16_t absoluteMaxCruisePwm = ESC_MIN_PWM + (uint16_t)((ESC_MAX_PWM - ESC_MIN_PWM) * cruiseMaxPct); pwm = min(pwm, absoluteMaxCruisePwm); diff --git a/src/sp140/utilities.cpp b/src/sp140/utilities.cpp index d5444b7..94920f3 100644 --- a/src/sp140/utilities.cpp +++ b/src/sp140/utilities.cpp @@ -1,27 +1,10 @@ -// Copyright 2021 -#include "sp140/utilities.h" -#include "Arduino.h" - -double mapd(double x, double in_min, double in_max, double out_min, double out_max) { - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - -/** - * For digital time display - prints leading 0 - * - * @param digits number to be converted to a string. - * @return string `12`, or 07 if `digits` is less than 10. - */ -String convertToDigits(byte digits) { - String digits_string = ""; - if (digits < 10) digits_string.concat("0"); - digits_string.concat(digits); - return digits_string; -} - -/** - * Gets a unique ID string for the ESP32 chip - * +// Copyright 2021 +#include "sp140/utilities.h" +#include "Arduino.h" + +/** + * Gets a unique ID string for the ESP32 chip + * * @return String representation of the chip ID */ String chipId() { diff --git a/src/sp140/vibration_pwm.cpp b/src/sp140/vibration_pwm.cpp index 096385a..23d2ca5 100644 --- a/src/sp140/vibration_pwm.cpp +++ b/src/sp140/vibration_pwm.cpp @@ -1,6 +1,5 @@ #include "sp140/vibration_pwm.h" #include "sp140/esp32s3-config.h" -#include "sp140/lvgl/lvgl_updates.h" // Pin is configured via s3_config.vibe_pwm const int VIBE_PWM_FREQ = 1000; // Adjust as needed @@ -12,27 +11,6 @@ const int VIBE_PWM_RESOLUTION = 8; // 8-bit resolution // Channels 2-7: Available for future use const int VIBE_PWM_CHANNEL = 0; -static bool criticalVibrationActive = false; -static TaskHandle_t criticalVibeTaskHandle = NULL; - -/** - * Critical vibration task - provides continuous vibration for critical alerts - */ -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 - vTaskDelay(pdMS_TO_TICKS(300)); // 300ms on - ledcWrite(VIBE_PWM_CHANNEL, 0); - vTaskDelay(pdMS_TO_TICKS(700)); // 700ms off (total 1 second cycle) - } else { - // If not active, suspend task to save resources - vTaskSuspend(NULL); - } - } -} - /** * Vibration task - processes vibration requests from queue */ @@ -211,46 +189,3 @@ void customVibePattern(const uint8_t intensities[], const uint16_t durations[], } ledcWrite(VIBE_PWM_CHANNEL, 0); } - -// Service state for critical alerts -static bool g_critical_alert_active = false; - -/** - * @brief Initializes the critical alert service. - */ -void initCriticalAlertService() { - // Initialization can be expanded if needed in the future. -} - -/** - * @brief Starts the critical alert notifications. - */ -void startCriticalAlerts() { - if (g_critical_alert_active) { - return; - } - g_critical_alert_active = true; - - // Start the single master LVGL timer, which will handle both border and vibration - startCriticalBorderFlash(); -} - -/** - * @brief Stops the critical alert notifications. - */ -void stopCriticalAlerts() { - if (!g_critical_alert_active) { - return; - } - g_critical_alert_active = false; - - // Stop the master LVGL timer - stopCriticalBorderFlash(); -} - -/** - * @brief Checks if the critical alert system is currently active. - */ -bool isCriticalAlertActive() { - return g_critical_alert_active; -} diff --git a/test/native_stubs/Arduino.h b/test/native_stubs/Arduino.h index 089be60..ed8c8e6 100644 --- a/test/native_stubs/Arduino.h +++ b/test/native_stubs/Arduino.h @@ -43,7 +43,9 @@ inline void delay(unsigned long ms) { #endif inline void pinMode(int, int) {} -inline int analogRead(int) { return 0; } +inline int g_testAnalogReadValue = 0; +inline void setTestAnalogReadValue(int value) { g_testAnalogReadValue = value; } +inline int analogRead(int) { return g_testAnalogReadValue; } inline void analogReadResolution(int) {} // Math helpers diff --git a/test/test_throttle/test_throttle.cpp b/test/test_throttle/test_throttle.cpp index f182bd5..924c1c2 100644 --- a/test/test_throttle/test_throttle.cpp +++ b/test/test_throttle/test_throttle.cpp @@ -159,6 +159,22 @@ TEST(ThrottleTest, ApplyModeRampClamp) { EXPECT_EQ(result, 1867); // Should ramp by 27, not clamp yet } +// Test deceleration clamping at ESC minimum floor +TEST(ThrottleTest, ApplyModeRampClampDecelToFloor) { + int prevPwm = 1040; + + // Large decel in CHILL mode should never go below ESC_MIN_PWM + int result = applyModeRampClamp(500, prevPwm, 0); + EXPECT_EQ(result, 1035); + EXPECT_EQ(prevPwm, 1035); + + // Large decel in SPORT mode should also clamp at ESC_MIN_PWM + prevPwm = 1040; + result = applyModeRampClamp(500, prevPwm, 1); + EXPECT_EQ(result, 1035); + EXPECT_EQ(prevPwm, 1035); +} + // Test throttle filter functions TEST(ThrottleTest, ThrottleFiltering) { // Clear buffer first @@ -182,6 +198,29 @@ TEST(ThrottleTest, ThrottleFiltering) { EXPECT_EQ(throttleFilterAverage(), 1500); // Should be filled with 1500 } +// Test throttle filter ring buffer rollover (capacity = 8) +TEST(ThrottleTest, ThrottleFilterRollover) { + throttleFilterClear(); + + // Fill exactly to capacity + throttleFilterPush(1000); + throttleFilterPush(1100); + throttleFilterPush(1200); + throttleFilterPush(1300); + throttleFilterPush(1400); + throttleFilterPush(1500); + throttleFilterPush(1600); + throttleFilterPush(1700); + EXPECT_EQ(throttleFilterAverage(), 1350); // (1000..1700)/8 + + // Push beyond capacity: oldest entries should roll off + throttleFilterPush(1800); // Buffer now 1100..1800 + EXPECT_EQ(throttleFilterAverage(), 1450); + + throttleFilterPush(1900); // Buffer now 1200..1900 + EXPECT_EQ(throttleFilterAverage(), 1550); +} + // Test resetThrottleState function TEST(ThrottleTest, ResetThrottleState) { int prevPwm = 1500; @@ -198,83 +237,117 @@ TEST(ThrottleTest, ResetThrottleState) { EXPECT_EQ(throttleFilterAverage(), 0); // Buffer should be empty } -// Test calculateCruisePwm function - ensures cruise maintains actual throttle level -TEST(ThrottleTest, CalculateCruisePwmBasic) { - // Test that cruise PWM matches normal throttle mapping - // potRawToPwm(2048) = 1492 (50% of full range) - // In chill mode, 1492 < 1600 (CHILL_MODE_MAX_PWM), so no clamping - // With 60% cruise cap: 1035 + 915*0.6 = 1584 +// Test getSmoothedThrottlePwm mode-aware mapping + smoothing behavior +TEST(ThrottleTest, GetSmoothedThrottlePwmModeAwareAndSmoothing) { + // CHILL full-stick should map to chill max + throttleFilterClear(); + setTestAnalogReadValue(4095); + EXPECT_EQ(getSmoothedThrottlePwm(0), 1600); + + // SPORT full-stick should map to full ESC max + throttleFilterClear(); + setTestAnalogReadValue(4095); + EXPECT_EQ(getSmoothedThrottlePwm(1), 1950); + + // Verify smoothing in SPORT mode across two samples + throttleFilterClear(); + setTestAnalogReadValue(0); // 1035 + EXPECT_EQ(getSmoothedThrottlePwm(1), 1035); + setTestAnalogReadValue(4095); // 1950 + EXPECT_EQ(getSmoothedThrottlePwm(1), 1492); // (1035 + 1950) / 2 +} - // 50% pot in SPORT mode (mode 1) - should match potRawToPwm exactly +// Test potRawToModePwm - mode-aware mapping +TEST(ThrottleTest, PotRawToModePwmMapping) { + // SPORT mode (mode 1) should be identical to potRawToPwm (full range) + EXPECT_EQ(potRawToModePwm(0, 1), 1035); + EXPECT_EQ(potRawToModePwm(4095, 1), 1950); + EXPECT_EQ(potRawToModePwm(2047, 1), 1492); + EXPECT_EQ(potRawToModePwm(1024, 1), 1263); + EXPECT_EQ(potRawToModePwm(3072, 1), 1721); + + // CHILL mode (mode 0) maps full physical range to 1035-1600 + // Range: 1600 - 1035 = 565 + EXPECT_EQ(potRawToModePwm(0, 0), 1035); // Min ADC -> Min PWM + EXPECT_EQ(potRawToModePwm(4095, 0), 1600); // Max ADC -> CHILL max + EXPECT_EQ(potRawToModePwm(2048, 0), 1317); // 50% -> mid chill range + EXPECT_EQ(potRawToModePwm(1024, 0), 1176); // 25% -> 25% of chill range + EXPECT_EQ(potRawToModePwm(3072, 0), 1458); // 75% -> 75% of chill range +} + +// Test calculateCruisePwm function - ensures cruise uses same mode-aware mapping +TEST(ThrottleTest, CalculateCruisePwmBasic) { + // 50% pot in SPORT mode (mode 1) - should match potRawToModePwm exactly + // potRawToModePwm(2048, 1) = 1492, cruise cap at 60% = 1584 uint16_t result = calculateCruisePwm(2048, 1, 0.60); - EXPECT_EQ(result, 1492); // Same as potRawToPwm(2048) + EXPECT_EQ(result, 1492); // Below cruise cap - // 50% pot in CHILL mode (mode 0) - should also be 1492 (below chill max) + // 50% pot in CHILL mode (mode 0) - uses chill range mapping + // potRawToModePwm(2048, 0) = 1317, cruise cap at 60% = 1584 result = calculateCruisePwm(2048, 0, 0.60); - EXPECT_EQ(result, 1492); // Not clamped, same as normal throttle + EXPECT_EQ(result, 1317); // Chill mode maps full physical range to 1035-1600 } -// Test the bug that was fixed: chill mode cruise shouldn't drop throttle -TEST(ThrottleTest, CalculateCruisePwmChillModeNoDrop) { - // THE BUG: Old code did map(pot, 0, 4095, 1035, 1600) in chill mode - // which gave 1317 for 50% pot instead of 1492 +// Test that cruise in chill mode uses the same mapping as normal throttle +TEST(ThrottleTest, CalculateCruisePwmChillModeConsistency) { + // Cruise should use the same mode-aware mapping as normal throttle. + // In chill mode, full physical range maps to 1035-1600. + int normalThrottle = potRawToModePwm(2048, 0); // 1317 + EXPECT_EQ(normalThrottle, 1317); - // Verify normal throttle mapping at 50% - int normalThrottle = potRawToPwm(2048); - EXPECT_EQ(normalThrottle, 1492); - - // Cruise in chill mode should match normal throttle (no drop!) + // Cruise in chill mode should match the normal throttle mapping uint16_t cruiseThrottle = calculateCruisePwm(2048, 0, 0.60); - EXPECT_EQ(cruiseThrottle, normalThrottle); // CRITICAL: Must match! + EXPECT_EQ(cruiseThrottle, (uint16_t)normalThrottle); // Both use same mapping } -// Test chill mode max clamping -TEST(ThrottleTest, CalculateCruisePwmChillModeClamping) { - // At high throttle in chill mode, should clamp to CHILL_MODE_MAX_PWM (1600) - // potRawToPwm(4095) = 1950, but chill mode caps at 1600 - - // 100% pot in CHILL mode - should clamp to 1600 (not 1950) +// Test chill mode at full physical range +TEST(ThrottleTest, CalculateCruisePwmChillModeFullRange) { + // 100% pot in CHILL mode - maps to exactly CHILL_MODE_MAX_PWM (1600) + // potRawToModePwm(4095, 0) = 1600, cruise cap at 70% = 1675 uint16_t result = calculateCruisePwm(4095, 0, 0.70); - EXPECT_EQ(result, 1600); // Clamped to CHILL_MODE_MAX_PWM + EXPECT_EQ(result, 1600); // Full physical range -> CHILL_MODE_MAX_PWM - // 80% pot in CHILL mode - potRawToPwm(3276) ≈ 1767, clamps to 1600 + // 80% pot in CHILL mode - potRawToModePwm(3276, 0) = 1487 + // Cruise cap at 70% = 1675, so 1487 is below cap result = calculateCruisePwm(3276, 0, 0.70); - EXPECT_EQ(result, 1600); // Clamped to CHILL_MODE_MAX_PWM + EXPECT_EQ(result, 1487); // Full granular control, no clamping } // Test cruise max percentage capping TEST(ThrottleTest, CalculateCruisePwmCruiseMaxCap) { // Cruise max cap at 60%: 1035 + (1950-1035)*0.6 = 1035 + 549 = 1584 - // In SPORT mode at high throttle, cruise cap should apply - // potRawToPwm(4095) = 1950, but cruise cap at 60% = 1584 + // In SPORT mode at full throttle, cruise cap should apply + // potRawToModePwm(4095, 1) = 1950, cruise cap at 60% = 1584 uint16_t result = calculateCruisePwm(4095, 1, 0.60); - EXPECT_EQ(result, 1584); // Capped by cruise max, not mode max + EXPECT_EQ(result, 1584); // Capped by cruise max // At 70% cruise cap: 1035 + 915*0.7 = 1675 result = calculateCruisePwm(4095, 1, 0.70); EXPECT_EQ(result, 1675); // Higher cruise cap - // In chill mode, the lower of (chill max, cruise cap) applies - // Chill max = 1600, cruise cap at 70% = 1675, so 1600 wins + // In chill mode at full throttle, chill max (1600) < cruise cap (1675) + // potRawToModePwm(4095, 0) = 1600 result = calculateCruisePwm(4095, 0, 0.70); - EXPECT_EQ(result, 1600); // Chill mode max is lower than cruise cap + EXPECT_EQ(result, 1600); // Chill mode max is the limiter } // Test edge cases TEST(ThrottleTest, CalculateCruisePwmEdgeCases) { - // Minimum pot value + // Minimum pot value - same in both modes uint16_t result = calculateCruisePwm(0, 0, 0.60); EXPECT_EQ(result, 1035); // ESC_MIN_PWM result = calculateCruisePwm(0, 1, 0.60); EXPECT_EQ(result, 1035); // ESC_MIN_PWM - // Low throttle (25%) - should not be affected by any caps - // potRawToPwm(1024) = 1263 + // Low throttle (25%) in chill mode - uses chill mapping + // potRawToModePwm(1024, 0) = 1176 result = calculateCruisePwm(1024, 0, 0.60); - EXPECT_EQ(result, 1263); + EXPECT_EQ(result, 1176); // Chill mode maps to reduced range + // Low throttle (25%) in sport mode - uses full mapping + // potRawToModePwm(1024, 1) = 1263 result = calculateCruisePwm(1024, 1, 0.60); EXPECT_EQ(result, 1263); }