From f763701206b180d36c1c811bf95707565ccce7c4 Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Thu, 20 Aug 2026 10:51:53 -0500 Subject: [PATCH 1/5] - Truncate only the BLE-advertised device name when needed so the SmartSpin2k service UUID remains present in the legacy scan response. --- CHANGELOG.md | 1 + src/BLE_Server.cpp | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b66323..b6602d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added new BLE firmware update protocol. ### Changed +- Truncate only the BLE-advertised device name when needed so the SmartSpin2k service UUID remains present in the legacy scan response. ### Hardware diff --git a/src/BLE_Server.cpp b/src/BLE_Server.cpp index 877efaf1..d85dc880 100644 --- a/src/BLE_Server.cpp +++ b/src/BLE_Server.cpp @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include #include "BLE_Cycling_Speed_Cadence.h" #include "BLE_Cycling_Power_Service.h" #include "BLE_Heart_Service.h" @@ -43,6 +45,27 @@ BLE_OpenBikeControl_Service openBikeControlService; namespace { constexpr uint8_t SMARTSPIN2K_IP_ADVERTISEMENT_VERSION = 1; +constexpr size_t BLE_LEGACY_ADVERTISEMENT_MAX_SIZE = 31; +constexpr size_t BLE_AD_FIELD_OVERHEAD = 2; +constexpr size_t BLE_UUID128_SIZE = 16; +// A legacy scan response is 31 bytes. After the 128-bit service UUID and both +// AD field headers, 11 bytes remain for the local name. +constexpr size_t BLE_ADVERTISED_NAME_MAX_SIZE = + BLE_LEGACY_ADVERTISEMENT_MAX_SIZE - (BLE_UUID128_SIZE + BLE_AD_FIELD_OVERHEAD) - BLE_AD_FIELD_OVERHEAD; + +std::string bleAdvertisementName(const char* deviceName) { + std::string name = deviceName; + if (name.size() <= BLE_ADVERTISED_NAME_MAX_SIZE) { + return name; + } + + size_t length = BLE_ADVERTISED_NAME_MAX_SIZE; + while (length > 0 && (static_cast(name[length]) & 0xc0) == 0x80) { + --length; + } + name.resize(length); + return name; +} void addIpAddressToAdvertisement(NimBLEAdvertising* advertising) { IPAddress ipAddress = WiFi.status() == WL_CONNECTED ? WiFi.localIP() : WiFi.softAPIP(); @@ -103,7 +126,13 @@ void startBLEServer() { // Keep the name and 128-bit SmartSpin2k UUID in the scan response. The primary // advertisement uses the space previously occupied by the duplicate name for the IP address. addIpAddressToAdvertisement(pAdvertising); - oScanResponseData.setName(userConfig->getDeviceName()); + const std::string advertisedName = bleAdvertisementName(userConfig->getDeviceName()); + if (advertisedName.size() < std::strlen(userConfig->getDeviceName())) { + oScanResponseData.setShortName(advertisedName); + SS2K_LOGW(BLE_SERVER_LOG_TAG, "BLE device name shortened to '%s' to fit scan response", advertisedName.c_str()); + } else { + oScanResponseData.setName(advertisedName); + } oScanResponseData.setCompleteServices(SMARTSPIN2K_SERVICE_UUID); pAdvertising->setScanResponseData(oScanResponseData); From d7aaa993709bc4a5c2664d3c13b2359d945362fd Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Thu, 20 Aug 2026 10:57:49 -0500 Subject: [PATCH 2/5] Simplified constant creation --- src/BLE_Server.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/BLE_Server.cpp b/src/BLE_Server.cpp index d85dc880..90eebaf4 100644 --- a/src/BLE_Server.cpp +++ b/src/BLE_Server.cpp @@ -45,13 +45,8 @@ BLE_OpenBikeControl_Service openBikeControlService; namespace { constexpr uint8_t SMARTSPIN2K_IP_ADVERTISEMENT_VERSION = 1; -constexpr size_t BLE_LEGACY_ADVERTISEMENT_MAX_SIZE = 31; -constexpr size_t BLE_AD_FIELD_OVERHEAD = 2; -constexpr size_t BLE_UUID128_SIZE = 16; -// A legacy scan response is 31 bytes. After the 128-bit service UUID and both -// AD field headers, 11 bytes remain for the local name. -constexpr size_t BLE_ADVERTISED_NAME_MAX_SIZE = - BLE_LEGACY_ADVERTISEMENT_MAX_SIZE - (BLE_UUID128_SIZE + BLE_AD_FIELD_OVERHEAD) - BLE_AD_FIELD_OVERHEAD; +// Leaves room for the 128-bit SmartSpin2k service UUID in the 31-byte scan response. +constexpr size_t BLE_ADVERTISED_NAME_MAX_SIZE = 11; std::string bleAdvertisementName(const char* deviceName) { std::string name = deviceName; From a460d3117533f93552f1bf87c9f8af5ff10ced4d Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Thu, 20 Aug 2026 11:40:26 -0500 Subject: [PATCH 3/5] Fix ERG latching off when cadence drops below 30 --- CHANGELOG.md | 1 + include/ERG_Mode.h | 3 ++- src/BLE_Fitness_Machine_Service.cpp | 3 +++ src/ERG_Mode.cpp | 23 ++++++++++++++++++++--- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6602d58..e154811a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Truncate only the BLE-advertised device name when needed so the SmartSpin2k service UUID remains present in the legacy scan response. +- Resume the latest ERG target automatically when cadence recovers after a low-cadence fallback, unless another FTMS command selects a different mode. ### Hardware diff --git a/include/ERG_Mode.h b/include/ERG_Mode.h index 66ea7071..36207af3 100644 --- a/include/ERG_Mode.h +++ b/include/ERG_Mode.h @@ -25,10 +25,11 @@ class ErgMode { // What used to be in the ERGTaskLoop(). This is the main control function for ERG Mode and the powertable operations. void runERG(); void computeErg(); + void onFTMSCommand(uint8_t opcode); void _writeLog(float currentIncline, float newIncline, int currentSetPoint, int newSetPoint, int currentWatts, int newWatts, int currentCadence, int newCadence); private: - bool engineStopped = false; + bool resumeErgOnCadence = false; int mode = Mode::MAINTAIN; Measurement prevWatts; diff --git a/src/BLE_Fitness_Machine_Service.cpp b/src/BLE_Fitness_Machine_Service.cpp index f5610394..ae53a844 100644 --- a/src/BLE_Fitness_Machine_Service.cpp +++ b/src/BLE_Fitness_Machine_Service.cpp @@ -6,6 +6,7 @@ */ #include "BLE_Fitness_Machine_Service.h" #include "DirConManager.h" +#include "ERG_Mode.h" #include "Main.h" #include #include @@ -168,6 +169,8 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() { int logBufLength = ss2k_log_hex_to_buffer(pData, length, logBuf, 0, kLogBufCapacity); int port = 0; + ergMode->onFTMSCommand(static_cast(rxValue[0])); + switch ((uint8_t)rxValue[0]) { case FitnessMachineControlPointProcedure::RequestControl: returnValue[2] = FitnessMachineControlPointResultCode::Success; diff --git a/src/ERG_Mode.cpp b/src/ERG_Mode.cpp index 442d0224..c4713dcc 100644 --- a/src/ERG_Mode.cpp +++ b/src/ERG_Mode.cpp @@ -69,12 +69,20 @@ double clampErgGain(double gain, double sensitivity) { } // namespace void ErgMode::runERG() { - static ErgMode ergMode; static PowerBuffer powerBuffer; static bool hasConnectedPowerMeter = false; static bool simulationRunning = false; static int loopCounter = 0; + if (resumeErgOnCadence && rtConfig->cad.getValue() > MIN_ERG_CADENCE) { + if (rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetIndoorBikeSimulationParameters) { + rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetTargetPower); + SS2K_LOG(ERG_MODE_LOG_TAG, "Cadence resumed; restoring ERG mode at %dw", rtConfig->watts.getTarget()); + } + resumeErgOnCadence = false; + ergTimer = 0; + } + if (mode == Mode::INCREASING) { if (rtConfig->watts.getValue() > rtConfig->watts.getTarget()) { // Resume PID control ergTimer = 0; @@ -144,7 +152,7 @@ void ErgMode::runERG() { // compute ERG if ((rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetTargetPower) && (hasConnectedPowerMeter || simulationRunning)) { - ergMode.computeErg(); + this->computeErg(); } // Set Min and Max Stepper positions @@ -357,14 +365,23 @@ void ErgMode::_updateValues(float newIncline) { bool ErgMode::_userIsSpinning(int cadence, float incline) { if (cadence <= MIN_ERG_CADENCE) { + resumeErgOnCadence = true; + mode = Mode::MAINTAIN; + isDelayed = false; rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetIndoorBikeSimulationParameters); rtConfig->setTargetIncline(1.0f); return false; // Cadence too low, nothing to do here } - this->engineStopped = false; return true; } +void ErgMode::onFTMSCommand(uint8_t opcode) { + if (resumeErgOnCadence && opcode != FitnessMachineControlPointProcedure::SetTargetPower) { + resumeErgOnCadence = false; + SS2K_LOG(ERG_MODE_LOG_TAG, "ERG cadence resume cancelled by FTMS command 0x%02x", opcode); + } +} + void ErgMode::_writeLog(float currentIncline, float newIncline, int currentSetPoint, int newSetPoint, int currentWatts, int newWatts, int currentCadence, int newCadence) { SS2K_LOGW(ERG_MODE_LOG_CSV_TAG, "%d;%.2f;%.2f;%d;%d;%d;%d;%d", currentIncline, newIncline, currentSetPoint, newSetPoint, currentWatts, newWatts, currentCadence, newCadence); } From 19ddb915d622d71e4b393695261e7d22180f8d5b Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Fri, 21 Aug 2026 12:20:14 -0500 Subject: [PATCH 4/5] Refactor ERG mode behavior to maintain target watts during low cadence and update related logging; adjust minimum cadence threshold. --- AGENTS.md | 2 +- CHANGELOG.md | 3 +- include/ERG_Mode.h | 6 --- include/settings.h | 14 ++--- src/BLE_Client.cpp | 2 +- src/BLE_Fitness_Machine_Service.cpp | 3 -- src/ERG_Mode.cpp | 82 ++++++++++------------------- 7 files changed, 40 insertions(+), 72 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd8d6e99..1dbe10bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -404,7 +404,7 @@ Primary files: `include/ERG_Mode.h`, `src/ERG_Mode.cpp`. `computeErg()`: -- Stops ERG and switches back to simulation mode if cadence is below `MIN_ERG_CADENCE`. +- Keeps ERG active and lowers its target to `userConfig->minWatts` if cadence is below `MIN_ERG_CADENCE`. - Raises target to `userConfig->minWatts` when apps request too little. - Skips if the same watt timestamp/target was already processed or current watts are negative. - For large setpoint changes, tries `_setPointChangeState()` using the power table when homed. diff --git a/CHANGELOG.md b/CHANGELOG.md index e154811a..add8d271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Truncate only the BLE-advertised device name when needed so the SmartSpin2k service UUID remains present in the legacy scan response. -- Resume the latest ERG target automatically when cadence recovers after a low-cadence fallback, unless another FTMS command selects a different mode. +- Keep ERG mode active and lower its target to the configured minimum brake watts when cadence falls below the ERG threshold. +- Prevent table-assisted ERG target changes from indefinitely blocking PID control when power approaches or settles near the new target. ### Hardware diff --git a/include/ERG_Mode.h b/include/ERG_Mode.h index 36207af3..11952c60 100644 --- a/include/ERG_Mode.h +++ b/include/ERG_Mode.h @@ -25,19 +25,13 @@ class ErgMode { // What used to be in the ERGTaskLoop(). This is the main control function for ERG Mode and the powertable operations. void runERG(); void computeErg(); - void onFTMSCommand(uint8_t opcode); void _writeLog(float currentIncline, float newIncline, int currentSetPoint, int newSetPoint, int currentWatts, int newWatts, int currentCadence, int newCadence); private: - bool resumeErgOnCadence = false; - int mode = Mode::MAINTAIN; Measurement prevWatts; Measurement prevCadence; - // check if user is spinning, reset incline if user stops spinning - bool _userIsSpinning(int cadence, float incline); - // calculate incline if setpoint (from Zwift) changes int32_t _setPointChangeState(); diff --git a/include/settings.h b/include/settings.h index 4362d8a0..eeceb522 100644 --- a/include/settings.h +++ b/include/settings.h @@ -101,8 +101,8 @@ const char* const DEFAULT_PASSWORD = "password"; // Use internal ERG control on external FTMS Trainer. // #define INTERNAL_ERG_4EXT_FTMS -// Minimum cadence where ERG mode stops. -#define MIN_ERG_CADENCE 30 +// Cadence at or below which ERG lowers its target to the configured minimum brake watts. +#define MIN_ERG_CADENCE 20 // Default minimum ERG target while the stepper is unhomed. // Homed operation uses the known stepper travel limits instead. @@ -160,7 +160,7 @@ const char* const DEFAULT_PASSWORD = "password"; // BLE Device Generic Names constexpr const char* NONE = "none"; -constexpr const char* ANY = "any"; +constexpr const char* ANY = "any"; // Name of default Power Meter. any connects to anything, none connects to // nothing. @@ -253,10 +253,10 @@ constexpr const char* ANY = "any"; // Limit power table size to save memory #define TABLE_DIVISOR 10.0f -//Max distance a failed neighbor can be horizontally from target position +// Max distance a failed neighbor can be horizontally from target position #define HORIZONTAL_NEIGHBOR_RANGE 0.6f - -//Max distance a failed neighbor can be vertically from target position + +// Max distance a failed neighbor can be vertically from target position #define VERTICAL_NEIGHBOR_RANGE 0.8f // Temperature of the ESP32 at which to start reducing the power output of the stepper motor driver. @@ -287,6 +287,7 @@ constexpr const char* ANY = "any"; #define HOMING_TAP_TOLERANCE 150 #define HOMING_RECOVERY_BACKOFF_MULT 3 #define HOMING_MAX_SENSITIVITY 100 +#define SHIFTER_MIDDLE_POSITION 8 // BLE automatic reconnect interval in milliseconds. #define BLE_RECONNECT_SCAN_INTERVAL 8000 @@ -325,4 +326,3 @@ constexpr const char* ANY = "any"; // uncomment to enable bench testing of ptab4pwr // #define TEST_PTAB4PWR - diff --git a/src/BLE_Client.cpp b/src/BLE_Client.cpp index be616d58..2cb382b5 100644 --- a/src/BLE_Client.cpp +++ b/src/BLE_Client.cpp @@ -235,7 +235,7 @@ void bleClientTask(void* pvParameters) { } else { // Startup Homing ss2k->goHome(false); } - rtConfig->setShifterPosition(8); // Reset to middle position + rtConfig->setShifterPosition(SHIFTER_MIDDLE_POSITION); // Reset to middle position spinBLEServer.spinDownFlag = 0; } } diff --git a/src/BLE_Fitness_Machine_Service.cpp b/src/BLE_Fitness_Machine_Service.cpp index ae53a844..f5610394 100644 --- a/src/BLE_Fitness_Machine_Service.cpp +++ b/src/BLE_Fitness_Machine_Service.cpp @@ -6,7 +6,6 @@ */ #include "BLE_Fitness_Machine_Service.h" #include "DirConManager.h" -#include "ERG_Mode.h" #include "Main.h" #include #include @@ -169,8 +168,6 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() { int logBufLength = ss2k_log_hex_to_buffer(pData, length, logBuf, 0, kLogBufCapacity); int port = 0; - ergMode->onFTMSCommand(static_cast(rxValue[0])); - switch ((uint8_t)rxValue[0]) { case FitnessMachineControlPointProcedure::RequestControl: returnValue[2] = FitnessMachineControlPointResultCode::Success; diff --git a/src/ERG_Mode.cpp b/src/ERG_Mode.cpp index c4713dcc..69074791 100644 --- a/src/ERG_Mode.cpp +++ b/src/ERG_Mode.cpp @@ -51,8 +51,8 @@ double scheduledErgGain(double sensitivity, int operatingWatts, int cadence, boo double gain = fallbackErgGain(sensitivity, operatingWatts); // Sparse linear fits are useful for lookup, but not stable enough to schedule ERG gain from their slope. - if (powerTable->ptHelpers.resistanceModel.getIsValid() && powerTable->ptHelpers.resistanceModel.getIsQuadratic() && lowerPosition != RETURN_ERROR && upperPosition != RETURN_ERROR && - upperPosition > lowerPosition) { + if (powerTable->ptHelpers.resistanceModel.getIsValid() && powerTable->ptHelpers.resistanceModel.getIsQuadratic() && lowerPosition != RETURN_ERROR && + upperPosition != RETURN_ERROR && upperPosition > lowerPosition) { const double localStepsPerWatt = static_cast(upperPosition - lowerPosition) / static_cast(upperWatts - lowerWatts); gain = localStepsPerWatt * sensitivity / ERG_SLOPE_CONTROL_DIVISOR; usedPowerTable = true; @@ -74,35 +74,25 @@ void ErgMode::runERG() { static bool simulationRunning = false; static int loopCounter = 0; - if (resumeErgOnCadence && rtConfig->cad.getValue() > MIN_ERG_CADENCE) { - if (rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetIndoorBikeSimulationParameters) { - rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetTargetPower); - SS2K_LOG(ERG_MODE_LOG_TAG, "Cadence resumed; restoring ERG mode at %dw", rtConfig->watts.getTarget()); + if (rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetTargetPower && rtConfig->cad.getValue() <= MIN_ERG_CADENCE) { + if (rtConfig->watts.getTarget() != userConfig->getMinWatts()) { + SS2K_LOG(ERG_MODE_LOG_TAG, "Cadence below ERG minimum; lowering target to %dw", userConfig->getMinWatts()); + rtConfig->watts.setTarget(userConfig->getMinWatts()); + mode = Mode::MAINTAIN; + isDelayed = false; + ergTimer = 0; } - resumeErgOnCadence = false; - ergTimer = 0; } - if (mode == Mode::INCREASING) { - if (rtConfig->watts.getValue() > rtConfig->watts.getTarget()) { // Resume PID control - ergTimer = 0; - mode = Mode::MAINTAIN; - SS2K_LOG(ERG_MODE_LOG_TAG, "ERG increasing target reached."); - } else if (rtConfig->watts.getValue() >= this->prevWatts.getValue()) { - // power is still increasing, wait longer - return; - } - } else if (mode == Mode::DECREASING) { - if (rtConfig->watts.getValue() < rtConfig->watts.getTarget()) // Resume PID control - { - ergTimer = 0; - mode = Mode::MAINTAIN; - SS2K_LOG(ERG_MODE_LOG_TAG, "ERG decreasing target reached."); - } else if (rtConfig->watts.getValue() <= this->prevWatts.getValue()) { - // power is still decreasing, wait longer - return; - } + const bool reachedIncreasingTarget = mode == Mode::INCREASING && rtConfig->watts.getValue() >= rtConfig->watts.getTarget(); + const bool reachedDecreasingTarget = mode == Mode::DECREASING && rtConfig->watts.getValue() <= rtConfig->watts.getTarget(); + if (reachedIncreasingTarget || reachedDecreasingTarget) { + SS2K_LOG(ERG_MODE_LOG_TAG, "ERG setpoint reached; resuming PID control"); + mode = Mode::MAINTAIN; + isDelayed = false; + ergTimer = 0; } + if (isDelayed && (ss2k->getCurrentPosition() == ss2k->getTargetPosition())) { SS2K_LOG(ERG_MODE_LOG_TAG, "ERG delay cleared, %dw, tgt %dw, pos %d, tgt %d", rtConfig->watts.getValue(), rtConfig->watts.getTarget(), ss2k->getCurrentPosition(), ss2k->getTargetPosition()); @@ -117,6 +107,11 @@ void ErgMode::runERG() { isDelayed = false; } + if (mode != Mode::MAINTAIN) { + SS2K_LOG(ERG_MODE_LOG_TAG, "ERG setpoint seek complete; resuming PID control"); + mode = Mode::MAINTAIN; + } + // reset the timer. ergTimer = millis() + ERG_MODE_DELAY; @@ -138,7 +133,7 @@ void ErgMode::runERG() { powerTable->_manageSaveState(); } - if (rtConfig->cad.getValue()) { + if (rtConfig->cad.getValue() > MIN_ERG_CADENCE / 2) { hasConnectedPowerMeter = spinBLEClient.connectedPM; simulationRunning = rtConfig->watts.getTarget(); if (!simulationRunning) { @@ -208,12 +203,6 @@ void ErgMode::runERG() { void ErgMode::computeErg() { int32_t result = RETURN_ERROR; - bool isUserSpinning = this->_userIsSpinning(rtConfig->cad.getValue(), ss2k->getCurrentPosition()); - if (!isUserSpinning) { - SS2K_LOG(ERG_MODE_LOG_TAG, "ERG Mode but no User Spin"); - return; - } - // Without known travel limits, keep ERG above the configured minimum bike watts. // Once homed, moveStepper() clamps the commanded position to the known min/max step range instead. if (!rtConfig->getHomed() && rtConfig->watts.getTarget() < userConfig->getMinWatts()) { @@ -238,6 +227,12 @@ void ErgMode::computeErg() { result = _inSetpointState(); } #endif + +//Avoid ERG Black hole + if (rtConfig->cad.getValue() < MIN_ERG_CADENCE && rtConfig->getHomed()) { + SS2K_LOG(ERG_MODE_LOG_TAG, "Cadence below ERG minimum"); + result = userConfig->getShiftStep() * SHIFTER_MIDDLE_POSITION; + } _updateValues(result); } @@ -363,25 +358,6 @@ void ErgMode::_updateValues(float newIncline) { this->prevCadence = rtConfig->cad; } -bool ErgMode::_userIsSpinning(int cadence, float incline) { - if (cadence <= MIN_ERG_CADENCE) { - resumeErgOnCadence = true; - mode = Mode::MAINTAIN; - isDelayed = false; - rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetIndoorBikeSimulationParameters); - rtConfig->setTargetIncline(1.0f); - return false; // Cadence too low, nothing to do here - } - return true; -} - -void ErgMode::onFTMSCommand(uint8_t opcode) { - if (resumeErgOnCadence && opcode != FitnessMachineControlPointProcedure::SetTargetPower) { - resumeErgOnCadence = false; - SS2K_LOG(ERG_MODE_LOG_TAG, "ERG cadence resume cancelled by FTMS command 0x%02x", opcode); - } -} - void ErgMode::_writeLog(float currentIncline, float newIncline, int currentSetPoint, int newSetPoint, int currentWatts, int newWatts, int currentCadence, int newCadence) { SS2K_LOGW(ERG_MODE_LOG_CSV_TAG, "%d;%.2f;%.2f;%d;%d;%d;%d;%d", currentIncline, newIncline, currentSetPoint, newSetPoint, currentWatts, newWatts, currentCadence, newCadence); } From 71ceed871e3984d7ee6dcfefa5aeaf1de69e2306 Mon Sep 17 00:00:00 2001 From: Anthony Doud Date: Fri, 21 Aug 2026 12:28:20 -0500 Subject: [PATCH 5/5] restore target power after cadence exceeds minimum threshold; track last set point for improved responsiveness. --- src/ERG_Mode.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ERG_Mode.cpp b/src/ERG_Mode.cpp index 69074791..d0676f20 100644 --- a/src/ERG_Mode.cpp +++ b/src/ERG_Mode.cpp @@ -73,15 +73,21 @@ void ErgMode::runERG() { static bool hasConnectedPowerMeter = false; static bool simulationRunning = false; static int loopCounter = 0; + static int lastSetPoint = 0; if (rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetTargetPower && rtConfig->cad.getValue() <= MIN_ERG_CADENCE) { if (rtConfig->watts.getTarget() != userConfig->getMinWatts()) { SS2K_LOG(ERG_MODE_LOG_TAG, "Cadence below ERG minimum; lowering target to %dw", userConfig->getMinWatts()); + lastSetPoint = rtConfig->watts.getTarget(); rtConfig->watts.setTarget(userConfig->getMinWatts()); mode = Mode::MAINTAIN; isDelayed = false; ergTimer = 0; } + } else if (lastSetPoint != 0 && rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetTargetPower && rtConfig->cad.getValue() > MIN_ERG_CADENCE) { + SS2K_LOG(ERG_MODE_LOG_TAG, "Cadence above ERG minimum; restoring target to %dw", lastSetPoint); + rtConfig->watts.setTarget(lastSetPoint); + lastSetPoint = 0; } const bool reachedIncreasingTarget = mode == Mode::INCREASING && rtConfig->watts.getValue() >= rtConfig->watts.getTarget(); @@ -228,7 +234,7 @@ void ErgMode::computeErg() { } #endif -//Avoid ERG Black hole + // Avoid ERG Black hole if (rtConfig->cad.getValue() < MIN_ERG_CADENCE && rtConfig->getHomed()) { SS2K_LOG(ERG_MODE_LOG_TAG, "Cadence below ERG minimum"); result = userConfig->getShiftStep() * SHIFTER_MIDDLE_POSITION;