Skip to content

Commit fab06c2

Browse files
authored
Merge pull request #711 from doudar/Marc_Roy
This PR implements ERG mode tuning and improvements developed in collaboration with Marc Roy. The changes focus on refining PID control, improving power table lookup logic, and adding proper type conversions for sensor data. Key Changes: Refactored ERG mode calculation to return position values instead of directly updating, enabling better control flow Added proper rounding for float-to-int conversions in sensor data (cadence and power) Improved ERG mode calculations with new PID gain adjustments, better time-to-target calculations, and negative result validation for homed systems
2 parents b1f11fc + e64c726 commit fab06c2

21 files changed

+495
-96
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,5 @@
121121
],
122122
"idf.pythonInstallPath": "C:\\Users\\anthony\\.espressif\\tools\\idf-python\\3.11.2\\python.exe",
123123
"idf.flashType": "UART",
124+
"liveServer.settings.port": 5501,
124125
}

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
### Changed
13+
- Added feed forward, disabled PowerTable for ERG lookup.
14+
- Added tests and removal of duplicates in pt column.
15+
- Added removal of negative numbers in pt table.
16+
17+
### Hardware
18+
19+
20+
## [25.12.28]
21+
22+
### Added
23+
1224
### Changed
1325
- Removed >0 watts requirement to compute ERG.
1426
- Filter cadence for crazy values. Only >0 && <250 now accepted.
1527
- Filter watts for crazy values. Only >0 && <3000 now accepted.
1628
- Fixed bug where scans may not happen even when configured devices aren't connected.
29+
- Worked with Mark Roy to tune PID.
30+
- Added proper rounding from float to int for power and cadence.
31+
- More ERG tweaks for Marc Roy.
32+
- If homed, we throw out negative PowerTable returns.
33+
- After startup homing, set gear 8.
1734

1835
### Hardware
1936

