diff --git a/src/devices/cscbike/cscbike.cpp b/src/devices/cscbike/cscbike.cpp index e9f3aaf593..86af079bc8 100644 --- a/src/devices/cscbike/cscbike.cpp +++ b/src/devices/cscbike/cscbike.cpp @@ -30,11 +30,12 @@ cscbike::cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevi } void cscbike::enableManualResistancePowerAdjustment(resistance_t resistance) { - if (!jorotoBike) { + if (!jorotoBike && !useCustomResistancePowerTable()) { return; } - resistance_t clampedResistance = qBound(1, resistance, 15); + resistance_t clampedResistance = + jorotoBike ? qBound(1, resistance, 15) : clampedCustomResistance(resistance); manualResistanceTarget = clampedResistance; manualResistancePowerAdjustmentActive = true; Resistance = clampedResistance; @@ -42,7 +43,11 @@ void cscbike::enableManualResistancePowerAdjustment(resistance_t resistance) { if (!manualResistancePowerAdjustmentToastShown && homeform::singleton()) { homeform::singleton()->setToastRequested( - QStringLiteral("Manual resistance power adjustment enabled: power now scales with the Resistance tile value.")); + jorotoBike + ? QStringLiteral( + "Manual resistance power adjustment enabled: power now scales with the Resistance tile value.") + : QStringLiteral( + "Custom CSC power table enabled: power now follows the configured resistance/watt points.")); manualResistancePowerAdjustmentToastShown = true; } } @@ -60,10 +65,65 @@ uint16_t cscbike::manualResistanceAdjustedWatts() { return qRound(cadenceOnlyWatts * manualResistancePowerMultiplier()); } +uint16_t cscbike::customResistanceAdjustedWatts() { + if (currentCadence().value() == 0) { + return 0; + } + + QSettings settings; + const double resistanceLevel1 = + settings.value(QZSettings::cscbike_custom_resistance_level_1, + QZSettings::default_cscbike_custom_resistance_level_1) + .toDouble(); + const double watt1 = + settings.value(QZSettings::cscbike_custom_watt_1, QZSettings::default_cscbike_custom_watt_1).toDouble(); + const double resistanceLevel2 = + settings.value(QZSettings::cscbike_custom_resistance_level_2, + QZSettings::default_cscbike_custom_resistance_level_2) + .toDouble(); + const double watt2 = + settings.value(QZSettings::cscbike_custom_watt_2, QZSettings::default_cscbike_custom_watt_2).toDouble(); + const double resistance = clampedCustomResistance(manualResistanceTarget); + + if (resistanceLevel1 == resistanceLevel2) { + return qMax(0, qRound((watt1 + watt2) / 2.0)); + } + + const double slope = (watt2 - watt1) / (resistanceLevel2 - resistanceLevel1); + const double watts = watt1 + ((resistance - resistanceLevel1) * slope); + return qMax(0, qRound(watts)); +} + double cscbike::manualResistancePowerMultiplier() { const double normalizedResistance = (qBound(1, static_cast(manualResistanceTarget), 15) - 1) / 14.0; return 1.0 + (normalizedResistance * normalizedResistance * 2.0); } + +bool cscbike::useCustomResistancePowerTable() const { + QSettings settings; + return settings + .value(QZSettings::cscbike_custom_resistance_power_table, + QZSettings::default_cscbike_custom_resistance_power_table) + .toBool(); +} + +resistance_t cscbike::clampedCustomResistance(resistance_t resistance) const { + QSettings settings; + int resistanceMin = + qRound(settings.value(QZSettings::zwift_erg_resistance_down, + QZSettings::default_zwift_erg_resistance_down) + .toDouble()); + int resistanceMax = + qRound(settings.value(QZSettings::zwift_erg_resistance_up, + QZSettings::default_zwift_erg_resistance_up) + .toDouble()); + + if (resistanceMin > resistanceMax) { + qSwap(resistanceMin, resistanceMax); + } + + return qBound(static_cast(resistanceMin), resistance, static_cast(resistanceMax)); +} /* void cscbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool wait_for_response) @@ -111,8 +171,10 @@ void cscbike::update() { bool rogue_echo_bike = settings.value(QZSettings::rogue_echo_bike, QZSettings::default_rogue_echo_bike).toBool(); - if (manualResistancePowerAdjustmentActive) { + if (manualResistancePowerAdjustmentActive && jorotoBike) { m_watt = manualResistanceAdjustedWatts(); + } else if (manualResistancePowerAdjustmentActive && useCustomResistancePowerTable()) { + m_watt = customResistanceAdjustedWatts(); } else if (rogue_echo_bike) { double rpm = currentCadence().value(); m_watt = 0.000602337 * pow(rpm, 3.11762) + 32.6404; diff --git a/src/devices/cscbike/cscbike.h b/src/devices/cscbike/cscbike.h index 7e6f2eaa94..06ba5e94dd 100644 --- a/src/devices/cscbike/cscbike.h +++ b/src/devices/cscbike/cscbike.h @@ -45,6 +45,8 @@ class cscbike : public bike { // bool wait_for_response = false); void startDiscover(); uint16_t watts() override; + bool useCustomResistancePowerTable() const; + resistance_t clampedCustomResistance(resistance_t resistance) const; QTimer *refresh; @@ -82,6 +84,7 @@ class cscbike : public bike { #endif uint16_t manualResistanceAdjustedWatts(); + uint16_t customResistanceAdjustedWatts(); double manualResistancePowerMultiplier(); signals: diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index 5a80444d10..1d8cc0c12f 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -329,6 +329,11 @@ const QString QZSettings::default_cadence_sensor_name = QStringLiteral("Disabled const QString QZSettings::cadence_sensor_as_bike = QStringLiteral("cadence_sensor_as_bike"); const QString QZSettings::cadence_sensor_as_treadmill = QStringLiteral("cadence_sensor_as_treadmill"); const QString QZSettings::cadence_sensor_speed_ratio = QStringLiteral("cadence_sensor_speed_ratio"); +const QString QZSettings::cscbike_custom_resistance_power_table = QStringLiteral("cscbike_custom_resistance_power_table"); +const QString QZSettings::cscbike_custom_resistance_level_1 = QStringLiteral("cscbike_custom_resistance_level_1"); +const QString QZSettings::cscbike_custom_watt_1 = QStringLiteral("cscbike_custom_watt_1"); +const QString QZSettings::cscbike_custom_resistance_level_2 = QStringLiteral("cscbike_custom_resistance_level_2"); +const QString QZSettings::cscbike_custom_watt_2 = QStringLiteral("cscbike_custom_watt_2"); const QString QZSettings::power_hr_pwr1 = QStringLiteral("power_hr_pwr1"); const QString QZSettings::power_hr_hr1 = QStringLiteral("power_hr_hr1"); const QString QZSettings::power_hr_pwr2 = QStringLiteral("power_hr_pwr2"); @@ -1083,7 +1088,7 @@ const QString QZSettings::proform_carbon_tlx_v84_314_treadmill = QStringLiteral( const QString QZSettings::proform_carbon_tl_PFTL59723_6 = QStringLiteral("proform_carbon_tl_PFTL59723_6"); -const uint32_t allSettingsCount = 882; +const uint32_t allSettingsCount = 887; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1352,6 +1357,11 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::cadence_sensor_as_bike, QZSettings::default_cadence_sensor_as_bike}, {QZSettings::cadence_sensor_as_treadmill, QZSettings::default_cadence_sensor_as_treadmill}, {QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio}, + {QZSettings::cscbike_custom_resistance_power_table, QZSettings::default_cscbike_custom_resistance_power_table}, + {QZSettings::cscbike_custom_resistance_level_1, QZSettings::default_cscbike_custom_resistance_level_1}, + {QZSettings::cscbike_custom_watt_1, QZSettings::default_cscbike_custom_watt_1}, + {QZSettings::cscbike_custom_resistance_level_2, QZSettings::default_cscbike_custom_resistance_level_2}, + {QZSettings::cscbike_custom_watt_2, QZSettings::default_cscbike_custom_watt_2}, {QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1}, {QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1}, {QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2}, diff --git a/src/qzsettings.h b/src/qzsettings.h index ff4d15a86e..549acf6227 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -969,6 +969,21 @@ class QZSettings { static const QString cadence_sensor_speed_ratio; static constexpr float default_cadence_sensor_speed_ratio = 0.33; + static const QString cscbike_custom_resistance_power_table; + static constexpr bool default_cscbike_custom_resistance_power_table = false; + + static const QString cscbike_custom_resistance_level_1; + static constexpr float default_cscbike_custom_resistance_level_1 = 1; + + static const QString cscbike_custom_watt_1; + static constexpr float default_cscbike_custom_watt_1 = 100; + + static const QString cscbike_custom_resistance_level_2; + static constexpr float default_cscbike_custom_resistance_level_2 = 15; + + static const QString cscbike_custom_watt_2; + static constexpr float default_cscbike_custom_watt_2 = 300; + static const QString power_hr_pwr1; static constexpr float default_power_hr_pwr1 = 200; diff --git a/src/settings.qml b/src/settings.qml index ad1c339124..bb47b8d099 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -1313,6 +1313,11 @@ import Qt.labs.platform 1.1 property double power_sensor_speed_inclination_coeff_a: 0.0 property double power_sensor_speed_inclination_coeff_b: 0.0 property bool proform_carbon_tlx_v84_314_treadmill: false + property bool cscbike_custom_resistance_power_table: false + property real cscbike_custom_resistance_level_1: 1 + property real cscbike_custom_watt_1: 100 + property real cscbike_custom_resistance_level_2: 15 + property real cscbike_custom_watt_2: 300 } @@ -12029,6 +12034,138 @@ import Qt.labs.platform 1.1 Layout.fillWidth: true color: Material.color(Material.Lime) } + + IndicatorOnlySwitch { + text: qsTr("Custom CSC Resistance/Watt Table") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.cscbike_custom_resistance_power_table + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.cscbike_custom_resistance_power_table = checked; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("Enable a custom linear resistance/watt table for CSC bikes. Joroto bikes keep using their dedicated resistance power profile. Resistance is clamped using the existing Min. Resistance and Max. Resistance settings.") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Resistance Level 1:") + Layout.fillWidth: true + } + TextField { + id: cscBikeCustomResistanceLevel1TextField + text: settings.cscbike_custom_resistance_level_1 + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.cscbike_custom_resistance_level_1 = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.cscbike_custom_resistance_level_1 = cscBikeCustomResistanceLevel1TextField.text; toast.show("Setting saved!"); } + } + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Watt 1:") + Layout.fillWidth: true + } + TextField { + id: cscBikeCustomWatt1TextField + text: settings.cscbike_custom_watt_1 + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.cscbike_custom_watt_1 = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.cscbike_custom_watt_1 = cscBikeCustomWatt1TextField.text; toast.show("Setting saved!"); } + } + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Resistance Level 2:") + Layout.fillWidth: true + } + TextField { + id: cscBikeCustomResistanceLevel2TextField + text: settings.cscbike_custom_resistance_level_2 + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.cscbike_custom_resistance_level_2 = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.cscbike_custom_resistance_level_2 = cscBikeCustomResistanceLevel2TextField.text; toast.show("Setting saved!"); } + } + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Watt 2:") + Layout.fillWidth: true + } + TextField { + id: cscBikeCustomWatt2TextField + text: settings.cscbike_custom_watt_2 + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.cscbike_custom_watt_2 = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.cscbike_custom_watt_2 = cscBikeCustomWatt2TextField.text; toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("QZ will build a linear equation from the two resistance/watt points and clamp the effective resistance using the existing Min. Resistance and Max. Resistance settings.") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } } }