diff --git a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj index 26250a8bb..fa6126769 100644 --- a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj +++ b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj @@ -4068,7 +4068,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 919; + CURRENT_PROJECT_VERSION = 922; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1"; @@ -4259,7 +4259,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 919; + CURRENT_PROJECT_VERSION = 922; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; @@ -4486,7 +4486,7 @@ CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 919; + CURRENT_PROJECT_VERSION = 922; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -4582,7 +4582,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 919; + CURRENT_PROJECT_VERSION = 922; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; @@ -4674,7 +4674,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 919; + CURRENT_PROJECT_VERSION = 922; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; ENABLE_PREVIEWS = YES; @@ -4788,7 +4788,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 919; + CURRENT_PROJECT_VERSION = 922; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; diff --git a/src/devices/bike.cpp b/src/devices/bike.cpp index b64d715d5..597bf2151 100644 --- a/src/devices/bike.cpp +++ b/src/devices/bike.cpp @@ -106,6 +106,14 @@ void bike::setGears(double gears) { qDebug() << "new gear value ignored because of gears_zwift_ratio setting!"; return; } + if(gears > maxGears()) { + qDebug() << "new gear value ignored because of maxGears" << maxGears(); + return; + } + if(gears < minGears()) { + qDebug() << "new gear value ignored because of minGears" << minGears(); + return; + } m_gears = gears; if(homeform::singleton()) { homeform::singleton()->updateGearsValue(); diff --git a/src/devices/bike.h b/src/devices/bike.h index c296a9138..f075bd784 100644 --- a/src/devices/bike.h +++ b/src/devices/bike.h @@ -23,6 +23,8 @@ class bike : public bluetoothdevice { double currentCrankRevolutions() override; uint16_t lastCrankEventTime() override; bool connected() override; + virtual double maxGears() { return 9999.0; } + virtual double minGears() { return -9999.0; } virtual uint16_t watts(); virtual resistance_t pelotonToBikeResistance(int pelotonResistance); virtual resistance_t resistanceFromPowerRequest(uint16_t power); diff --git a/src/devices/bluetooth.cpp b/src/devices/bluetooth.cpp index 4690b16b8..f1472dad4 100644 --- a/src/devices/bluetooth.cpp +++ b/src/devices/bluetooth.cpp @@ -1511,7 +1511,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { (b.name().toUpper().startsWith(ftmsAccessoryName.toUpper()) && settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton) .toBool()) || // ss2k on a peloton bike - ((b.name().toUpper().startsWith("KICKR CORE")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || (b.name().toUpper().startsWith("MERACH-MR667-")) || (b.name().toUpper().startsWith("DS60-")) || (b.name().toUpper().startsWith("BIKE-")) || @@ -1562,6 +1561,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { this->signalBluetoothDeviceConnected(ftmsBike); } else if ((b.name().toUpper().startsWith("KICKR SNAP") || b.name().toUpper().startsWith("KICKR BIKE") || b.name().toUpper().startsWith("KICKR ROLLR") || + b.name().toUpper().startsWith("KICKR CORE") || (b.name().toUpper().startsWith("HAMMER ") && saris_trainer) || (b.name().toUpper().startsWith("WAHOO KICKR"))) && !wahooKickrSnapBike && !ftmsBike && filter) { diff --git a/src/devices/ftmsbike/ftmsbike.cpp b/src/devices/ftmsbike/ftmsbike.cpp index a118cb1f9..58f85b776 100644 --- a/src/devices/ftmsbike/ftmsbike.cpp +++ b/src/devices/ftmsbike/ftmsbike.cpp @@ -35,6 +35,8 @@ ftmsbike::ftmsbike(bool noWriteResistance, bool noHeartService, int8_t bikeResis initDone = false; connect(refresh, &QTimer::timeout, this, &ftmsbike::update); refresh->start(settings.value(QZSettings::poll_device_time, QZSettings::default_poll_device_time).toInt()); + wheelCircumference::GearTable g; + g.printTable(); } void ftmsbike::writeCharacteristicZwiftPlay(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, @@ -170,6 +172,18 @@ void ftmsbike::zwiftPlayInit() { } } +void ftmsbike::setWheelDiameter(double diameter) { + uint8_t write[] = {FTMS_SET_WHEEL_CIRCUMFERENCE, 0x00, 0x00}; + + diameter = diameter * 10.0; + + write[1] = ((uint16_t)diameter) & 0xFF; + write[2] = ((uint16_t)diameter) >> 8; + + writeCharacteristic(write, sizeof(write), QStringLiteral("setWheelCircumference ") + QString::number(diameter)); +} + + void ftmsbike::forcePower(int16_t requestPower) { if(resistance_lvl_mode) { forceResistance(resistanceFromPowerRequest(requestPower)); @@ -285,14 +299,16 @@ void ftmsbike::update() { if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && (requestPower == 0 || requestPower == -1)) { init(); - forceResistance(requestResistance + (gears() * 5)); + if(requestResistance != - 1) + forceResistance(requestResistance + (gears() * 5)); + else + setWheelDiameter(wheelCircumference::gearsToWheelDiameter(gears())); } } requestResistance = -1; } if((virtualBike && virtualBike->ftmsDeviceConnected()) && lastGearValue != gears() && lastRawRequestedInclinationValue != -100 && lastPacketFromFTMS.length() >= 7) { - qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP" << lastPacketFromFTMS.toHex(' '); - ftmsCharacteristicChanged(QLowEnergyCharacteristic(), lastPacketFromFTMS); + setWheelDiameter(wheelCircumference::gearsToWheelDiameter(gears())); } QSettings settings; @@ -1065,13 +1081,8 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact lastPacketFromFTMS.append(b.at(i)); qDebug() << "lastPacketFromFTMS" << lastPacketFromFTMS.toHex(' '); int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8)); - if (gears() != 0) { - slope += (gears() * 50); - } b[3] = slope & 0xFF; - b[4] = slope >> 8; - - qDebug() << "applying gears mod" << gears() << slope; + b[4] = slope >> 8; /*} else if(b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && zwiftPlayService != nullptr && gears_zwift_ratio) { int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8)); uint8_t gear2[] = {0x04, 0x22, 0x02, 0x10, 0x00}; diff --git a/src/devices/ftmsbike/ftmsbike.h b/src/devices/ftmsbike/ftmsbike.h index fe21e284b..fa4fd4e06 100644 --- a/src/devices/ftmsbike/ftmsbike.h +++ b/src/devices/ftmsbike/ftmsbike.h @@ -26,6 +26,7 @@ #include #include +#include "wheelcircumference.h" #include "devices/bike.h" #ifdef Q_OS_IOS @@ -85,6 +86,7 @@ class ftmsbike : public bike { void init(); void forceResistance(resistance_t requestResistance); void forcePower(int16_t requestPower); + void setWheelDiameter(double diameter); uint16_t wattsFromResistance(double resistance); QTimer *refresh; diff --git a/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp b/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp index caa489acc..f15abc55c 100644 --- a/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp +++ b/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp @@ -31,6 +31,8 @@ wahookickrsnapbike::wahookickrsnapbike(bool noWriteResistance, bool noHeartServi initDone = false; connect(refresh, &QTimer::timeout, this, &wahookickrsnapbike::update); refresh->start(200ms); + wheelCircumference::GearTable g; + g.printTable(); } bool wahookickrsnapbike::writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log, @@ -192,10 +194,16 @@ void wahookickrsnapbike::update() { } QThread::msleep(700); + QByteArray d = setWheelCircumference(wheelCircumference::gearsToWheelDiameter(gears())); + uint8_t e[20]; + setGears(1); + memcpy(e, d.constData(), d.length()); + writeCharacteristic(e, d.length(), "setWheelCircumference", false, true); + // required to the SS2K only one time Resistance = 0; emit resistanceRead(Resistance.value()); - initRequest = false; + initRequest = false; } else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState //&& // gattCommunicationChannelService && @@ -259,7 +267,10 @@ void wahookickrsnapbike::update() { memcpy(b, a.constData(), a.length()); writeCharacteristic(b, a.length(), "setResistance", false, true); } else if (virtualBike && virtualBike->ftmsDeviceConnected() && lastGearValue != gears()) { - inclinationChanged(lastGrade, lastGrade); + QByteArray a = setWheelCircumference(wheelCircumference::gearsToWheelDiameter(gears())); + uint8_t b[20]; + memcpy(b, a.constData(), a.length()); + writeCharacteristic(b, a.length(), "setWheelCircumference", false, true); } lastGearValue = gears(); requestResistance = -1; @@ -823,7 +834,6 @@ void wahookickrsnapbike::inclinationChanged(double grade, double percentage) { emit debug(QStringLiteral("writing inclination ") + QString::number(grade)); QSettings settings; double g = grade; - g += gears(); QByteArray a = setSimGrade(g); uint8_t b[20]; memcpy(b, a.constData(), a.length()); @@ -833,3 +843,12 @@ void wahookickrsnapbike::inclinationChanged(double grade, double percentage) { bool wahookickrsnapbike::inclinationAvailableByHardware() { return KICKR_BIKE; } + +double wahookickrsnapbike::maxGears() { + wheelCircumference::GearTable g; + return g.maxGears; +} + +double wahookickrsnapbike::minGears() { + return 1; +} diff --git a/src/devices/wahookickrsnapbike/wahookickrsnapbike.h b/src/devices/wahookickrsnapbike/wahookickrsnapbike.h index 97357ff85..16522fa4c 100644 --- a/src/devices/wahookickrsnapbike/wahookickrsnapbike.h +++ b/src/devices/wahookickrsnapbike/wahookickrsnapbike.h @@ -26,6 +26,7 @@ #include #include +#include "wheelcircumference.h" #include "devices/bike.h" #include "virtualdevices/virtualbike.h" @@ -42,6 +43,8 @@ class wahookickrsnapbike : public bike { bool connected() override; resistance_t maxResistance() override { return 100; } bool inclinationAvailableByHardware() override; + double maxGears() override; + double minGears() override; enum OperationCode : uint8_t { _unlock = 32, @@ -56,7 +59,7 @@ class wahookickrsnapbike : public bike { _setWheelCircumference = 72, }; - private: + private: QByteArray unlockCommand(); QByteArray setResistanceMode(double resistance); QByteArray setStandardMode(uint8_t level); @@ -107,7 +110,7 @@ class wahookickrsnapbike : public bike { volatile int notificationSubscribed = 0; - resistance_t lastForcedResistance = -1; + resistance_t lastForcedResistance = -1; #ifdef Q_OS_IOS lockscreen *h = 0; diff --git a/src/gears.qml b/src/gears.qml new file mode 100644 index 000000000..6f1d6ea0f --- /dev/null +++ b/src/gears.qml @@ -0,0 +1,817 @@ +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.0 +import Qt.labs.settings 1.0 + +ScrollView { + contentWidth: -1 + focus: true + anchors.horizontalCenter: parent.horizontalCenter + anchors.fill: parent + id: gearSettingsWindow + visible: true + clip: true + + // Properties + Settings { + id: settings + property string gear_configuration: "1|38|44|true\n2|38|38|true\n3|38|32|true\n4|38|28|true\n5|38|24|true\n6|38|21|true\n7|38|19|true\n8|38|17|true\n9|38|15|true\n10|38|13|true\n11|38|11|true\n12|38|10|true" + property int gear_crankset_size: 42 + property int gear_cog_size: 14 + property string gear_wheel_size: "700 x 18C" + property real gear_circumference: 2070 + } + + property int selectedCranksetSize: settings.gear_crankset_size + property int selectedCogSize: settings.gear_cog_size + property string selectedWheelSize: settings.gear_wheel_size + property real selectedCircumference: settings.gear_circumference + property bool inited: false + + property int initialWheelSizeIndex: { + // Trova l'indice corretto basato sul valore salvato + for (let i = 0; i < wheelSizes.count; i++) { + if (wheelSizes.get(i).text === settings.gear_wheel_size) { + return i; + } + } + return 0; // default se non trovato + } + + Component.onCompleted: { + if (settings.gear_configuration) { + gearRows = stringToGearRows(settings.gear_configuration) + } + console.log("Component.onCompleted " + settings.gear_crankset_size + " " + settings.gear_cog_size + " " + settings.gear_wheel_size + " " + settings.gear_circumference) + wheelSizeCombo.currentIndex = initialWheelSizeIndex + selectedCranksetSize = settings.gear_crankset_size + selectedCogSize = settings.gear_cog_size + inited = true + } + + function updateSettings() { + if(!inited) + return; + settings.gear_crankset_size = selectedCranksetSize + settings.gear_cog_size = selectedCogSize + settings.gear_wheel_size = selectedWheelSize + settings.gear_circumference = selectedCircumference + } + + function stringToGearRows(gearString) { + if (!gearString) return [] + + return gearString.split("\n").map(function(row) { + const parts = row.split("|") + return { + gear: parseInt(parts[0]), + crankset: parseInt(parts[1]), + cog: parseInt(parts[2]), + active: parts[3] === "true" + } + }) + } + + function gearRowsToString(gearRows) { + return gearRows.map(function(row) { + return row.gear + "|" + row.crankset + "|" + row.cog + "|" + row.active + }).join("\n") + } + + // Monitora i cambiamenti nelle gear e salva automaticamente + onGearConfigurationChanged: { + settings.gear_configuration = gearRowsToString(gearRows) + } + + onSettingsChanged: { + console.log("onSettingsChanged") + updateSettings() + } + + Connections { + target: gearSettingsWindow + function onGearConfigurationChanged() { + gearTable.updateGearListModel() + } + } + + function loadGearProfile(profileName) { + if (profileName in gearProfiles) { + const profile = gearProfiles[profileName] + + // Create new array with copied objects + var newGears = [] + for (var i = 0; i < profile.gears.length; i++) { + newGears.push({ + gear: profile.gears[i].gear, + crankset: profile.gears[i].crankset, + cog: profile.gears[i].cog, + active: profile.gears[i].active + }) + } + + gearRows = newGears + + // Force update + var temp = gearRows + gearRows = [] + gearRows = temp + gearConfigurationChanged(gearRows) + } + } + + property var gearProfiles: { + "Time Trial": { + name: "Time Trial (52/36, 10 - 28)", + gears: [ + { gear: 1, crankset: 36, cog: 28, active: true }, + { gear: 2, crankset: 36, cog: 24, active: true }, + { gear: 3, crankset: 36, cog: 21, active: true }, + { gear: 4, crankset: 36, cog: 19, active: true }, + { gear: 5, crankset: 36, cog: 18, active: true }, + { gear: 6, crankset: 36, cog: 17, active: true }, + { gear: 7, crankset: 36, cog: 16, active: true }, + { gear: 8, crankset: 36, cog: 15, active: true }, + { gear: 9, crankset: 36, cog: 14, active: true }, + { gear: 10, crankset: 52, cog: 19, active: true }, + { gear: 11, crankset: 52, cog: 18, active: true }, + { gear: 12, crankset: 52, cog: 17, active: true }, + { gear: 13, crankset: 52, cog: 16, active: true }, + { gear: 14, crankset: 52, cog: 15, active: true }, + { gear: 15, crankset: 52, cog: 14, active: true }, + { gear: 16, crankset: 52, cog: 13, active: true }, + { gear: 17, crankset: 52, cog: 12, active: true }, + { gear: 18, crankset: 52, cog: 11, active: true }, + { gear: 19, crankset: 52, cog: 10, active: true } + ] + }, + "Rolling Hills": { + name: "Rolling Hills (46/33, 10 - 33)", + gears: [ + { gear: 1, crankset: 33, cog: 33, active: true }, + { gear: 2, crankset: 33, cog: 28, active: true }, + { gear: 3, crankset: 33, cog: 24, active: true }, + { gear: 4, crankset: 33, cog: 21, active: true }, + { gear: 5, crankset: 33, cog: 19, active: true }, + { gear: 6, crankset: 33, cog: 17, active: true }, + { gear: 7, crankset: 33, cog: 15, active: true }, + { gear: 8, crankset: 46, cog: 19, active: true }, + { gear: 9, crankset: 46, cog: 17, active: true }, + { gear: 10, crankset: 46, cog: 15, active: true }, + { gear: 11, crankset: 46, cog: 14, active: true }, + { gear: 12, crankset: 46, cog: 13, active: true }, + { gear: 13, crankset: 46, cog: 12, active: true }, + { gear: 14, crankset: 46, cog: 11, active: true }, + { gear: 15, crankset: 46, cog: 10, active: true } + ] + }, + "Alpine": { + name: "Alpine (43/30, 10 - 36)", + gears: [ + { gear: 1, crankset: 30, cog: 36, active: true }, + { gear: 2, crankset: 30, cog: 32, active: true }, + { gear: 3, crankset: 30, cog: 28, active: true }, + { gear: 4, crankset: 30, cog: 24, active: true }, + { gear: 5, crankset: 30, cog: 21, active: true }, + { gear: 6, crankset: 30, cog: 19, active: true }, + { gear: 7, crankset: 30, cog: 17, active: true }, + { gear: 8, crankset: 30, cog: 15, active: true }, + { gear: 9, crankset: 43, cog: 19, active: true }, + { gear: 10, crankset: 43, cog: 17, active: true }, + { gear: 11, crankset: 43, cog: 15, active: true }, + { gear: 12, crankset: 43, cog: 13, active: true }, + { gear: 13, crankset: 43, cog: 12, active: true }, + { gear: 14, crankset: 43, cog: 11, active: true }, + { gear: 15, crankset: 43, cog: 10, active: true } + ] + }, + "Reality Bender": { + name: "Reality Bender (24 even spaced)", + gears: [ + { gear: 1, crankset: 30, cog: 40, active: true }, + { gear: 2, crankset: 30, cog: 36, active: true }, + { gear: 3, crankset: 30, cog: 33, active: true }, + { gear: 4, crankset: 30, cog: 30, active: true }, + { gear: 5, crankset: 30, cog: 27, active: true }, + { gear: 6, crankset: 34, cog: 28, active: true }, + { gear: 7, crankset: 34, cog: 26, active: true }, + { gear: 8, crankset: 34, cog: 24, active: true }, + { gear: 9, crankset: 34, cog: 22, active: true }, + { gear: 10, crankset: 44, cog: 26, active: true }, + { gear: 11, crankset: 44, cog: 24, active: true }, + { gear: 12, crankset: 44, cog: 22, active: true }, + { gear: 13, crankset: 44, cog: 20, active: true }, + { gear: 14, crankset: 44, cog: 18, active: true }, + { gear: 15, crankset: 56, cog: 21, active: true }, + { gear: 16, crankset: 56, cog: 19, active: true }, + { gear: 17, crankset: 58, cog: 18, active: true }, + { gear: 18, crankset: 60, cog: 17, active: true }, + { gear: 19, crankset: 62, cog: 16, active: true }, + { gear: 20, crankset: 63, cog: 15, active: true }, + { gear: 21, crankset: 64, cog: 14, active: true }, + { gear: 22, crankset: 66, cog: 13, active: true }, + { gear: 23, crankset: 67, cog: 12, active: true } + ] + }, + "Explorer": { + name: "Explorer (40, 10 - 46)", + gears: [ + { gear: 1, crankset: 40, cog: 46, active: true }, + { gear: 2, crankset: 40, cog: 38, active: true }, + { gear: 3, crankset: 40, cog: 32, active: true }, + { gear: 4, crankset: 40, cog: 28, active: true }, + { gear: 5, crankset: 40, cog: 24, active: true }, + { gear: 6, crankset: 40, cog: 21, active: true }, + { gear: 7, crankset: 40, cog: 19, active: true }, + { gear: 8, crankset: 40, cog: 17, active: true }, + { gear: 9, crankset: 40, cog: 15, active: true }, + { gear: 10, crankset: 40, cog: 13, active: true }, + { gear: 11, crankset: 40, cog: 12, active: true }, + { gear: 12, crankset: 40, cog: 11, active: true }, + { gear: 13, crankset: 40, cog: 10, active: true } + ] + } + } + + // Initial gear data + property var gearRows: [ + { gear: 1, crankset: 38, cog: 44, active: true }, + { gear: 2, crankset: 38, cog: 38, active: true }, + { gear: 3, crankset: 38, cog: 32, active: true }, + { gear: 4, crankset: 38, cog: 28, active: true }, + { gear: 5, crankset: 38, cog: 24, active: true }, + { gear: 6, crankset: 38, cog: 21, active: true }, + { gear: 7, crankset: 38, cog: 19, active: true }, + { gear: 8, crankset: 38, cog: 17, active: true }, + { gear: 9, crankset: 38, cog: 15, active: true }, + { gear: 10, crankset: 38, cog: 13, active: true }, + { gear: 11, crankset: 38, cog: 11, active: true }, + { gear: 12, crankset: 38, cog: 10, active: true } + ] + + function addNewGear() { + // Find the first inactive gear or add at the end + let newGearIndex = gearRows.findIndex(row => !row.active); + if (newGearIndex === -1) { + newGearIndex = gearRows.length; + } + + // Create new gear with default values + const newGear = { + gear: newGearIndex + 1, + crankset: selectedCranksetSize, + cog: selectedCogSize, + active: true + }; + + if (newGearIndex < gearRows.length) { + gearRows[newGearIndex] = newGear; + } else { + gearRows.push(newGear); + } + + // Force update + var temp = gearRows; + gearRows = []; + gearRows = temp; + gearConfigurationChanged(gearRows); + } + + function clearGearsFromIndex(startIndex) { + for (let i = startIndex; i < gearRows.length; i++) { + gearRows[i].active = false + } + // Force update + var temp = gearRows + gearRows = [] + gearRows = temp + gearConfigurationChanged(gearRows) + } + + function initializeGearRows() { + gearRows = [ + { gear: 1, crankset: 38, cog: 44, active: true }, + { gear: 2, crankset: 38, cog: 38, active: true }, + { gear: 3, crankset: 38, cog: 32, active: true }, + { gear: 4, crankset: 38, cog: 28, active: true }, + { gear: 5, crankset: 38, cog: 24, active: true }, + { gear: 6, crankset: 38, cog: 21, active: true }, + { gear: 7, crankset: 38, cog: 19, active: true }, + { gear: 8, crankset: 38, cog: 17, active: true }, + { gear: 9, crankset: 38, cog: 15, active: true }, + { gear: 10, crankset: 38, cog: 13, active: true }, + { gear: 11, crankset: 38, cog: 11, active: true }, + { gear: 12, crankset: 38, cog: 10, active: true } + ] + // Force update + var temp = gearRows + gearRows = [] + gearRows = temp + } + + // Signals to notify when values change + signal settingsChanged() + signal gearConfigurationChanged(var gearRows) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 20 + id: chainringColumn + + // Crankset Size + GroupBox { + title: "Chainring Size" + Layout.fillWidth: true + + ColumnLayout { + Label { + text: "Tooth count of your chainring on the bike you are currently riding on your trainer - enter 42 for Zwift Ride" + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.maximumWidth: chainringColumn.width - 20 + } + + SpinBox { + from: 1 + to: 60 + value: selectedCranksetSize + onValueChanged: { + selectedCranksetSize = value + console.log("Crankset Size changed"); + settingsChanged() + } + } + } + } + + // Cog Size + GroupBox { + title: "Cog Size" + Layout.fillWidth: true + + ColumnLayout { + Label { + text: "Tooth count of your rear cog on your trainer - enter 14 if you have the Zwift Cog" + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.maximumWidth: chainringColumn.width - 20 + } + SpinBox { + from: 1 + to: 50 + value: selectedCogSize + onValueChanged: { + selectedCogSize = value + console.log("Cog Size changed"); + settingsChanged() + } + } + } + } + + // Wheel Size + GroupBox { + title: "Virtual Wheel Size" + Layout.fillWidth: true + + ComboBox { + id: wheelSizeCombo + width: parent.width + currentIndex: initialWheelSizeIndex + textRole: "text" + model: ListModel { + id: wheelSizes + ListElement { text: "700 x 18C"; circumference: 2070 } + ListElement { text: "700 x 19C"; circumference: 2080 } + ListElement { text: "700 x 20C"; circumference: 2086 } + ListElement { text: "700 x 23C"; circumference: 2096 } + ListElement { text: "700 x 25C"; circumference: 2109 } + ListElement { text: "700 x 28C"; circumference: 2127 } + ListElement { text: "700 x 30C"; circumference: 2140 } + ListElement { text: "700 x 32C"; circumference: 2152 } + ListElement { text: "700 x 35C"; circumference: 2171 } + ListElement { text: "700 x 38C"; circumference: 2190 } + ListElement { text: "700 x 40C"; circumference: 2203 } + ListElement { text: "700 x 44C"; circumference: 2230 } + ListElement { text: "700 x 45C"; circumference: 2234 } + ListElement { text: "700 x 47C"; circumference: 2247 } + ListElement { text: "700 x 50C"; circumference: 2265 } + ListElement { text: "650 x 20C"; circumference: 1938 } + ListElement { text: "650 x 23C"; circumference: 1944 } + ListElement { text: "650 x 35A"; circumference: 2090 } + ListElement { text: "650 x 38B"; circumference: 2105 } + ListElement { text: "650 x 38A"; circumference: 2125 } + ListElement { text: "12\" x 1.75\""; circumference: 935 } + ListElement { text: "12\" x 1.95\""; circumference: 940 } + ListElement { text: "14\" x 1.50\""; circumference: 1020 } + ListElement { text: "14\" x 1.75\""; circumference: 1055 } + ListElement { text: "16\" x 1.50\""; circumference: 1185 } + ListElement { text: "16\" x 1.75\""; circumference: 1195 } + ListElement { text: "16\" x 2.00\""; circumference: 1245 } + ListElement { text: "16\" x 1-1/8\""; circumference: 1290 } + ListElement { text: "16\" x 1-3/8\""; circumference: 1300 } + ListElement { text: "18\" x 1.50\""; circumference: 1340 } + ListElement { text: "18\" x 1.75\""; circumference: 1350 } + ListElement { text: "20\" x 1.25\""; circumference: 1450 } + ListElement { text: "20\" x 1.35\""; circumference: 1460 } + ListElement { text: "20\" x 1.50\""; circumference: 1490 } + ListElement { text: "20\" x 1.75\""; circumference: 1515 } + ListElement { text: "20\" x 1.95\""; circumference: 1565 } + ListElement { text: "20\" x 1-1/8\""; circumference: 1545 } + ListElement { text: "20\" x 1-3/8\""; circumference: 1615 } + ListElement { text: "22\" x 1-3/8\""; circumference: 1770 } + ListElement { text: "22\" x 1-1/2\""; circumference: 1785 } + ListElement { text: "24\" x 3/4\" Tubular"; circumference: 1785 } + ListElement { text: "24\" x 1\""; circumference: 1753 } + ListElement { text: "24\" x 1-1/8\""; circumference: 1795 } + ListElement { text: "24\" x 1-1/4\""; circumference: 1905 } + ListElement { text: "24\" x 1.75\""; circumference: 1890 } + ListElement { text: "24\" x 2.00\""; circumference: 1925 } + ListElement { text: "24\" x 2.125\""; circumference: 1965 } + ListElement { text: "26\" x 7/8\" Tubular"; circumference: 1920 } + ListElement { text: "26\" x 1.25\""; circumference: 1950 } + ListElement { text: "26\" x 1.40\""; circumference: 2005 } + ListElement { text: "26\" x 1.50\""; circumference: 2010 } + ListElement { text: "26\" x 1.75\""; circumference: 2023 } + ListElement { text: "26\" x 1.95\""; circumference: 2050 } + ListElement { text: "26\" x 2.00\""; circumference: 2055 } + ListElement { text: "26\" x 2.10\""; circumference: 2068 } + ListElement { text: "26\" x 2.125\""; circumference: 2070 } + ListElement { text: "26\" x 2.35\""; circumference: 2083 } + ListElement { text: "26\" x 3.00\""; circumference: 2170 } + ListElement { text: "26\" x 1-1.0\""; circumference: 1913 } + ListElement { text: "26\" x 1\""; circumference: 1952 } + ListElement { text: "26\" x 1-1/8\""; circumference: 1970 } + ListElement { text: "26\" x 1-3/8\""; circumference: 2068 } + ListElement { text: "26\" x 1-1/2\""; circumference: 2100 } + ListElement { text: "27\" x 1\""; circumference: 2145 } + ListElement { text: "27\" x 1-1/8\""; circumference: 2155 } + ListElement { text: "27\" x 1-1/4\""; circumference: 2161 } + ListElement { text: "27\" x 1-3/8\""; circumference: 2169 } + ListElement { text: "27.5\" / 650B x 1.50\""; circumference: 2079 } + ListElement { text: "27.5\" / 650B x 1.95\""; circumference: 2090 } + ListElement { text: "27.5\" / 650B x 2.10\""; circumference: 2148 } + ListElement { text: "27.5\" / 650B x 2.25\""; circumference: 2182 } + ListElement { text: "27.5\" / 650B x 2.3\""; circumference: 2199 } + ListElement { text: "27.5\" / 650B x 2.35\""; circumference: 2207 } + ListElement { text: "27.5\" / 650B x 2.4\""; circumference: 2213 } + ListElement { text: "27.5\" / 650B x 2.5\""; circumference: 2231 } + ListElement { text: "27.5\" / 650B x 2.6\""; circumference: 2247 } + ListElement { text: "27.5\" / 650B x 2.8\""; circumference: 2279 } + ListElement { text: "29\" x 2.1\""; circumference: 2286 } + ListElement { text: "29\" x 2.2\""; circumference: 2302 } + ListElement { text: "29\" x 2.25\""; circumference: 2310 } + ListElement { text: "29\" x 2.3\""; circumference: 2326 } + ListElement { text: "29\" x 2.35\""; circumference: 2326 } + ListElement { text: "29\" x 2.4\""; circumference: 2333 } + ListElement { text: "29\" x 2.5\""; circumference: 2350 } + ListElement { text: "29\" x 2.6\""; circumference: 2366 } + } + onCurrentIndexChanged: { + if (currentIndex >= 0) { + selectedWheelSize = model.get(currentIndex).text + selectedCircumference = model.get(currentIndex).circumference + console.log("wheelSizeCombo changed"); + settingsChanged() + } + } + } + } + + GroupBox { + title: "Preset Gear Profiles" + Layout.fillWidth: true + + ComboBox { + id: profileCombo + width: parent.width + textRole: "text" + displayText: currentIndex < 0 ? "Select a profile..." : model.get(currentIndex).text + model: ListModel { + id: profileModel + } + + Component.onCompleted: { + for (var key in gearProfiles) { + profileModel.append({ + text: gearProfiles[key].name, + value: key + }) + } + } + + onCurrentIndexChanged: { + if (currentIndex >= 0) { + loadGearProfile(profileModel.get(currentIndex).value) + } + } + } + } + + + // Gear Table GroupBox + GroupBox { + title: "Virtual Gear Table" + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + // Updated Buttons Row + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 40 + spacing: 10 + + Button { + text: "Add Gear" + Layout.fillWidth: true + Layout.preferredHeight: 40 + onClicked: addNewGear() + } + + Button { + text: "Clear Selected Gear and Following" + Layout.fillWidth: true + Layout.preferredHeight: 40 + onClicked: { + if (gearTable.currentRow >= 0) { + clearGearsFromIndex(gearTable.currentRow) + } + } + } + + Button { + text: "Reset All Gears" + Layout.fillWidth: true + Layout.preferredHeight: 40 + onClicked: initializeGearRows() + } + } + + // Table Header (same as before) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + color: "#f0f0f0" + border.width: 1 + border.color: "#cccccc" + + Row { + anchors.fill: parent + + Rectangle { + width: parent.width / 3 + height: parent.height + border.width: 1 + border.color: "#cccccc" + color: "transparent" + + Text { + anchors.centerIn: parent + text: "Gear" + font.bold: true + color: "black" + } + } + + Rectangle { + width: parent.width / 3 + height: parent.height + border.width: 1 + border.color: "#cccccc" + color: "transparent" + + Text { + anchors.centerIn: parent + text: "Chainring" + font.bold: true + color: "black" + } + } + + Rectangle { + width: parent.width / 3 + height: parent.height + border.width: 1 + border.color: "#cccccc" + color: "transparent" + + Text { + anchors.centerIn: parent + text: "Rear Cog" + font.bold: true + color: "black" + } + } + } + } + + // Table Content + ListView { + id: gearTable + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + property int currentRow: -1 + model: ListModel { + id: gearListModel + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOff + } + + + Component.onCompleted: { + updateGearListModel() + } + + function updateGearListModel() { + gearListModel.clear() + for (var i = 0; i < gearRows.length; i++) { + if (gearRows[i].active) { + gearListModel.append(gearRows[i]) + } + } + } + + delegate: Rectangle { + width: gearTable.width + height: 40 + color: gearTable.currentRow === index ? "#e0e0e0" : "white" + + MouseArea { + anchors.fill: parent + onClicked: gearTable.currentRow = index + } + + Row { + anchors.fill: parent + + // Gear Number (non-editable) + Rectangle { + width: parent.width / 3 + height: parent.height + border.width: 1 + border.color: "#cccccc" + color: "transparent" + + Text { + anchors.centerIn: parent + text: gear + color: "black" + } + } + + // Crankset (editable) + Rectangle { + width: parent.width / 3 + height: parent.height + border.width: 1 + border.color: "#cccccc" + color: "transparent" + + SpinBox { + id: cranksetSpinBox + anchors.centerIn: parent + width: parent.width * 0.8 + height: 30 + from: 1 + to: 60 + value: crankset + onValueModified: { + gearRows[index].crankset = value + gearConfigurationChanged(gearRows) + } + + // Style the SpinBox + contentItem: TextInput { + z: 2 + text: cranksetSpinBox.textFromValue(cranksetSpinBox.value, cranksetSpinBox.locale) + font: cranksetSpinBox.font + color: "black" + selectionColor: "#21be2b" + selectedTextColor: "#ffffff" + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + } + + up.indicator: Rectangle { + x: parent.width - width + height: parent.height + width: height + color: parent.up.pressed ? "#e4e4e4" : "#f6f6f6" + border.color: "#cccccc" + + Text { + text: "+" + color: "black" + anchors.centerIn: parent + font.pixelSize: 12 + } + } + + down.indicator: Rectangle { + x: 0 + height: parent.height + width: height + color: parent.down.pressed ? "#e4e4e4" : "#f6f6f6" + border.color: "#cccccc" + + Text { + text: "-" + color: "black" + anchors.centerIn: parent + font.pixelSize: 12 + } + } + + background: Rectangle { + color: "white" + border.color: "#cccccc" + } + } + } + + // Rear Cog (editable) + Rectangle { + width: parent.width / 3 + height: parent.height + border.width: 1 + border.color: "#cccccc" + color: "transparent" + + SpinBox { + id: cogSpinBox + anchors.centerIn: parent + width: parent.width * 0.8 + height: 30 + from: 1 + to: 50 + value: cog + onValueModified: { + gearRows[index].cog = value + gearConfigurationChanged(gearRows) + } + + // Style the SpinBox (same as cranksetSpinBox) + contentItem: TextInput { + z: 2 + text: cogSpinBox.textFromValue(cogSpinBox.value, cogSpinBox.locale) + font: cogSpinBox.font + color: "black" + selectionColor: "#21be2b" + selectedTextColor: "#ffffff" + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + } + + up.indicator: Rectangle { + x: parent.width - width + height: parent.height + width: height + color: parent.up.pressed ? "#e4e4e4" : "#f6f6f6" + border.color: "#cccccc" + + Text { + text: "+" + color: "black" + anchors.centerIn: parent + font.pixelSize: 12 + } + } + + down.indicator: Rectangle { + x: 0 + height: parent.height + width: height + color: parent.down.pressed ? "#e4e4e4" : "#f6f6f6" + border.color: "#cccccc" + + Text { + text: "-" + color: "black" + anchors.centerIn: parent + font.pixelSize: 12 + } + } + + background: Rectangle { + color: "white" + border.color: "#cccccc" + } + } + } + } + } + } + } + } + } +} diff --git a/src/qdomyos-zwift.pri b/src/qdomyos-zwift.pri index fa8d97c41..47108ef8a 100644 --- a/src/qdomyos-zwift.pri +++ b/src/qdomyos-zwift.pri @@ -313,6 +313,7 @@ HEADERS += \ $$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.h \ $$PWD/ergtable.h \ $$PWD/treadmillErgTable.h \ + $$PWD/wheelcircumference.h \ QTelnet.h \ devices/bkoolbike/bkoolbike.h \ devices/csaferower/csafe.h \ diff --git a/src/qml.qrc b/src/qml.qrc index 75042363d..43f81b742 100644 --- a/src/qml.qrc +++ b/src/qml.qrc @@ -109,5 +109,6 @@ ChartFooterInnerNoJS.qml inner_templates/chartjs/dochartliveheart.js Wizard.qml + gears.qml diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index ac0433131..f4b33957e 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -769,8 +769,15 @@ const QString QZSettings::default_peloton_date_format = QStringLiteral("MM/dd/yy const QString QZSettings::force_resistance_instead_inclination = QStringLiteral("force_resistance_instead_inclination"); const QString QZSettings::proform_treadmill_575i = QStringLiteral("proform_treadmill_575i"); const QString QZSettings::zwift_play_emulator = QStringLiteral("zwift_play_emulator"); +const QString QZSettings::gear_configuration = QStringLiteral("gear_configuration"); +const QString QZSettings::default_gear_configuration = QStringLiteral("1|38|44|true\n2|38|38|true\n3|38|32|true\n4|38|28|true\n5|38|24|true\n6|38|21|true\n7|38|19|true\n8|38|17|true\n9|38|15|true\n10|38|13|true\n11|38|11|true\n12|38|10|true"); +const QString QZSettings::gear_crankset_size = QStringLiteral("gear_crankset_size"); +const QString QZSettings::gear_cog_size = QStringLiteral("gear_cog_size"); +const QString QZSettings::gear_wheel_size = QStringLiteral("gear_wheel_size"); +const QString QZSettings::default_gear_wheel_size = QStringLiteral("700 x 18C"); +const QString QZSettings::gear_circumference = QStringLiteral("gear_circumference"); -const uint32_t allSettingsCount = 651; +const uint32_t allSettingsCount = 656; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1428,6 +1435,11 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination}, {QZSettings::proform_treadmill_575i, QZSettings::default_proform_treadmill_575i}, {QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator}, + {QZSettings::gear_configuration, QZSettings::default_gear_configuration}, + {QZSettings::gear_crankset_size, QZSettings::default_gear_crankset_size}, + {QZSettings::gear_cog_size, QZSettings::default_gear_cog_size}, + {QZSettings::gear_wheel_size, QZSettings::default_gear_wheel_size}, + {QZSettings::gear_circumference, QZSettings::default_gear_circumference}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index 8fdb021ed..eb2ebf1b1 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -2153,6 +2153,21 @@ class QZSettings { static const QString zwift_play_emulator; static constexpr bool default_zwift_play_emulator = false; + static const QString gear_configuration; + static const QString default_gear_configuration; + + static const QString gear_crankset_size; + static constexpr int default_gear_crankset_size = 42; + + static const QString gear_cog_size; + static constexpr int default_gear_cog_size = 14; + + static const QString gear_wheel_size; + static const QString default_gear_wheel_size; + + static const QString gear_circumference; + static constexpr double default_gear_circumference = 2070.0; + /** * @brief Write the QSettings values using the constants from this namespace. * @param showDefaults Optionally indicates if the default should be shown with the key. diff --git a/src/settings-treadmill-inclination-override.qml b/src/settings-treadmill-inclination-override.qml index fc2d75938..3072d3460 100644 --- a/src/settings-treadmill-inclination-override.qml +++ b/src/settings-treadmill-inclination-override.qml @@ -11,7 +11,7 @@ ScrollView { anchors.fill: parent //anchors.bottom: footerSettings.top //anchors.bottomMargin: footerSettings.height + 10 - id: settingsTTLPane + id: settingsInclinationPane Settings { id: settings diff --git a/src/settings.qml b/src/settings.qml index 0b10a071d..6a872938e 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -987,6 +987,13 @@ import QtQuick.Dialogs 1.0 // from version 2.18.1 property bool zwift_play_emulator: false + + // from version 2.18.2 + property string gear_configuration: "1|38|44|true\n2|38|38|true\n3|38|32|true\n4|38|28|true\n5|38|24|true\n6|38|21|true\n7|38|19|true\n8|38|17|true\n9|38|15|true\n10|38|13|true\n11|38|11|true\n12|38|10|true" + property int gear_crankset_size: 42 + property int gear_cog_size: 14 + property string gear_wheel_size: "700 x 18C" + property real gear_circumference: 2070 } function paddingZeros(text, limit) { @@ -2493,6 +2500,14 @@ import QtQuick.Dialogs 1.0 color: Material.color(Material.Lime) } + NewPageElement { + title: qsTr("Wahoo Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: "gears.qml" + } + AccordionElement { id: schwinnBikeAccordion title: qsTr("Schwinn Bike Options") @@ -9800,7 +9815,7 @@ import QtQuick.Dialogs 1.0 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true color: Material.color(Material.Lime) - } + } } } diff --git a/src/wheelcircumference.h b/src/wheelcircumference.h new file mode 100644 index 000000000..a43c4d4e0 --- /dev/null +++ b/src/wheelcircumference.h @@ -0,0 +1,120 @@ +#ifndef WHEELCIRCUMFERENCE_H +#define WHEELCIRCUMFERENCE_H + +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "qzsettings.h" + +class wheelCircumference : public QObject { + + Q_OBJECT + + public: + static double gearsToWheelDiameter(double gear) { + QSettings settings; + GearTable table; + if(gear < 1) gear = 1; + else if(gear > table.maxGears) gear = table.maxGears; + double original_ratio = ((double)settings.value(QZSettings::gear_crankset_size, QZSettings::default_gear_crankset_size).toDouble()) / ((double)settings.value(QZSettings::gear_cog_size, QZSettings::default_gear_cog_size).toDouble()); + GearTable::GearInfo g = table.getGear((int)gear); + double current_ratio = ((double)g.crankset / (double)g.rearCog); + return (((double)settings.value(QZSettings::gear_circumference, QZSettings::default_gear_circumference).toDouble()) / original_ratio) * ((double)current_ratio); + } + + + class GearTable { + public: + + int maxGears = 12; + + struct GearInfo { + int gear; + int crankset; + int rearCog; + }; + + void loadGearSettings() { + QSettings settings; + + QString gearConfig = settings.value("gear_configuration").toString(); + if (gearConfig.isEmpty()) { + + gearConfig = "1|38|44|true\n2|38|38|true\n3|38|32|true\n4|38|28|true\n" + "5|38|24|true\n6|38|21|true\n7|38|19|true\n8|38|17|true\n" + "9|38|15|true\n10|38|13|true\n11|38|11|true\n12|38|10|true"; + } + + gears.clear(); + maxGears = 0; + + // Parsa la configurazione + QStringList rows = gearConfig.split('\n'); + for (const QString& row : rows) { + QStringList parts = row.split('|'); + if (parts.size() >= 4 && (parts[3] == "true")) { + GearInfo config; + config.gear = parts[0].toInt(); + config.crankset = parts[1].toInt(); + config.rearCog = parts[2].toInt(); + + gears.push_back(config); + maxGears = qMax(maxGears, config.gear); + } + } + } + + void addGear(int gear, int crankset, int rearCog) { + gears.push_back({gear, crankset, rearCog}); + } + + void removeGear(int gear) { + gears.erase(std::remove_if(gears.begin(), gears.end(), + [gear](const GearInfo& info) { return info.gear == gear; }), + gears.end()); + } + + void printTable() const { + qDebug() << "| Gear | Crankset | Rear Cog |\n"; + qDebug() << "|------|----------|----------|\n"; + for (const auto& gear : gears) { + qDebug() << "| " << gear.gear << " | " << gear.crankset + << " | " << gear.rearCog << " |\n"; + } + } + + GearInfo getGear(int gearNumber) const { + auto it = std::find_if(gears.begin(), gears.end(), + [gearNumber](const GearInfo& info) { return info.gear == gearNumber; }); + + if (it != gears.end()) { + return *it; + } + return GearInfo(); + } + + GearTable() { + loadGearSettings(); + } + + private: + std::vector gears; + }; +}; + +#endif // WHEELCIRCUMFERENCE_H