include/ERG_Mode.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ class ErgMode {
3535
bool _userIsSpinning(int cadence, float incline);
3636

3737
// calculate incline if setpoint (from Zwift) changes
38-
void _setPointChangeState(int newCadence, Measurement& newWatts);
38+
int32_t _setPointChangeState(int newCadence, Measurement& newWatts);
3939

4040
// calculate incline if setpoint is unchanged
41-
void _inSetpointState(int newCadence, Measurement& newWatts);
41+
int32_t _inSetpointState(int newCadence, Measurement& newWatts);
4242

4343
// update localvalues + incline, creates a log
4444
void _updateValues(int newCadence, Measurement& newWatts, float newIncline);

include/PowerTable_Helpers.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class PTHelpers {
8989
ptIndex calculateIndex(int watts, int cad);
9090
void enterData(PTData& ptData,ptIndex index, int pos);
9191
void clean(PTData& ptData);
92+
void fillGaps(PTData& ptData);
9293
bool fillAllWattColumns(PTData& ptData);
9394
bool fillAllCadenceLines(PTData& ptData);
9495
std::pair<std::vector<float>, std::vector<float>> getRow(int row, PTData& ptData);

include/settings.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const char* const DEFAULT_PASSWORD = "password";
7777
// I.E. If the difference between ERG target and Current watts were 30, and the Shift step is defined as 600 steps,
7878
// and ERG_Sensitivity were 1.0, ERG mode would move the stepper motor 600 steps to compensate. With an ERG_Sensitivity of 2.0, the stepper
7979
// would move 1200 steps to compensate, however ERG_Sensitivity values much different than 1.0 imply shiftStep has been improperly configured.
80-
#define ERG_SENSITIVITY 2.0f
80+
#define ERG_SENSITIVITY 3.0f
8181

8282
// Number of watts per shift expected by ERG mode for it's calculation. The user should target this number by adjusting Shift Step until WATTS_PER_SHIFT
8383
// is obtained as closely as possible during each shift.
@@ -276,8 +276,8 @@ constexpr const char* ANY = "any";
276276
// Uncomment to use guardrails for ERG mode in the stepper loop.
277277
#define ERG_GUARDRAILS
278278

279-
//Uncomment to enable the use of the power table for ERG mode.
280-
#define ERG_MODE_USE_POWER_TABLE
279+
// Uncomment to enable the use of the power table for ERG mode.
280+
// #define ERG_MODE_USE_POWER_TABLE
281281

282282
// Uncomment to use the PID controller for ERG mode.
283283
#define ERG_MODE_USE_PID

platformio.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ build_flags =
108108
-Wunreachable-code
109109
-std=c++11
110110
-D PLATFORMIO_ENV_NATIVE
111+
-I include
111112
lib_ldf_mode = chain+
112113
lib_compat_mode = soft
113114
check_tool = cppcheck

src/BLE_Client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ void bleClientTask(void* pvParameters) {
239239
ss2k->goHome(true);
240240
} else { // Startup Homing
241241
ss2k->goHome(false);
242+
rtConfig->setShifterPosition(8); // Set to middle position after homing on startup
242243
}
243244
spinBLEServer.spinDownFlag = 0;
244245
}

src/ERG_Mode.cpp

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ void ErgMode::runERG() {
121121
void ErgMode::computeErg() {
122122
Measurement newWatts = rtConfig->watts;
123123
int newCadence = rtConfig->cad.getValue();
124+
int32_t result = RETURN_ERROR;
124125

125126
bool isUserSpinning = this->_userIsSpinning(newCadence, ss2k->getCurrentPosition());
126127
if (!isUserSpinning) {
@@ -141,29 +142,29 @@ void ErgMode::computeErg() {
141142
}
142143

143144
#ifdef ERG_MODE_USE_POWER_TABLE
144-
// SetPoint changed
145-
#ifdef ERG_MODE_USE_PID
146145
if (abs(this->setPoint - newWatts.getTarget()) > ERG_MODE_PID_WINDOW && rtConfig->getHomed()) {
147-
#endif
148-
_setPointChangeState(newCadence, newWatts);
149-
return;
150-
#ifdef ERG_MODE_USE_PID
146+
result = _setPointChangeState(newCadence, newWatts);
151147
}
152148
#endif
153-
#endif
154-
155149
#ifdef ERG_MODE_USE_PID
156150
// Setpoint unchanged
157-
_inSetpointState(newCadence, newWatts);
151+
if (result == INT32_MIN) {
152+
result = _inSetpointState(newCadence, newWatts);
153+
}
158154
#endif
155+
_updateValues(newCadence, newWatts, result);
159156
}
160157

161-
void ErgMode::_setPointChangeState(int newCadence, Measurement& newWatts) {
162-
// It's better to undershoot increasing watts and overshoot decreasing watts, so lets set the lookup target to the nearest side of ERG_MODE_PID_WINDOW
163-
int adjustedTarget = newWatts.getTarget() >= newWatts.getValue() ? newWatts.getTarget() - ERG_MODE_PID_WINDOW : newWatts.getTarget() + ERG_MODE_PID_WINDOW;
164-
158+
int32_t ErgMode::_setPointChangeState(int newCadence, Measurement& newWatts) {
159+
// It's better to undershoot increasing watts and overshoot decreasing watts, so lets set the lookup target to the nearest side of POWERTABLE_WATT_INCREMENT
160+
int adjustedTarget = newWatts.getTarget() >= newWatts.getValue() ? newWatts.getTarget() - POWERTABLE_WATT_INCREMENT : newWatts.getTarget() + POWERTABLE_WATT_INCREMENT;
165161
int32_t tableResult = powerTable->lookup(adjustedTarget, newCadence);
166162

163+
// A lot of times this likes to undershoot going from High to low. Lets fix it.
164+
if (adjustedTarget < newWatts.getValue() && adjustedTarget < 200) {
165+
adjustedTarget = (adjustedTarget + ss2k->getCurrentPosition()) / 2;
166+
}
167+
167168
// Test current watts against the table result. If We're already lower or higher than target, flag the result as a return error.
168169
if (tableResult != RETURN_ERROR) {
169170
if (rtConfig->watts.getValue() > adjustedTarget && tableResult > ss2k->getCurrentPosition()) {
@@ -176,26 +177,32 @@ void ErgMode::_setPointChangeState(int newCadence, Measurement& newWatts) {
176177
}
177178
}
178179

180+
// Sanity check - with homing enabled, we should never have a negative result. If we do, something went wrong.
181+
if (rtConfig->getHomed() && tableResult < 0) {
182+
SS2K_LOG(ERG_MODE_LOG_TAG, "PowerTable returned negative result with homing enabled. Using PID");
183+
tableResult = RETURN_ERROR;
184+
}
185+
179186
// Handle return errors
180187
if (tableResult == RETURN_ERROR) {
181188
SS2K_LOG(ERG_MODE_LOG_TAG, "Lookup Error. Using PID");
182-
_inSetpointState(newCadence, newWatts);
183-
return;
184-
}
185-
186-
SS2K_LOG(ERG_MODE_LOG_TAG, "SetPoint changed:%dw PowerTable Result: %d", adjustedTarget, tableResult);
187-
_updateValues(newCadence, newWatts, tableResult);
188-
189-
if (rtConfig->getTargetIncline() != ss2k->getCurrentPosition()) { // add some time to wait while the knob moves to target position.
190-
isDelayed = true;
191-
int timeToAdd = abs(ss2k->getCurrentPosition() - rtConfig->getTargetIncline());
192-
if (timeToAdd > 3000) { // 3 seconds
193-
SS2K_LOG(ERG_MODE_LOG_TAG, "Capping ERG seek time to 3 seconds");
194-
timeToAdd = 3000;
189+
tableResult = _inSetpointState(newCadence, newWatts);
190+
} else {
191+
SS2K_LOG(ERG_MODE_LOG_TAG, "SetPoint changed:%dw PowerTable Result: %d", adjustedTarget, tableResult);
192+
if (tableResult != ss2k->getCurrentPosition()) { // add some time to wait while the knob moves to target position.
193+
isDelayed = true;
194+
long int stepDistance = abs(ss2k->getCurrentPosition() - tableResult);
195+
// Calculate time to add based on step distance and stepper speed
196+
long int timeToAdd = round(((double)stepDistance * 1000.0) / (double)userConfig->getStepperSpeed());
197+
if (timeToAdd > 3000) { // 3 seconds
198+
SS2K_LOG(ERG_MODE_LOG_TAG, "Capping ERG seek time to 3 seconds");
199+
timeToAdd = 3000;
200+
}
201+
ergTimer += timeToAdd;
195202
}
196-
ergTimer += timeToAdd;
203+
ergTimer += (ERG_MODE_DELAY); // Wait for power meter to register new watts
197204
}
198-
ergTimer += (ERG_MODE_DELAY); // Wait for power meter to register new watts
205+
return tableResult;
199206
}
200207

201208
// INTRODUCING PID CONTROL LOOP
@@ -206,63 +213,53 @@ void ErgMode::_setPointChangeState(int newCadence, Measurement& newWatts) {
206213
// Derivative term: rate of change of error
207214

208215
// PrevError
209-
void ErgMode::_inSetpointState(int newCadence, Measurement& newWatts) {
216+
int32_t ErgMode::_inSetpointState(int newCadence, Measurement& newWatts) {
210217
// Setting Gains For PID Loop
211-
float Kp = userConfig->getERGSensitivity();
212-
float Ki = 0.5;
213-
float Kd = 0.1;
214-
215-
static float integral = 0.0;
216-
static float prevError = 0.0;
218+
double Kp = userConfig->getERGSensitivity(); // Proportional gain based on user sensitivity
217219

218220
// retrieves the current Watt output
219221
int watts = newWatts.getValue();
220222
// retrieves target Watt output
221223
int target = newWatts.getTarget();
222224
// subtracting target from current watts
223-
float error = target - watts;
225+
int error = target - watts;
226+
227+
// modifying gains based on error
228+
if (abs(error) < 10) {
229+
Kp = Kp * 0.25; // decrease further for tiny errors
230+
} else if (abs(error) < 50) {
231+
Kp = Kp * 0.75; // Moderate for medium errors
232+
} else if (abs(error) > 100) {
233+
Kp = Kp * 1.25; // Aggressive for large errors
234+
}
224235

225236
// Defining proportional term
226-
float proportional = Kp * error;
237+
double proportional = Kp * error;
227238
if (newWatts.getValue() < userConfig->getMinWatts()) {
228239
proportional = proportional * userConfig->getERGSensitivity(); // increase proportional term when at very low watts. Prevents Zwift from timeout on initial interval.
229240
}
230241

231-
// Defining integral term
232-
integral += error;
233-
float integralFinal = Ki * integral;
234-
235-
// Clamping down integral term
236-
float integralMax = 60 * userConfig->getERGSensitivity();
237-
float integralMin = -60 * userConfig->getERGSensitivity();
238-
239-
if (integral > integralMax) {
240-
integral = integralMax;
241-
} else if (integral < integralMin) {
242-
integral = integralMin;
243-
}
244-
245-
// Defining derivative term
246-
float derivative = error - prevError; // Difference between current and previous errors
247-
float derivativeTerm = Kd * derivative;
248-
249242
// final PID output
250-
float PID_output = proportional + integralFinal + derivativeTerm;
243+
double PID_output = proportional;
251244

252-
// log proportional, integral, derivative every five seconds
245+
// log proportional every five seconds
253246
static unsigned long lastTime = 0;
254247
if (millis() - lastTime > 5000) {
255248
lastTime = millis();
256-
SS2K_LOG(ERG_MODE_LOG_TAG, "Proportional: %f, Integral: %f, Derivative: %f", proportional, integralFinal, derivativeTerm);
249+
SS2K_LOG(ERG_MODE_LOG_TAG, "Proportional: %f", proportional);
250+
}
251+
252+
// Cap the change to no more than we can move until the next reading
253+
int maxChange = round((long)((userConfig->getStepperSpeed() * ERG_MODE_DELAY)) / 1000.0f); // max change based on stepper speed and delay
254+
if (PID_output > maxChange) {
255+
PID_output = maxChange;
256+
} else if (PID_output < -maxChange) {
257+
PID_output = -maxChange;
257258
}
258259

259260
// Calculate new incline
260261
float newIncline = ss2k->getCurrentPosition() + PID_output;
261-
262-
prevError = error;
263-
264-
// Apply the new values
265-
_updateValues(newCadence, newWatts, newIncline);
262+
return newIncline;
266263
}
267264

268265
void ErgMode::_updateValues(int newCadence, Measurement& newWatts, float newIncline) {

src/PowerTable_Helpers.cpp

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,15 @@ int32_t PTHelpers::lookup(int watts, int cad, PTData& ptData) {
141141
}
142142
}
143143
}
144+
144145
if (resistance != RETURN_ERROR) {
145146
SS2K_LOG(PTDATA_LOG_TAG, "Extrapolated resistance: %d for watts=%d, cad=%d", resistance, watts, cad);
146147
// Return early if we found a valid extrapolated value
147148
} else {
148149
SS2K_LOG(PTDATA_LOG_TAG, "Extrapolation failed for watts=%d, cad=%d", watts, cad);
149150
}
150151

151-
return resistance; // All lookup methods failed
152+
return resistance;
152153
}
153154

154155
/**
@@ -374,7 +375,7 @@ int PTHelpers::extrapolateWattsFromCadence(int cad, int32_t targetPosition, PTDa
374375
* prerequisite for the PAVA functions to work correctly and prevent line crossings.
375376
* @param ptData The main power table data structure.
376377
*/
377-
void fillGaps(PTData& ptData) {
378+
void PTHelpers::fillGaps(PTData& ptData) {
378379
for (int i = 0; i < POWERTABLE_CAD_SIZE; ++i) { // For each row
379380
for (int j = 0; j < POWERTABLE_WATT_SIZE; ++j) { // For each cell
380381
if (ptData.tableRow[i].tableEntry[j].targetPosition == INT16_MIN) {
@@ -614,7 +615,7 @@ void PTHelpers::enterData(PTData& ptData, ptIndex index, int pos) {
614615
}
615616

616617
// // After updating a point, re-process the entire table to enforce global monotonicity.
617-
fillGaps(ptData);
618+
this->fillGaps(ptData);
618619
// for (int i = 0; i < 10; i++) { // Run the PAVA functions multiple times to ensure convergence
619620
bool caddone = false;
620621
bool wattdone = false;
@@ -635,9 +636,11 @@ void PTHelpers::enterData(PTData& ptData, ptIndex index, int pos) {
635636

636637
void PTHelpers::clean(PTData& ptData) {
637638
int removed = 0;
639+
640+
// First pass: remove entries with readings < 1 or negative positions
638641
for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) {
639642
for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) {
640-
if (ptData.tableRow[i].tableEntry[j].readings < 1) {
643+
if (ptData.tableRow[i].tableEntry[j].readings < 1 || ptData.tableRow[i].tableEntry[j].targetPosition < 0) {
641644
if (ptData.tableRow[i].tableEntry[j].targetPosition != INT16_MIN) {
642645
removed++;
643646
}
@@ -646,6 +649,42 @@ void PTHelpers::clean(PTData& ptData) {
646649
}
647650
}
648651
}
652+
653+
// Second pass: remove duplicate values in columns (same resistance for multiple cadences)
654+
// For each column, track all unique values and remove duplicates
655+
for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) {
656+
for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) {
657+
if (ptData.tableRow[i].tableEntry[j].targetPosition == INT16_MIN) continue;
658+
659+
int16_t currentValue = ptData.tableRow[i].tableEntry[j].targetPosition;
660+
661+
// Check all other entries in this column for the same value
662+
for (int k = i + 1; k < POWERTABLE_CAD_SIZE; k++) {
663+
if (ptData.tableRow[k].tableEntry[j].targetPosition == currentValue) {
664+
// Found duplicate - remove the one with fewer readings
665+
// If readings are equal, keep the one at higher cadence (higher index)
666+
if (ptData.tableRow[k].tableEntry[j].readings < ptData.tableRow[i].tableEntry[j].readings) {
667+
ptData.tableRow[k].tableEntry[j].targetPosition = INT16_MIN;
668+
ptData.tableRow[k].tableEntry[j].readings = 0;
669+
removed++;
670+
} else if (ptData.tableRow[k].tableEntry[j].readings > ptData.tableRow[i].tableEntry[j].readings) {
671+
// Current entry has fewer readings, mark it for removal and update current to the better one
672+
ptData.tableRow[i].tableEntry[j].targetPosition = INT16_MIN;
673+
ptData.tableRow[i].tableEntry[j].readings = 0;
674+
removed++;
675+
break; // Move to next i since current is removed
676+
} else {
677+
// Readings are equal - keep the one at higher cadence (k), remove the one at i
678+
ptData.tableRow[i].tableEntry[j].targetPosition = INT16_MIN;
679+
ptData.tableRow[i].tableEntry[j].readings = 0;
680+
removed++;
681+
break; // Move to next i since current is removed
682+
}
683+
}
684+
}
685+
}
686+
}
687+
649688
if (removed > 0) {
650689
SS2K_LOG(PTDATA_LOG_TAG, "Cleaned %d readings", removed);
651690
}

0 commit comments

Comments
 (0)