Skip to content

Commit 40f1d7c

Browse files
authored
Merge branch 'master' into applewatch-as-treadmill-speed
2 parents 22cac3e + 88c97c0 commit 40f1d7c

8 files changed

Lines changed: 268 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ src/inner_templates/googlemaps/cesium-key.js
5353
/tst/Devices/.vs
5454
src/inner_templates/googlemaps/cesium-key.js
5555
src/qdomyos-zwift.pro.user.49de507
56-
/.vs
56+
/.vs

src/devices/cscbike/cscbike.cpp

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,24 @@ cscbike::cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevi
3030
}
3131

3232
void cscbike::enableManualResistancePowerAdjustment(resistance_t resistance) {
33-
if (!jorotoBike) {
33+
if (!jorotoBike && !useCustomResistancePowerTable()) {
3434
return;
3535
}
3636

37-
resistance_t clampedResistance = qBound<resistance_t>(1, resistance, 15);
37+
resistance_t clampedResistance =
38+
jorotoBike ? qBound<resistance_t>(1, resistance, 15) : clampedCustomResistance(resistance);
3839
manualResistanceTarget = clampedResistance;
3940
manualResistancePowerAdjustmentActive = true;
4041
Resistance = clampedResistance;
4142
emit resistanceRead(Resistance.value());
4243

4344
if (!manualResistancePowerAdjustmentToastShown && homeform::singleton()) {
4445
homeform::singleton()->setToastRequested(
45-
QStringLiteral("Manual resistance power adjustment enabled: power now scales with the Resistance tile value."));
46+
jorotoBike
47+
? QStringLiteral(
48+
"Manual resistance power adjustment enabled: power now scales with the Resistance tile value.")
49+
: QStringLiteral(
50+
"Custom CSC power table enabled: power now follows the configured resistance/watt points."));
4651
manualResistancePowerAdjustmentToastShown = true;
4752
}
4853
}
@@ -60,10 +65,65 @@ uint16_t cscbike::manualResistanceAdjustedWatts() {
6065
return qRound(cadenceOnlyWatts * manualResistancePowerMultiplier());
6166
}
6267

