From a8ec0bf6ff67dd81a1d2ec8a066ad335be64f062 Mon Sep 17 00:00:00 2001 From: cysimons Date: Sun, 19 Apr 2026 21:56:00 +0200 Subject: [PATCH] Fix HR PID race condition with training program speed changes The HR controller was reading the current speed, calculating an adjustment, then firing the change. If a training program changed speed in between these steps, the HR controller would overwrite the new speed with a stale calculation. Solution: Track when the training program changes speed via a timestamp in homeform. Both HR PID modes (zone-based and min/max) now skip adjustments for the configured delta period (typically 10 seconds) after a training program speed change, allowing the body to respond to the new speed first. Changes: - Add lastTrainingProgramSpeedChange timestamp member to homeform - Add onTrainingProgramSpeedChanged() slot to record speed change timestamps - Connect training program changeSpeed signal to the new slot - Guard both HR PID adjustment sections with recentSpeedChange check - Skip adjustments if a training program speed change occurred within delta seconds - Fix indentation to match project style (cpp and header files) --- .gitignore | 2 +- src/homeform.cpp | 34 ++++++++++++++++++++++++++++++---- src/homeform.h | 4 ++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 4710683d85..3a5b3c8abc 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,4 @@ src/inner_templates/googlemaps/cesium-key.js /tst/Devices/.vs src/inner_templates/googlemaps/cesium-key.js src/qdomyos-zwift.pro.user.49de507 -/.vs +/.vs \ No newline at end of file diff --git a/src/homeform.cpp b/src/homeform.cpp index d6cc85bf19..1978b121d8 100644 --- a/src/homeform.cpp +++ b/src/homeform.cpp @@ -1590,6 +1590,8 @@ void homeform::trainProgramSignals() { disconnect(trainProgram, &trainprogram::lap, this, &homeform::Lap); disconnect(trainProgram, &trainprogram::changeSpeed, ((treadmill *)bluetoothManager->device()), &treadmill::changeSpeed); + disconnect(trainProgram, &trainprogram::changeSpeed, this, + &homeform::onTrainingProgramSpeedChanged); disconnect(trainProgram, &trainprogram::changeInclination, ((treadmill *)bluetoothManager->device()), &treadmill::changeInclination); disconnect(trainProgram, &trainprogram::changeNextInclination300Meters, bluetoothManager->device(), @@ -1647,6 +1649,9 @@ void homeform::trainProgramSignals() { connect(trainProgram, &trainprogram::stop, this, &homeform::StopFromTrainProgram); connect(trainProgram, &trainprogram::lap, this, &homeform::Lap); connect(trainProgram, &trainprogram::toastRequest, this, &homeform::onToastRequested); + // Connect training program speed changes to reset HR PID timer + connect(trainProgram, &trainprogram::changeSpeed, this, + &homeform::onTrainingProgramSpeedChanged); if (bluetoothManager->device()->deviceType() == TREADMILL) { connect(trainProgram, &trainprogram::changeSpeed, ((treadmill *)bluetoothManager->device()), &treadmill::changeSpeed); @@ -1738,6 +1743,13 @@ void homeform::onToastRequested(QString message) { } } +void homeform::onTrainingProgramSpeedChanged(double speed) { + // Record the timestamp when the training program changed speed + // This is used by the HR PID controller to avoid race conditions + lastTrainingProgramSpeedChange = QDateTime::currentDateTime(); +} + + QStringList homeform::tile_order() { QStringList r; @@ -7300,6 +7312,12 @@ void homeform::update() { if (!stopped && !paused && bluetoothManager->device()->currentHeart().value() && zone > 0 && bluetoothManager->device()->currentSpeed().value() > 0.0f) { + // Skip HR PID adjustments for a period after training program changes speed + // This prevents race conditions where HR PID overwrites training program speed changes + qint64 msSinceSpeedChange = lastTrainingProgramSpeedChange.msecsTo(QDateTime::currentDateTime()); + bool recentSpeedChange = (msSinceSpeedChange < (delta * 1000)); + + if (!recentSpeedChange) { if (bluetoothManager->device()->deviceType() == TREADMILL) { const double step = 0.2; @@ -7411,6 +7429,7 @@ void homeform::update() { ((rower *)bluetoothManager->device())->changeResistance(currentResistance + step); } } + } // Close the if (!recentSpeedChange) block } } } else if ((settings.value(QZSettings::treadmill_pid_heart_min, QZSettings::default_treadmill_pid_heart_min) @@ -7468,6 +7487,12 @@ void homeform::update() { << QStringLiteral("HRmin:") << hrmin << QStringLiteral("HRmax:") << hrmax << QStringLiteral("fromTrainProgram:") << fromTrainProgram; + // Skip HR PID adjustments for a period after training program changes speed + // This prevents race conditions where HR PID overwrites training program speed changes + qint64 msSinceSpeedChange = lastTrainingProgramSpeedChange.msecsTo(QDateTime::currentDateTime()); + bool recentSpeedChange = (msSinceSpeedChange < (delta * 1000)); + + if (!recentSpeedChange) { if (bluetoothManager->device()->deviceType() == TREADMILL) { const double step = 0.2; @@ -7589,10 +7614,11 @@ void homeform::update() { qDebug() << QStringLiteral("ROWING PID HR - HR < HRmin, INCREASING resistance from") << currentResistance << QStringLiteral("to") << (currentResistance + step); ((rower *)bluetoothManager->device())->changeResistance(currentResistance + step); - } else { - qDebug() << QStringLiteral("ROWING PID HR - No action taken (in zone or at limits)"); - } - } + } else { + qDebug() << QStringLiteral("ROWING PID HR - No action taken (in zone or at limits)"); + } + } + } // Close the if (!recentSpeedChange) block } } } diff --git a/src/homeform.h b/src/homeform.h index 5c8ddcc5fb..1f7c32e5ad 100644 --- a/src/homeform.h +++ b/src/homeform.h @@ -974,6 +974,9 @@ class homeform : public QObject { QTimer *backupTimer; QTimer *automaticShiftingTimer; + // HR PID controller state - tracks when training program changes speed to prevent race conditions + QDateTime lastTrainingProgramSpeedChange = QDateTime::fromMSecsSinceEpoch(0); + // FIT backup threading QThread *fitBackupThread; FitBackupWriter *fitBackupWriter; @@ -1089,6 +1092,7 @@ class homeform : public QObject { void saveSessionAsTrainingProgram(); void strava_connect_clicked(); void trainProgramSignals(); + void onTrainingProgramSpeedChanged(double speed); void refresh_bluetooth_devices_clicked(); void onStravaGranted(); void onStravaAuthorizeWithBrowser(const QUrl &url);