68+
uint16_t cscbike::customResistanceAdjustedWatts() {
69+
if (currentCadence().value() == 0) {
70+
return 0;
71+
}
72+
73+
QSettings settings;
74+
const double resistanceLevel1 =
75+
settings.value(QZSettings::cscbike_custom_resistance_level_1,
76+
QZSettings::default_cscbike_custom_resistance_level_1)
77+
.toDouble();
78+
const double watt1 =
79+
settings.value(QZSettings::cscbike_custom_watt_1, QZSettings::default_cscbike_custom_watt_1).toDouble();
80+
const double resistanceLevel2 =
81+
settings.value(QZSettings::cscbike_custom_resistance_level_2,
82+
QZSettings::default_cscbike_custom_resistance_level_2)
83+
.toDouble();
84+
const double watt2 =
85+
settings.value(QZSettings::cscbike_custom_watt_2, QZSettings::default_cscbike_custom_watt_2).toDouble();
86+
const double resistance = clampedCustomResistance(manualResistanceTarget);
87+
88+
if (resistanceLevel1 == resistanceLevel2) {
89+
return qMax(0, qRound((watt1 + watt2) / 2.0));
90+
}
91+
92+
const double slope = (watt2 - watt1) / (resistanceLevel2 - resistanceLevel1);
93+
const double watts = watt1 + ((resistance - resistanceLevel1) * slope);
94+
return qMax(0, qRound(watts));
95+
}
96+
6397
double cscbike::manualResistancePowerMultiplier() {
6498
const double normalizedResistance = (qBound(1, static_cast<int>(manualResistanceTarget), 15) - 1) / 14.0;
6599
return 1.0 + (normalizedResistance * normalizedResistance * 2.0);
66100
}
101+
102+
bool cscbike::useCustomResistancePowerTable() const {
103+
QSettings settings;
104+
return settings
105+
.value(QZSettings::cscbike_custom_resistance_power_table,
106+
QZSettings::default_cscbike_custom_resistance_power_table)
107+
.toBool();
108+
}
109+
110+
resistance_t cscbike::clampedCustomResistance(resistance_t resistance) const {
111+
QSettings settings;
112+
int resistanceMin =
113+
qRound(settings.value(QZSettings::zwift_erg_resistance_down,
114+
QZSettings::default_zwift_erg_resistance_down)
115+
.toDouble());
116+
int resistanceMax =
117+
qRound(settings.value(QZSettings::zwift_erg_resistance_up,
118+
QZSettings::default_zwift_erg_resistance_up)
119+
.toDouble());
120+
121+
if (resistanceMin > resistanceMax) {
122+
qSwap(resistanceMin, resistanceMax);
123+
}
124+
125+
return qBound(static_cast<resistance_t>(resistanceMin), resistance, static_cast<resistance_t>(resistanceMax));
126+
}
67127
/*
68128
void cscbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool
69129
wait_for_response)
@@ -111,8 +171,10 @@ void cscbike::update() {
111171

112172
bool rogue_echo_bike = settings.value(QZSettings::rogue_echo_bike, QZSettings::default_rogue_echo_bike).toBool();
113173

114-
if (manualResistancePowerAdjustmentActive) {
174+
if (manualResistancePowerAdjustmentActive && jorotoBike) {
115175
m_watt = manualResistanceAdjustedWatts();
176+
} else if (manualResistancePowerAdjustmentActive && useCustomResistancePowerTable()) {
177+
m_watt = customResistanceAdjustedWatts();
116178
} else if (rogue_echo_bike) {
117179
double rpm = currentCadence().value();
118180
m_watt = 0.000602337 * pow(rpm, 3.11762) + 32.6404;

src/devices/cscbike/cscbike.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class cscbike : public bike {
4545
// bool wait_for_response = false);
4646
void startDiscover();
4747
uint16_t watts() override;
48+
bool useCustomResistancePowerTable() const;
49+
resistance_t clampedCustomResistance(resistance_t resistance) const;
4850

4951
QTimer *refresh;
5052

@@ -82,6 +84,7 @@ class cscbike : public bike {
8284
#endif
8385

8486
uint16_t manualResistanceAdjustedWatts();
87+
uint16_t customResistanceAdjustedWatts();
8588
double manualResistancePowerMultiplier();
8689

8790
signals:

src/homeform.cpp

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,6 +1590,8 @@ void homeform::trainProgramSignals() {
15901590
disconnect(trainProgram, &trainprogram::lap, this, &homeform::Lap);
15911591
disconnect(trainProgram, &trainprogram::changeSpeed, ((treadmill *)bluetoothManager->device()),
15921592
&treadmill::changeSpeed);
1593+
disconnect(trainProgram, &trainprogram::changeSpeed, this,
1594+
&homeform::onTrainingProgramSpeedChanged);
15931595
disconnect(trainProgram, &trainprogram::changeInclination, ((treadmill *)bluetoothManager->device()),
15941596
&treadmill::changeInclination);
15951597
disconnect(trainProgram, &trainprogram::changeNextInclination300Meters, bluetoothManager->device(),
@@ -1647,6 +1649,9 @@ void homeform::trainProgramSignals() {
16471649
connect(trainProgram, &trainprogram::stop, this, &homeform::StopFromTrainProgram);
16481650
connect(trainProgram, &trainprogram::lap, this, &homeform::Lap);
16491651
connect(trainProgram, &trainprogram::toastRequest, this, &homeform::onToastRequested);
1652+
// Connect training program speed changes to reset HR PID timer
1653+
connect(trainProgram, &trainprogram::changeSpeed, this,
1654+
&homeform::onTrainingProgramSpeedChanged);
16501655
if (bluetoothManager->device()->deviceType() == TREADMILL) {
16511656
connect(trainProgram, &trainprogram::changeSpeed, ((treadmill *)bluetoothManager->device()),
16521657
&treadmill::changeSpeed);
@@ -1738,6 +1743,13 @@ void homeform::onToastRequested(QString message) {
17381743
}
17391744
}
17401745

1746+
void homeform::onTrainingProgramSpeedChanged(double speed) {
1747+
// Record the timestamp when the training program changed speed
1748+
// This is used by the HR PID controller to avoid race conditions
1749+
lastTrainingProgramSpeedChange = QDateTime::currentDateTime();
1750+
}
1751+
1752+
17411753
QStringList homeform::tile_order() {
17421754

17431755
QStringList r;
@@ -7300,6 +7312,12 @@ void homeform::update() {
73007312

73017313
if (!stopped && !paused && bluetoothManager->device()->currentHeart().value() && zone > 0 &&
73027314
bluetoothManager->device()->currentSpeed().value() > 0.0f) {
7315+
// Skip HR PID adjustments for a period after training program changes speed
7316+
// This prevents race conditions where HR PID overwrites training program speed changes
7317+
qint64 msSinceSpeedChange = lastTrainingProgramSpeedChange.msecsTo(QDateTime::currentDateTime());
7318+
bool recentSpeedChange = (msSinceSpeedChange < (delta * 1000));
7319+
7320+
if (!recentSpeedChange) {
73037321
if (bluetoothManager->device()->deviceType() == TREADMILL) {
73047322

73057323
const double step = 0.2;
@@ -7411,6 +7429,7 @@ void homeform::update() {
74117429
((rower *)bluetoothManager->device())->changeResistance(currentResistance + step);
74127430
}
74137431
}
7432+
} // Close the if (!recentSpeedChange) block
74147433
}
74157434
}
74167435
} else if ((settings.value(QZSettings::treadmill_pid_heart_min, QZSettings::default_treadmill_pid_heart_min)
@@ -7468,6 +7487,12 @@ void homeform::update() {
74687487
<< QStringLiteral("HRmin:") << hrmin << QStringLiteral("HRmax:") << hrmax
74697488
<< QStringLiteral("fromTrainProgram:") << fromTrainProgram;
74707489

7490+
// Skip HR PID adjustments for a period after training program changes speed
7491+
// This prevents race conditions where HR PID overwrites training program speed changes
7492+
qint64 msSinceSpeedChange = lastTrainingProgramSpeedChange.msecsTo(QDateTime::currentDateTime());
7493+
bool recentSpeedChange = (msSinceSpeedChange < (delta * 1000));
7494+
7495+
if (!recentSpeedChange) {
74717496
if (bluetoothManager->device()->deviceType() == TREADMILL) {
74727497

74737498
const double step = 0.2;
@@ -7589,10 +7614,11 @@ void homeform::update() {
75897614
qDebug() << QStringLiteral("ROWING PID HR - HR < HRmin, INCREASING resistance from")
75907615
<< currentResistance << QStringLiteral("to") << (currentResistance + step);
75917616
((rower *)bluetoothManager->device())->changeResistance(currentResistance + step);
7592-
} else {
7593-
qDebug() << QStringLiteral("ROWING PID HR - No action taken (in zone or at limits)");
7594-
}
7595-
}
7617+
} else {
7618+
qDebug() << QStringLiteral("ROWING PID HR - No action taken (in zone or at limits)");
7619+
}
7620+
}
7621+
} // Close the if (!recentSpeedChange) block
75967622
}
75977623
}
75987624
}

src/homeform.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,9 @@ class homeform : public QObject {
974974
QTimer *backupTimer;
975975
QTimer *automaticShiftingTimer;
976976

977+
// HR PID controller state - tracks when training program changes speed to prevent race conditions
978+
QDateTime lastTrainingProgramSpeedChange = QDateTime::fromMSecsSinceEpoch(0);
979+
977980
// FIT backup threading
978981
QThread *fitBackupThread;
979982
FitBackupWriter *fitBackupWriter;
@@ -1089,6 +1092,7 @@ class homeform : public QObject {
10891092
void saveSessionAsTrainingProgram();
10901093
void strava_connect_clicked();
10911094
void trainProgramSignals();
1095+
void onTrainingProgramSpeedChanged(double speed);
10921096
void refresh_bluetooth_devices_clicked();
10931097
void onStravaGranted();
10941098
void onStravaAuthorizeWithBrowser(const QUrl &url);

src/qzsettings.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ const QString QZSettings::default_cadence_sensor_name = QStringLiteral("Disabled
329329
const QString QZSettings::cadence_sensor_as_bike = QStringLiteral("cadence_sensor_as_bike");
330330
const QString QZSettings::cadence_sensor_as_treadmill = QStringLiteral("cadence_sensor_as_treadmill");
331331
const QString QZSettings::cadence_sensor_speed_ratio = QStringLiteral("cadence_sensor_speed_ratio");
332+
const QString QZSettings::cscbike_custom_resistance_power_table = QStringLiteral("cscbike_custom_resistance_power_table");
333+
const QString QZSettings::cscbike_custom_resistance_level_1 = QStringLiteral("cscbike_custom_resistance_level_1");
334+
const QString QZSettings::cscbike_custom_watt_1 = QStringLiteral("cscbike_custom_watt_1");
335+
const QString QZSettings::cscbike_custom_resistance_level_2 = QStringLiteral("cscbike_custom_resistance_level_2");
336+
const QString QZSettings::cscbike_custom_watt_2 = QStringLiteral("cscbike_custom_watt_2");
332337
const QString QZSettings::power_hr_pwr1 = QStringLiteral("power_hr_pwr1");
333338
const QString QZSettings::power_hr_hr1 = QStringLiteral("power_hr_hr1");
334339
const QString QZSettings::power_hr_pwr2 = QStringLiteral("power_hr_pwr2");
@@ -1084,7 +1089,7 @@ const QString QZSettings::proform_carbon_tl_PFTL59723_6 = QStringLiteral("profor
10841089
const QString QZSettings::applewatch_as_treadmill_speed = QStringLiteral("applewatch_as_treadmill_speed");
10851090

10861091

1087-
const uint32_t allSettingsCount = 883;
1092+
const uint32_t allSettingsCount = 888;
10881093

10891094
QVariant allSettings[allSettingsCount][2] = {
10901095
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1353,6 +1358,11 @@ QVariant allSettings[allSettingsCount][2] = {
13531358
{QZSettings::cadence_sensor_as_bike, QZSettings::default_cadence_sensor_as_bike},
13541359
{QZSettings::cadence_sensor_as_treadmill, QZSettings::default_cadence_sensor_as_treadmill},
13551360
{QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio},
1361+
{QZSettings::cscbike_custom_resistance_power_table, QZSettings::default_cscbike_custom_resistance_power_table},
1362+
{QZSettings::cscbike_custom_resistance_level_1, QZSettings::default_cscbike_custom_resistance_level_1},
1363+
{QZSettings::cscbike_custom_watt_1, QZSettings::default_cscbike_custom_watt_1},
1364+
{QZSettings::cscbike_custom_resistance_level_2, QZSettings::default_cscbike_custom_resistance_level_2},
1365+
{QZSettings::cscbike_custom_watt_2, QZSettings::default_cscbike_custom_watt_2},
13561366
{QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1},
13571367
{QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1},
13581368
{QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2},

src/qzsettings.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,21 @@ class QZSettings {
969969
static const QString cadence_sensor_speed_ratio;
970970
static constexpr float default_cadence_sensor_speed_ratio = 0.33;
971971

972+
static const QString cscbike_custom_resistance_power_table;
973+
static constexpr bool default_cscbike_custom_resistance_power_table = false;
974+
975+
static const QString cscbike_custom_resistance_level_1;
976+
static constexpr float default_cscbike_custom_resistance_level_1 = 1;
977+
978+
static const QString cscbike_custom_watt_1;
979+
static constexpr float default_cscbike_custom_watt_1 = 100;
980+
981+
static const QString cscbike_custom_resistance_level_2;
982+
static constexpr float default_cscbike_custom_resistance_level_2 = 15;
983+
984+
static const QString cscbike_custom_watt_2;
985+
static constexpr float default_cscbike_custom_watt_2 = 300;
986+
972987
static const QString power_hr_pwr1;
973988
static constexpr float default_power_hr_pwr1 = 200;
974989

0 commit comments

Comments
 (0)