From d5ba21da2ec3bb1bcdce8aa3e096ae246ad8ceea Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 14 Dec 2023 19:43:35 +0100 Subject: [PATCH 01/32] Octane Fitness 37xi connection to QZ app #1876 --- src/octaneelliptical.cpp | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index 8943a4145..937f9e8b4 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -170,6 +170,7 @@ octaneelliptical::octaneelliptical(uint32_t pollDeviceTime, bool noConsole, bool actualPace2Sign.clear(); // SPEED + actualPaceSign.append(0x01); actualPaceSign.append(0x07); actualPace2Sign.append(0x07); @@ -355,29 +356,6 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha } } - if (newValue.contains(actualOdometer)) { - int16_t i = newValue.indexOf(actualOdometer) + 1; - - if (i + 2 < newValue.length() && i % 2 == 0) { - - int d = ((uint16_t)value.at(i)) + ((((uint16_t)value.at(i + 1)) << 8) & 0xFF00); - if(d > distance) { - int oldDistance = distance; - distance = d; - QDateTime oldTime = lastTimeDistance; - lastTimeDistance = QDateTime::currentDateTime(); - double distanceDeltaKm = ((double)(distance - oldDistance) / 100) * 1.60934; - double timeDeltaHours = oldTime.msecsTo(lastTimeDistance) / 1000.0 / 3600.0; - speed = distanceDeltaKm / timeDeltaHours; - Speed = speed.average5s(); - emit speedChanged(speed.value()); - Distance = distance * 1.60934; - emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); - emit debug(QStringLiteral("Current Distance from The Machinery: ") + QString::number(distance)); - } - } - } - if (newValue.contains(actualHR)) { bool disable_hr_frommachinery = settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); @@ -406,15 +384,20 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha if (!newValue.contains(actualPaceSign) && !newValue.contains(actualPace2Sign)) return; - int16_t i = newValue.indexOf(actualPaceSign) + 1; - if (i <= 1) - i = newValue.indexOf(actualPace2Sign) + 1; + int16_t i = newValue.indexOf(actualPaceSign) + 2; + /*if (i <= 1) + i = newValue.indexOf(actualPace2Sign) + 1;*/ if (i + 1 >= newValue.length()) return; Cadence = ((uint8_t)value.at(i)); + // Q37xi has a fixed stride length of 20.5 inches (52cm). + Speed = (Cadence.value() * 52.0 * 60) / 10000; + emit speedChanged(speed.value()); + emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); + if (!firstCharacteristicChanged) { if (watts()) KCal += @@ -424,6 +407,8 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha (60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in // kg * 3.5) / 200 ) / 60 + Distance += ((Speed.value() / 3600000.0) * + ((double)lastTimeCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); lastTimeCharacteristicChanged = QDateTime::currentDateTime(); } From 80429895529589db0f87fa8903bdfed94d018403 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Fri, 15 Dec 2023 09:29:43 +0100 Subject: [PATCH 02/32] Octane Fitness 37xi connection to QZ app #1876 --- src/octaneelliptical.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index 937f9e8b4..2ff1cf212 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -381,17 +381,18 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } - if (!newValue.contains(actualPaceSign) && !newValue.contains(actualPace2Sign)) + if (!newValue.contains(actualPaceSign) /*&& !newValue.contains(actualPace2Sign)*/) return; int16_t i = newValue.indexOf(actualPaceSign) + 2; /*if (i <= 1) i = newValue.indexOf(actualPace2Sign) + 1;*/ - if (i + 1 >= newValue.length()) + if (i + 1 >= newValue.length() || i <= 1) return; Cadence = ((uint8_t)value.at(i)); + emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); // Q37xi has a fixed stride length of 20.5 inches (52cm). Speed = (Cadence.value() * 52.0 * 60) / 10000; From e0144445a32f5c81ac05133306824229830a4fee Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Sat, 16 Dec 2023 20:22:07 +0100 Subject: [PATCH 03/32] adding distance to virtual treadmill ftms to accomodate the new FTMS peloton function --- .../qdomyoszwift.xcodeproj/project.pbxproj | 12 ++++++------ src/characteristicnotifier2acd.cpp | 15 ++++++++++++++- src/ios/lockscreen.h | 2 +- src/ios/lockscreen.mm | 4 ++-- src/ios/virtualtreadmill_zwift.swift | 9 ++++++--- src/virtualtreadmill.cpp | 2 +- 6 files changed, 30 insertions(+), 14 deletions(-) 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 e404c5b19..62e836d46 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 @@ -3700,7 +3700,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 681; + CURRENT_PROJECT_VERSION = 682; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -3870,7 +3870,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 681; + CURRENT_PROJECT_VERSION = 682; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; @@ -4076,7 +4076,7 @@ CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 681; + CURRENT_PROJECT_VERSION = 682; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -4172,7 +4172,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 681; + CURRENT_PROJECT_VERSION = 682; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; @@ -4264,7 +4264,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 681; + CURRENT_PROJECT_VERSION = 682; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; ENABLE_PREVIEWS = YES; @@ -4378,7 +4378,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 681; + CURRENT_PROJECT_VERSION = 682; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; diff --git a/src/characteristicnotifier2acd.cpp b/src/characteristicnotifier2acd.cpp index f3849a78c..2a2b0ba95 100644 --- a/src/characteristicnotifier2acd.cpp +++ b/src/characteristicnotifier2acd.cpp @@ -8,7 +8,7 @@ CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QO int CharacteristicNotifier2ACD::notify(QByteArray &value) { bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType(); if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) { - value.append(0x08); // Inclination available + value.append(0x0C); // Inclination available and distance for peloton value.append((char)0x01); // heart rate available uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100); @@ -17,6 +17,17 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) { QByteArray speedBytes; speedBytes.append(b); speedBytes.append(a); + + uint16_t normalizeDistance = (uint16_t)qRound(Bike->odometer() * 1000); + a = (normalizeDistance >> 16) & 0XFF; + b = (normalizeDistance >> 8) & 0XFF; + char c = normalizeDistance & 0XFF; + QByteArray distanceBytes; + distanceBytes.append(c); + distanceBytes.append(b); + distanceBytes.append(a); + + uint16_t normalizeIncline = 0; if (dt == bluetoothdevice::TREADMILL) normalizeIncline = (uint32_t)qRound(((treadmill *)Bike)->currentInclination().value() * 10); @@ -36,6 +47,8 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) { rampBytes.append(a); value.append(speedBytes); // Actual value. + + value.append(distanceBytes); // Actual value. value.append(inclineBytes); // incline diff --git a/src/ios/lockscreen.h b/src/ios/lockscreen.h index caf998f1f..65851fb90 100644 --- a/src/ios/lockscreen.h +++ b/src/ios/lockscreen.h @@ -46,7 +46,7 @@ class lockscreen { double virtualtreadmill_getPowerRequested(); bool virtualtreadmill_updateFTMS(unsigned short normalizeSpeed, unsigned char currentResistance, unsigned short currentCadence, unsigned short currentWatt, - unsigned short currentInclination); + unsigned short currentInclination, unsigned long long currentDistance); // volume double getVolume(); diff --git a/src/ios/lockscreen.mm b/src/ios/lockscreen.mm index 1df56bef4..0e4489e0f 100644 --- a/src/ios/lockscreen.mm +++ b/src/ios/lockscreen.mm @@ -205,10 +205,10 @@ return 0; } -bool lockscreen::virtualtreadmill_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 currentInclination) +bool lockscreen::virtualtreadmill_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 currentInclination, UInt64 currentDistance) { if(_virtualtreadmill_zwift != nil) - return [_virtualtreadmill_zwift updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt currentInclination:currentInclination]; + return [_virtualtreadmill_zwift updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt currentInclination:currentInclination currentDistance:currentDistance]; return 0; } diff --git a/src/ios/virtualtreadmill_zwift.swift b/src/ios/virtualtreadmill_zwift.swift index c0a05068e..9a341d299 100644 --- a/src/ios/virtualtreadmill_zwift.swift +++ b/src/ios/virtualtreadmill_zwift.swift @@ -36,13 +36,14 @@ let treadmilldataUuid = CBUUID(string: "0x2ACD"); return peripheralManager.PowerRequested; } - @objc public func updateFTMS(normalizeSpeed: UInt16, currentCadence: UInt16, currentResistance: UInt8, currentWatt: UInt16, currentInclination: UInt16) -> Bool + @objc public func updateFTMS(normalizeSpeed: UInt16, currentCadence: UInt16, currentResistance: UInt8, currentWatt: UInt16, currentInclination: UInt16, currentDistance: UInt64) -> Bool { peripheralManager.NormalizeSpeed = normalizeSpeed peripheralManager.CurrentCadence = currentCadence peripheralManager.CurrentResistance = currentResistance peripheralManager.CurrentWatt = currentWatt peripheralManager.CurrentInclination = currentInclination + peripheralManager.CurrentDistance = currentDistance return peripheralManager.connected; } @@ -70,6 +71,7 @@ class BLEPeripheralManagerTreadmillZwift: NSObject, CBPeripheralManagerDelegate public var CurrentResistance: UInt8! = 0 public var CurrentWatt: UInt16! = 0 public var CurrentInclination: UInt16! = 0 + public var CurrentDistance: UInt64! = 0 public var lastCurrentSlope: UInt64! = 0; public var serviceToggle: UInt8 = 0 @@ -314,14 +316,15 @@ class BLEPeripheralManagerTreadmillZwift: NSObject, CBPeripheralManagerDelegate } func calculateTreadmillData() -> Data { - let flags0:UInt8 = 0x08 + let flags0:UInt8 = 0x0C let flags1:UInt8 = 0x01 var treadmillData: [UInt8] = [flags0, flags1, (UInt8)(self.NormalizeSpeed & 0xFF), (UInt8)((self.NormalizeSpeed >> 8) & 0xFF), + (UInt8)(self.CurrentDistance & 0xFF), (UInt8)((self.CurrentDistance >> 8) & 0xFF), (UInt8)((self.CurrentDistance >> 16) & 0xFF), (UInt8)(self.CurrentInclination & 0xFF), (UInt8)((self.CurrentInclination >> 8) & 0xFF), (UInt8)(self.CurrentInclination & 0xFF), (UInt8)((self.CurrentInclination >> 8) & 0xFF), self.heartRate] - let treadmillDataData = Data(bytes: &treadmillData, count: 10) + let treadmillDataData = Data(bytes: &treadmillData, count: 13) return treadmillDataData } diff --git a/src/virtualtreadmill.cpp b/src/virtualtreadmill.cpp index b9c4de675..919c954e4 100644 --- a/src/virtualtreadmill.cpp +++ b/src/virtualtreadmill.cpp @@ -353,7 +353,7 @@ void virtualtreadmill::treadmillProvider() { if (h->virtualtreadmill_updateFTMS( normalizeSpeed, 0, (uint16_t)((treadmill *)treadMill)->currentCadence().value() * cadence_multiplier, (uint16_t)((treadmill *)treadMill)->wattsMetric().value(), - treadMill->currentInclination().value() * 10)) { + treadMill->currentInclination().value() * 10, (uint64_t)(((treadmill *)treadMill)->odometer() * 1000.0))) { h->virtualtreadmill_setHeartRate(((treadmill *)treadMill)->currentHeart().value()); lastSlopeChanged = h->virtualtreadmill_lastChangeCurrentSlope(); if ((uint64_t)QDateTime::currentSecsSinceEpoch() < lastSlopeChanged + slopeTimeoutSecs) From effb3cdfe570397dca680b91c7341ded1c74400c Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Sun, 17 Dec 2023 18:16:56 +0100 Subject: [PATCH 04/32] Octane Fitness 37xi connection to QZ app #1876 --- src/octaneelliptical.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index 2ff1cf212..de8a969cc 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -395,7 +395,7 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); // Q37xi has a fixed stride length of 20.5 inches (52cm). - Speed = (Cadence.value() * 52.0 * 60) / 10000; + Speed = ((Cadence.value() / 2.0) * 52.07 * 60) / 10000; emit speedChanged(speed.value()); emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); From a152a0edb963cb51b12d676aec8b477e3f27de54 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Sun, 17 Dec 2023 19:12:19 +0100 Subject: [PATCH 05/32] version 2.16.28 --- src/android/AndroidManifest.xml | 2 +- src/main.qml | 2 +- src/qdomyos-zwift.pri | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml index 3e7317c51..a49826579 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/src/main.qml b/src/main.qml index 22461c0cd..13ec21611 100644 --- a/src/main.qml +++ b/src/main.qml @@ -751,7 +751,7 @@ ApplicationWindow { } ItemDelegate { - text: "version 2.16.27" + text: "version 2.16.28" width: parent.width } diff --git a/src/qdomyos-zwift.pri b/src/qdomyos-zwift.pri index 88aff000e..f37e96153 100644 --- a/src/qdomyos-zwift.pri +++ b/src/qdomyos-zwift.pri @@ -819,4 +819,4 @@ INCLUDEPATH += purchasing/inapp WINRT_MANIFEST = AppxManifest.xml -VERSION = 2.16.27 +VERSION = 2.16.28 From 21f977ad44470cbbd40dfa2e5293f205f98c839b Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Mon, 18 Dec 2023 14:28:57 +0100 Subject: [PATCH 06/32] Any hints / tips for using the following a GPX feature? #1893 --- src/trainprogram.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/trainprogram.cpp b/src/trainprogram.cpp index 81eb9ef6c..98fbc278f 100644 --- a/src/trainprogram.cpp +++ b/src/trainprogram.cpp @@ -436,27 +436,27 @@ double trainprogram::avgInclinationNext100Meters(int step) { if (sum == 1) { return rows.at(currentStep).inclination; } - return avg / (double)sum; + return avg / (double)km; } if (c == currentStep) km += (rows.at(c).distance - currentStepDistance); else km += (rows.at(c).distance); - avg += rows.at(c).inclination; + avg += rows.at(c).inclination * rows.at(c).distance; sum++; } else { if (sum == 1) { return rows.at(currentStep).inclination; } - return avg / (double)sum; + return avg / (double)km; } c++; } if (sum == 1) { return rows.at(currentStep).inclination; } - return avg / (double)sum; + return avg / (double)km; } double trainprogram::avgAzimuthNext300Meters() { From 372d57368cb1eefd0f569788ccfea25911096251 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Tue, 19 Dec 2023 10:15:41 +0100 Subject: [PATCH 07/32] adding inclination calculation to the stryd --- src/strydrunpowersensor.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/strydrunpowersensor.cpp b/src/strydrunpowersensor.cpp index a753eb8ba..e5cb87a2e 100644 --- a/src/strydrunpowersensor.cpp +++ b/src/strydrunpowersensor.cpp @@ -119,7 +119,9 @@ void strydrunpowersensor::characteristicChanged(const QLowEnergyCharacteristic & if (newValue.length() > 3) { powerReceived = true; - m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2))); + double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); + double vwatts = ((9.8 * weight) * (currentInclination().value() / 100.0)); + m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2))) + vwatts; } emit powerChanged(m_watt.value()); From 2b541c220b2f96ee0467089001f3332e3663008f Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 20 Dec 2023 12:16:27 +0100 Subject: [PATCH 08/32] Support for Pro-Form Cycle Trainer 300 ci (Issue #1892) --- src/proformbike.cpp | 41 +++++++++++++++++++++++++++++++++++------ src/qzsettings.cpp | 4 +++- src/qzsettings.h | 3 +++ src/settings.qml | 16 ++++++++++++++++ 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/proformbike.cpp b/src/proformbike.cpp index ba39bc9cb..b31de2227 100644 --- a/src/proformbike.cpp +++ b/src/proformbike.cpp @@ -162,6 +162,9 @@ void proformbike::forceResistance(resistance_t requestResistance) { .value(QZSettings::proform_hybrid_trainer_PFEL03815, QZSettings::default_proform_hybrid_trainer_PFEL03815) .toBool(); bool proform_bike_sb = settings.value(QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb).toBool(); + bool proform_cycle_trainer_300_ci = + settings.value(QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci) + .toBool(); if (proform_studio || proform_tdf_10) { const uint8_t res1[] = {0xfe, 0x02, 0x16, 0x03}; @@ -406,6 +409,11 @@ void proformbike::forceResistance(resistance_t requestResistance) { const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xd1, 0x26, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00}; + if(proform_cycle_trainer_300_ci) { + uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02}; + writeCharacteristic((uint8_t *)noOpData7, sizeof(noOpData7), QStringLiteral("resrequest"), false, false); + } + switch (requestResistance) { case 1: writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true); @@ -509,6 +517,9 @@ void proformbike::update() { bool proform_tdf_10 = settings.value(QZSettings::proform_tdf_10, QZSettings::default_proform_tdf_10).toBool(); bool nordictrack_gx_2_7 = settings.value(QZSettings::nordictrack_gx_2_7, QZSettings::default_nordictrack_gx_2_7).toBool(); + bool proform_cycle_trainer_300_ci = + settings.value(QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci) + .toBool(); bool proform_cycle_trainer_400 = settings.value(QZSettings::proform_cycle_trainer_400, QZSettings::default_proform_cycle_trainer_400) .toBool(); @@ -601,7 +612,7 @@ void proformbike::update() { switch (counterPoll) { case 0: - if (nordictrack_gx_2_7 || proform_hybrid_trainer_PFEL03815 || proform_bike_sb) { + if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb) { writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp")); } else { writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp")); @@ -610,7 +621,7 @@ void proformbike::update() { case 1: if (proform_studio || proform_tdf_10) writeCharacteristic(noOpData2_proform_studio, sizeof(noOpData2_proform_studio), QStringLiteral("noOp")); - else if (nordictrack_gx_2_7) { + else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) { writeCharacteristic(noOpData2_nordictrack_gx_2_7, sizeof(noOpData2_nordictrack_gx_2_7), QStringLiteral("noOp")); } else if (proform_hybrid_trainer_PFEL03815) { @@ -637,7 +648,7 @@ void proformbike::update() { else if (proform_tour_de_france_clc) writeCharacteristic(noOpData3_proform_tour_de_france_clc, sizeof(noOpData3_proform_tour_de_france_clc), QStringLiteral("noOp")); - else if (nordictrack_gx_2_7) { + else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) { writeCharacteristic(noOpData3_nordictrack_gx_2_7, sizeof(noOpData3_nordictrack_gx_2_7), QStringLiteral("noOp")); } else if (proform_hybrid_trainer_PFEL03815) { @@ -658,7 +669,7 @@ void proformbike::update() { case 3: if (proform_studio || proform_tdf_10) writeCharacteristic(noOpData4_proform_studio, sizeof(noOpData4_proform_studio), QStringLiteral("noOp")); - else if (nordictrack_gx_2_7) { + else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) { innerWriteResistance(); writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp")); } else if (proform_hybrid_trainer_PFEL03815) { @@ -674,7 +685,7 @@ void proformbike::update() { case 4: if (proform_studio || proform_tdf_10) writeCharacteristic(noOpData5_proform_studio, sizeof(noOpData5_proform_studio), QStringLiteral("noOp")); - else if (nordictrack_gx_2_7) { + else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) { writeCharacteristic(noOpData5_nordictrack_gx_2_7, sizeof(noOpData5_nordictrack_gx_2_7), QStringLiteral("noOp")); } else if (proform_hybrid_trainer_PFEL03815) { @@ -733,7 +744,7 @@ void proformbike::update() { requestResistance == -1) { // this bike sends the frame noOpData7 only when it needs to change the resistance counterPoll = 0; - } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_hybrid_trainer_PFEL03815 || proform_bike_sb)) { + } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb)) { counterPoll = 0; } @@ -1271,6 +1282,8 @@ void proformbike::btinit() { settings.value(QZSettings::nordictrack_gx_2_7, QZSettings::default_nordictrack_gx_2_7).toBool(); bool proform_cycle_trainer_400 = settings.value(QZSettings::proform_cycle_trainer_400, QZSettings::default_proform_cycle_trainer_400).toBool(); + bool proform_cycle_trainer_300_ci = + settings.value(QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci).toBool(); bool proform_hybrid_trainer_PFEL03815 = settings .value(QZSettings::proform_hybrid_trainer_PFEL03815, QZSettings::default_proform_hybrid_trainer_PFEL03815) @@ -1524,6 +1537,22 @@ void proformbike::btinit() { QThread::msleep(400); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); QThread::msleep(400); + } else if (proform_cycle_trainer_300_ci) { + max_resistance = 16; + + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07, + 0x01, 0x10, 0xcc, 0x7a, 0x3e, 0xf4, 0xb8, 0x66, 0x3a, 0xf8}; + uint8_t initData11[] = {0x01, 0x12, 0xb4, 0x72, 0x46, 0x1c, 0xf0, 0xbe, 0x92, 0x40, + 0x3c, 0xea, 0xce, 0xa4, 0x88, 0x76, 0x4a, 0x28, 0x04, 0xe2}; + uint8_t initData12[] = {0xff, 0x08, 0xf6, 0xcc, 0xe0, 0x80, 0x02, 0x00, 0x00, 0xb5, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(400); } else if (proform_hybrid_trainer_PFEL03815) { max_resistance = 16; uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x04, diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index c85429b5a..d4c95a7cc 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -684,8 +684,9 @@ const QString QZSettings::iconsole_elliptical = QStringLiteral("iconsole_ellipti const QString QZSettings::autolap_distance = QStringLiteral("autolap_distance"); const QString QZSettings::nordictrack_s20_treadmill = QStringLiteral("nordictrack_s20_treadmill"); const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotion_coachbike_b22_7"); +const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci"); -const uint32_t allSettingsCount = 574; +const uint32_t allSettingsCount = 575; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1266,6 +1267,7 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::autolap_distance, QZSettings::default_autolap_distance}, {QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill}, {QZSettings::freemotion_coachbike_b22_7, QZSettings::default_freemotion_coachbike_b22_7}, + {QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index 0526cfb55..c94e0bfae 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -1923,6 +1923,9 @@ class QZSettings { static const QString freemotion_coachbike_b22_7; static constexpr bool default_freemotion_coachbike_b22_7 = false; + static const QString proform_cycle_trainer_300_ci; + static constexpr bool default_proform_cycle_trainer_300_ci = false; + /** * @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.qml b/src/settings.qml index 454044904..f52588127 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -850,6 +850,9 @@ import QtQuick.Dialogs 1.0 // from version 2.16.28 property bool freemotion_coachbike_b22_7: false + + // from version 2.16.29 + property bool proform_cycle_trainer_300_ci: false } function paddingZeros(text, limit) { @@ -2987,6 +2990,19 @@ import QtQuick.Dialogs 1.0 Layout.fillWidth: true onClicked: settings.proform_tdf_jonseed_watt = checked } + SwitchDelegate { + text: qsTr("Cycle Trainer 300 CI") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_cycle_trainer_300_ci + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_cycle_trainer_300_ci = checked; window.settings_restart_to_apply = true; } + } SwitchDelegate { id: proformCycleTrainerdelegate text: qsTr("Cycle Trainer 400") From f1e14d16522c34996db78c907bdfe2d8e8ef8b1d Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 20 Dec 2023 14:29:11 +0100 Subject: [PATCH 09/32] Octane Fitness 37xi connection to QZ app #1876 --- src/octaneelliptical.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index de8a969cc..bcd9b13b9 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -172,6 +172,7 @@ octaneelliptical::octaneelliptical(uint32_t pollDeviceTime, bool noConsole, bool // SPEED actualPaceSign.append(0x01); actualPaceSign.append(0x07); + actualPace2Sign.append(0x00); actualPace2Sign.append(0x07); actualHR.append((char)0x02); @@ -381,12 +382,12 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } - if (!newValue.contains(actualPaceSign) /*&& !newValue.contains(actualPace2Sign)*/) + if (!newValue.contains(actualPaceSign) && !newValue.contains(actualPace2Sign)) return; int16_t i = newValue.indexOf(actualPaceSign) + 2; - /*if (i <= 1) - i = newValue.indexOf(actualPace2Sign) + 1;*/ + if (i <= 1) + i = newValue.indexOf(actualPace2Sign) + 1; if (i + 1 >= newValue.length() || i <= 1) return; @@ -395,7 +396,7 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); // Q37xi has a fixed stride length of 20.5 inches (52cm). - Speed = ((Cadence.value() / 2.0) * 52.07 * 60) / 10000; + Speed = (((Cadence.value() / 2.0) * 52.07 * 60) / 10000) * 0.84135; emit speedChanged(speed.value()); emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); From 49e288ec01f9ccb3797577937f3090bf33f2f39b Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 20 Dec 2023 14:32:31 +0100 Subject: [PATCH 10/32] Update octaneelliptical.cpp --- src/octaneelliptical.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index bcd9b13b9..b371a3ee8 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -172,7 +172,7 @@ octaneelliptical::octaneelliptical(uint32_t pollDeviceTime, bool noConsole, bool // SPEED actualPaceSign.append(0x01); actualPaceSign.append(0x07); - actualPace2Sign.append(0x00); + actualPace2Sign.append((char)0x00); actualPace2Sign.append(0x07); actualHR.append((char)0x02); From 3330cc516e4e084ab89347234315ccad69f94b26 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 20 Dec 2023 14:57:32 +0100 Subject: [PATCH 11/32] Kingsmith G1 Walking Pad (Issue #1833 --- src/kingsmithr2treadmill.cpp | 4 ++++ src/kingsmithr2treadmill.h | 2 ++ src/qzsettings.cpp | 4 +++- src/qzsettings.h | 3 +++ src/settings.qml | 23 +++++++++++++++++++---- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/kingsmithr2treadmill.cpp b/src/kingsmithr2treadmill.cpp index d4f266fcc..c06d2367f 100644 --- a/src/kingsmithr2treadmill.cpp +++ b/src/kingsmithr2treadmill.cpp @@ -67,6 +67,8 @@ void kingsmithr2treadmill::writeCharacteristic(const QString &data, const QStrin encrypted.append(ENCRYPT_TABLE_v4[idx]); else if (settings.value(QZSettings::kingsmith_encrypt_v5, QZSettings::default_kingsmith_encrypt_v5).toBool()) encrypted.append(ENCRYPT_TABLE_v5[idx]); + else if (settings.value(QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad).toBool()) + encrypted.append(ENCRYPT_TABLE_v6[idx]); else encrypted.append(ENCRYPT_TABLE[idx]); } @@ -269,6 +271,8 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic idx = ENCRYPT_TABLE_v4.indexOf(ch); else if (settings.value(QZSettings::kingsmith_encrypt_v5, QZSettings::default_kingsmith_encrypt_v5).toBool()) idx = ENCRYPT_TABLE_v5.indexOf(ch); + else if (settings.value(QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad).toBool()) + idx = ENCRYPT_TABLE_v6.indexOf(ch); else idx = ENCRYPT_TABLE.indexOf(ch); decrypted.append(PLAINTEXT_TABLE[idx]); diff --git a/src/kingsmithr2treadmill.h b/src/kingsmithr2treadmill.h index 829556045..90ec55f6b 100644 --- a/src/kingsmithr2treadmill.h +++ b/src/kingsmithr2treadmill.h @@ -56,6 +56,8 @@ class kingsmithr2treadmill : public treadmill { QStringLiteral("ZaCw4FGHIJqLhN9P+RVTU/WcY6ObDdefgEijklmnopQrsBuvMxXz1yA2t5078KS3=").toUtf8(); const QByteArray ENCRYPT_TABLE_v5 = QStringLiteral("iaCw4FGHIJqLhN+P9RVTU/WcY6ObDdefgEZjklmnopQrsBuvMxXz1yA2t5078KS3=").toUtf8(); + const QByteArray ENCRYPT_TABLE_v6 = + QStringLiteral("ZaCw4FGHIJqLhN+P8RVTU/WcY6ObDdefgEijklmnopQrsBuvMxXz1yA2t5079KS3=").toUtf8(); double GetInclinationFromPacket(const QByteArray &packet); double GetKcalFromPacket(const QByteArray &packet); diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index d4c95a7cc..a9a8da8c9 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -685,8 +685,9 @@ const QString QZSettings::autolap_distance = QStringLiteral("autolap_distance"); const QString QZSettings::nordictrack_s20_treadmill = QStringLiteral("nordictrack_s20_treadmill"); const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotion_coachbike_b22_7"); const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci"); +const QString QZSettings::kingsmith_encrypt_g1_walking_pad = QStringLiteral("kingsmith_encrypt_g1_walking_pad"); -const uint32_t allSettingsCount = 575; +const uint32_t allSettingsCount = 576; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1268,6 +1269,7 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill}, {QZSettings::freemotion_coachbike_b22_7, QZSettings::default_freemotion_coachbike_b22_7}, {QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci}, + {QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index c94e0bfae..7ea9716b4 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -1926,6 +1926,9 @@ class QZSettings { static const QString proform_cycle_trainer_300_ci; static constexpr bool default_proform_cycle_trainer_300_ci = false; + static const QString kingsmith_encrypt_g1_walking_pad; + static constexpr bool default_kingsmith_encrypt_g1_walking_pad = false; + /** * @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.qml b/src/settings.qml index f52588127..d707dd428 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -853,6 +853,7 @@ import QtQuick.Dialogs 1.0 // from version 2.16.29 property bool proform_cycle_trainer_300_ci: false + property bool kingsmith_encrypt_g1_walking_pad: false } function paddingZeros(text, limit) { @@ -5803,7 +5804,7 @@ import QtQuick.Dialogs 1.0 checked: settings.kingsmith_encrypt_v2 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: { settings.kingsmith_encrypt_v2 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v4 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } + onClicked: { settings.kingsmith_encrypt_v2 = checked; settings.kingsmith_encrypt_g1_walking_pad = false; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v4 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5818,7 +5819,7 @@ import QtQuick.Dialogs 1.0 checked: settings.kingsmith_encrypt_v3 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: { settings.kingsmith_encrypt_v3 = checked; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } + onClicked: { settings.kingsmith_encrypt_v3 = checked; settings.kingsmith_encrypt_g1_walking_pad = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5833,7 +5834,7 @@ import QtQuick.Dialogs 1.0 checked: settings.kingsmith_encrypt_v4 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: { settings.kingsmith_encrypt_v4 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } + onClicked: { settings.kingsmith_encrypt_v4 = checked; settings.kingsmith_encrypt_g1_walking_pad = false; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5847,8 +5848,22 @@ import QtQuick.Dialogs 1.0 checked: settings.kingsmith_encrypt_v5 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: { settings.kingsmith_encrypt_v5 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; window.settings_restart_to_apply = true; } + onClicked: { settings.kingsmith_encrypt_v5 = checked; settings.kingsmith_encrypt_g1_walking_pad = false; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; window.settings_restart_to_apply = true; } } + + SwitchDelegate { + text: qsTr("WalkingPad G1") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.kingsmith_encrypt_g1_walking_pad + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.kingsmith_encrypt_g1_walking_pad = checked; settings.kingsmith_encrypt_v5 = false; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; window.settings_restart_to_apply = true; } + } } } From fe393bd70d94e18dde83d58f1f6d128b5716fb30 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 21 Dec 2023 09:19:00 +0100 Subject: [PATCH 12/32] Octane Fitness 37xi connection to QZ app #1876 --- src/octaneelliptical.cpp | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index b371a3ee8..095cb7cf9 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -382,15 +382,22 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } - if (!newValue.contains(actualPaceSign) && !newValue.contains(actualPace2Sign)) - return; - int16_t i = newValue.indexOf(actualPaceSign) + 2; - if (i <= 1) - i = newValue.indexOf(actualPace2Sign) + 1; - - if (i + 1 >= newValue.length() || i <= 1) - return; + /*if (i <= 1) + i = newValue.indexOf(actualPace2Sign) + 1;*/ + + if (i + 1 >= newValue.length() || i <= 1) { + // fallback for a previous firmware version + if(newValue.length() >= 20 && newValue.at(0) == 0xa5 &&) { + if(newValue.at(6) == 0x07) { + i = 7; + } else if(newValue.at(15) == 0x07) { + i = 16; + } + } else { + return; + } + } Cadence = ((uint8_t)value.at(i)); emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); From b7763154a3c6c6de0054c66b7210eef30567044e Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 21 Dec 2023 09:29:43 +0100 Subject: [PATCH 13/32] Update octaneelliptical.cpp --- src/octaneelliptical.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index 095cb7cf9..d3452739e 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -388,11 +388,13 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha if (i + 1 >= newValue.length() || i <= 1) { // fallback for a previous firmware version - if(newValue.length() >= 20 && newValue.at(0) == 0xa5 &&) { + if(newValue.length() >= 20 && newValue.at(0) == 0xa5) { if(newValue.at(6) == 0x07) { i = 7; } else if(newValue.at(15) == 0x07) { i = 16; + } else { + return; } } else { return; From 6c93d859320375fc80c3b80a0a81f090444ec25a Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 21 Dec 2023 10:54:02 +0100 Subject: [PATCH 14/32] Floating window: adding total remaining time after the elapsed time #1863 --- src/inner_templates/floating/floating.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inner_templates/floating/floating.htm b/src/inner_templates/floating/floating.htm index b7951b1a4..495133ab3 100644 --- a/src/inner_templates/floating/floating.htm +++ b/src/inner_templates/floating/floating.htm @@ -767,7 +767,7 @@

Peloton Workout in progress! } else if (key === 'gears') { gears = msg.content[key]; } else if (key === 'peloton_resistance_color') { - $('.peloton_resistance-value').css('color', msg.content[key]); + $('.pelotonresistance-value').css('color', msg.content[key]); } else if (key === 'heart_color') { $('.heart-value').css('color', msg.content[key]); } else if (key === 'cadence_color') { From fef5d8f4a76c368ebb347d3c758a6bbcd984d840 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 21 Dec 2023 15:03:52 +0100 Subject: [PATCH 15/32] Octane Fitness 37xi connection to QZ app #1876 --- src/octaneelliptical.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index d3452739e..1b0f2a858 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -388,7 +388,7 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha if (i + 1 >= newValue.length() || i <= 1) { // fallback for a previous firmware version - if(newValue.length() >= 20 && newValue.at(0) == 0xa5) { + if(newValue.length() >= 20 && (uint8_t)newValue.at(0) == 0xa5) { if(newValue.at(6) == 0x07) { i = 7; } else if(newValue.at(15) == 0x07) { From 52579e3efc7e5d6987822720c9a4c04620366bd5 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 21 Dec 2023 16:42:57 +0100 Subject: [PATCH 16/32] Support for Pro-Form Cycle Trainer 300 ci #1892 --- src/proformbike.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/proformbike.cpp b/src/proformbike.cpp index b31de2227..bae6dd3f3 100644 --- a/src/proformbike.cpp +++ b/src/proformbike.cpp @@ -1037,6 +1037,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte } } else if (!nordictrack_gx_2_7) { switch ((uint8_t)newValue.at(11)) { + case 0x00: case 0x02: Resistance = 1; m_pelotonResistance = 10; From f6b89845759a1f1b7631c5d6c1f98e9a6d4b0a72 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Sat, 23 Dec 2023 09:54:15 +0100 Subject: [PATCH 17/32] Floating window: adding total remaining time after the elapsed time (Issue #1863) --- src/homeform.cpp | 10 --- src/homeform.h | 124 +++++++++++++++--------------- src/metric.h | 5 +- src/templateinfosenderbuilder.cpp | 16 ++-- 4 files changed, 71 insertions(+), 84 deletions(-) diff --git a/src/homeform.cpp b/src/homeform.cpp index d65748416..82c6fec13 100644 --- a/src/homeform.cpp +++ b/src/homeform.cpp @@ -3785,7 +3785,6 @@ void homeform::update() { speed->setValueFontColor(QStringLiteral("red")); this->pace->setValueFontColor(QStringLiteral("red")); } - bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); } else { if (bluetoothManager->device()->currentSpeed().value() <= trainProgram->currentRow().upper_speed && bluetoothManager->device()->currentSpeed().value() >= trainProgram->currentRow().lower_speed) { @@ -3801,7 +3800,6 @@ void homeform::update() { this->target_zone->setValueFontColor(QStringLiteral("red")); this->pace->setValueFontColor(QStringLiteral("red")); } - bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); } this->target_pace->setValue( @@ -4053,7 +4051,6 @@ void homeform::update() { speed->setValueFontColor(QStringLiteral("red")); this->pace->setValueFontColor(QStringLiteral("red")); } - bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); } } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) { @@ -4155,10 +4152,6 @@ void homeform::update() { } else { this->peloton_resistance->setValueFontColor(QStringLiteral("orange")); } - if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - ((bike *)bluetoothManager->device()) - ->pelotonResistance() - .setColor(this->peloton_resistance->valueFontColor()); } int16_t lower_cadence = trainProgram->currentRow().lower_cadence; @@ -4181,7 +4174,6 @@ void homeform::update() { } else { this->cadence->setValueFontColor(QStringLiteral("orange")); } - bluetoothManager->device()->currentCadence().setColor(this->cadence->valueFontColor()); } } @@ -4277,7 +4269,6 @@ void homeform::update() { ftp->setValueFontColor(QStringLiteral("red")); watt->setValueFontColor(QStringLiteral("red")); } - bluetoothManager->device()->wattsMetric().setColor(watt->valueFontColor()); bluetoothManager->device()->setPowerZone(ftpZone); ftp->setValue(QStringLiteral("Z") + QString::number(ftpZone, 'f', 1)); ftp->setSecondLine(ftpMinW + QStringLiteral("-") + ftpMaxW + QStringLiteral("W ") + @@ -4475,7 +4466,6 @@ void homeform::update() { pidHR->setValueFontColor(QStringLiteral("white")); break; } - bluetoothManager->device()->currentHeart().setColor(heart->valueFontColor()); bluetoothManager->device()->setHeartZone(currentHRZone); Z = QStringLiteral("Z") + QString::number(currentHRZone, 'f', 1); heart->setSecondLine(Z + QStringLiteral(" AVG: ") + diff --git a/src/homeform.h b/src/homeform.h index f52502252..72587b2d5 100644 --- a/src/homeform.h +++ b/src/homeform.h @@ -555,6 +555,68 @@ class homeform : public QObject { QString getStravaAuthUrl() { return stravaAuthUrl; } bool stravaWebVisible() { return stravaAuthWebVisible; } trainprogram *trainingProgram() { return trainProgram; } + + DataObject *speed; + DataObject *inclination; + DataObject *cadence; + DataObject *elevation; + DataObject *calories; + DataObject *odometer; + DataObject *pace; + DataObject *datetime; + DataObject *resistance; + DataObject *watt; + DataObject *avgWatt; + DataObject *avgWattLap; + DataObject *heart; + DataObject *fan; + DataObject *jouls; + DataObject *peloton_offset; + DataObject *peloton_remaining; + DataObject *elapsed; + DataObject *moving_time; + DataObject *peloton_resistance; + DataObject *target_resistance; + DataObject *target_peloton_resistance; + DataObject *target_cadence; + DataObject *target_power; + DataObject *target_zone; + DataObject *target_speed; + DataObject *target_pace; + DataObject *target_incline; + DataObject *ftp; + DataObject *lapElapsed; + DataObject *weightLoss; + DataObject *strokesLength; + DataObject *strokesCount; + DataObject *wattKg; + DataObject *gears; + DataObject *remaningTimeTrainingProgramCurrentRow; + DataObject *nextRows; + DataObject *mets; + DataObject *targetMets; + DataObject *steeringAngle; + DataObject *pidHR; + DataObject *extIncline; + DataObject *instantaneousStrideLengthCM; + DataObject *groundContactMS; + DataObject *verticalOscillationMM; + DataObject *preset_resistance_1; + DataObject *preset_resistance_2; + DataObject *preset_resistance_3; + DataObject *preset_resistance_4; + DataObject *preset_resistance_5; + DataObject *preset_speed_1; + DataObject *preset_speed_2; + DataObject *preset_speed_3; + DataObject *preset_speed_4; + DataObject *preset_speed_5; + DataObject *preset_inclination_1; + DataObject *preset_inclination_2; + DataObject *preset_inclination_3; + DataObject *preset_inclination_4; + DataObject *preset_inclination_5; + DataObject *pace_last500m; private: static homeform *m_singleton; @@ -618,68 +680,6 @@ class homeform : public QObject { bool m_startRequested = false; bool m_overridePower = false; - DataObject *speed; - DataObject *inclination; - DataObject *cadence; - DataObject *elevation; - DataObject *calories; - DataObject *odometer; - DataObject *pace; - DataObject *datetime; - DataObject *resistance; - DataObject *watt; - DataObject *avgWatt; - DataObject *avgWattLap; - DataObject *heart; - DataObject *fan; - DataObject *jouls; - DataObject *peloton_offset; - DataObject *peloton_remaining; - DataObject *elapsed; - DataObject *moving_time; - DataObject *peloton_resistance; - DataObject *target_resistance; - DataObject *target_peloton_resistance; - DataObject *target_cadence; - DataObject *target_power; - DataObject *target_zone; - DataObject *target_speed; - DataObject *target_pace; - DataObject *target_incline; - DataObject *ftp; - DataObject *lapElapsed; - DataObject *weightLoss; - DataObject *strokesLength; - DataObject *strokesCount; - DataObject *wattKg; - DataObject *gears; - DataObject *remaningTimeTrainingProgramCurrentRow; - DataObject *nextRows; - DataObject *mets; - DataObject *targetMets; - DataObject *steeringAngle; - DataObject *pidHR; - DataObject *extIncline; - DataObject *instantaneousStrideLengthCM; - DataObject *groundContactMS; - DataObject *verticalOscillationMM; - DataObject *preset_resistance_1; - DataObject *preset_resistance_2; - DataObject *preset_resistance_3; - DataObject *preset_resistance_4; - DataObject *preset_resistance_5; - DataObject *preset_speed_1; - DataObject *preset_speed_2; - DataObject *preset_speed_3; - DataObject *preset_speed_4; - DataObject *preset_speed_5; - DataObject *preset_inclination_1; - DataObject *preset_inclination_2; - DataObject *preset_inclination_3; - DataObject *preset_inclination_4; - DataObject *preset_inclination_5; - DataObject *pace_last500m; - QTimer *timer; QTimer *backupTimer; diff --git a/src/metric.h b/src/metric.h index c13be8668..19e13125f 100644 --- a/src/metric.h +++ b/src/metric.h @@ -42,8 +42,6 @@ class metric { void operator+=(double); void setPaused(bool p); void setLap(bool accumulator); - void setColor(QString color) { m_color = color; } - QString color() { return m_color; } static double calculateMaxSpeedFromPower(double power, double inclination); static double calculatePowerFromSpeed(double speed, double inclination); @@ -54,7 +52,7 @@ class metric { static double calculateKCalfromHR(double HR_AVG, double elapsed); static double powerPeak(QList *session, int seconds); - + private: double m_value = 0; double m_totValue = 0; @@ -77,7 +75,6 @@ class metric { _metric_type m_type = METRIC_OTHER; bool paused = false; - QString m_color; }; #endif // METRIC_H diff --git a/src/templateinfosenderbuilder.cpp b/src/templateinfosenderbuilder.cpp index 791ac0b25..b23d57380 100644 --- a/src/templateinfosenderbuilder.cpp +++ b/src/templateinfosenderbuilder.cpp @@ -995,13 +995,13 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("moving_h"), el.hour()); obj.setProperty(QStringLiteral("speed"), (dep = device->currentSpeed()).value()); obj.setProperty(QStringLiteral("speed_avg"), dep.average()); - obj.setProperty(QStringLiteral("speed_color"), dep.color()); + obj.setProperty(QStringLiteral("speed_color"), homeform::singleton()->speed->valueFontColor()); obj.setProperty(QStringLiteral("speed_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("speed_lapmax"), dep.lapMax()); obj.setProperty(QStringLiteral("calories"), device->calories().value()); obj.setProperty(QStringLiteral("distance"), device->odometer()); obj.setProperty(QStringLiteral("heart"), (dep = device->currentHeart()).value()); - obj.setProperty(QStringLiteral("heart_color"), dep.color()); + obj.setProperty(QStringLiteral("heart_color"), homeform::singleton()->heart->valueFontColor()); obj.setProperty(QStringLiteral("heart_avg"), dep.average()); obj.setProperty(QStringLiteral("heart_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("heart_max"), dep.max()); @@ -1011,7 +1011,7 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("difficult"), device->difficult()); obj.setProperty(QStringLiteral("watts"), (dep = device->wattsMetric()).value()); obj.setProperty(QStringLiteral("watts_avg"), dep.average()); - obj.setProperty(QStringLiteral("watts_color"), dep.color()); + obj.setProperty(QStringLiteral("watts_color"), homeform::singleton()->watt->valueFontColor()); obj.setProperty(QStringLiteral("watts_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("watts_max"), dep.max()); obj.setProperty(QStringLiteral("watts_lapmax"), dep.lapMax()); @@ -1067,13 +1067,13 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("peloton_resistance"), (dep = ((bike *)device)->pelotonResistance()).value()); obj.setProperty(QStringLiteral("peloton_resistance_avg"), dep.average()); - obj.setProperty(QStringLiteral("peloton_resistance_color"), dep.color()); + obj.setProperty(QStringLiteral("peloton_resistance_color"), homeform::singleton()->peloton_resistance->valueFontColor()); obj.setProperty(QStringLiteral("peloton_resistance_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("peloton_resistance_lapmax"), dep.lapMax()); obj.setProperty(QStringLiteral("peloton_req_resistance"), (dep = ((bike *)device)->lastRequestedPelotonResistance()).value()); obj.setProperty(QStringLiteral("cadence"), (dep = ((bike *)device)->currentCadence()).value()); - obj.setProperty(QStringLiteral("cadence_color"), dep.color()); + obj.setProperty(QStringLiteral("cadence_color"), homeform::singleton()->cadence->valueFontColor()); obj.setProperty(QStringLiteral("cadence_avg"), dep.average()); obj.setProperty(QStringLiteral("cadence_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("cadence_lapmax"), dep.lapMax()); @@ -1096,7 +1096,7 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { (dep = ((rower *)device)->pelotonResistance()).value()); obj.setProperty(QStringLiteral("peloton_resistance_avg"), dep.average()); obj.setProperty(QStringLiteral("cadence"), (dep = ((rower *)device)->currentCadence()).value()); - obj.setProperty(QStringLiteral("cadence_color"), dep.color()); + obj.setProperty(QStringLiteral("cadence_color"), homeform::singleton()->cadence->valueFontColor()); obj.setProperty(QStringLiteral("cadence_avg"), dep.average()); obj.setProperty(QStringLiteral("cadence_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("cadence_lapmax"), dep.lapMax()); @@ -1116,7 +1116,7 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("target_inclination"), ((treadmill *)device)->lastRequestedInclination().value()); obj.setProperty(QStringLiteral("cadence"), (dep = ((treadmill *)device)->currentCadence()).value()); - obj.setProperty(QStringLiteral("cadence_color"), dep.color()); + obj.setProperty(QStringLiteral("cadence_color"), homeform::singleton()->cadence->valueFontColor()); obj.setProperty(QStringLiteral("cadence_avg"), dep.average()); obj.setProperty(QStringLiteral("cadence_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("cadence_lapmax"), dep.lapMax()); @@ -1132,7 +1132,7 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { (dep = ((treadmill *)device)->currentVerticalOscillation()).value()); } else if (tp == bluetoothdevice::ELLIPTICAL) { obj.setProperty(QStringLiteral("cadence"), (dep = ((elliptical *)device)->currentCadence()).value()); - obj.setProperty(QStringLiteral("cadence_color"), dep.color()); + obj.setProperty(QStringLiteral("cadence_color"), homeform::singleton()->cadence->valueFontColor()); obj.setProperty(QStringLiteral("cadence_avg"), dep.average()); obj.setProperty(QStringLiteral("cadence_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("cadence_lapmax"), dep.lapMax()); From 56c112ed4c59b97473a58ce33a0b81ec2b9b8d48 Mon Sep 17 00:00:00 2001 From: Al Udell <63697253+victorypoint@users.noreply.github.com> Date: Sat, 23 Dec 2023 05:02:10 -0400 Subject: [PATCH 18/32] Update python scripts to use AI.Server for OCR (#1847) * Update python scripts to use AI.Server for OCR * change python from 3.10 to 3.7 * Update main.yml * renaming script files * resuming here the old script as well * adding new build for ai server in CI * renaming python scripts for AI.server * Update homeform.cpp * Update homeform.cpp * fixing secrets on PR * restoring... * Update main.yml * Update homeform.cpp * Update main.yml * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_workout_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_workout_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_workout_paddleocr_thread.cpp * Update main.yml * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_workout_paddleocr_thread.cpp * Update windows_zwift_incline_paddleocr_thread.cpp * Update main.yml * Update main.yml * Update windows_zwift_incline_paddleocr_thread.cpp * Update windows_zwift_workout_paddleocr_thread.cpp * Update main.yml * removing nopython build for ai.server * Update main.yml --------- Co-authored-by: Roberto Viola --- .github/workflows/main.yml | 170 ++++++++++--- src/homeform.cpp | 10 +- src/windows/zwift-incline-ai-server.py | 117 +++++++++ .../zwift-incline-climb-portal-ai-server.py | 118 +++++++++ src/windows/zwift-incline-climb-portal.py | 232 +++++++++--------- src/windows/zwift-incline.py | 232 +++++++++--------- src/windows/zwift-workout-ai-server.py | 74 ++++++ src/windows/zwift-workout.py | 150 +++++------ ...windows_zwift_incline_paddleocr_thread.cpp | 11 +- ...windows_zwift_workout_paddleocr_thread.cpp | 10 +- 10 files changed, 779 insertions(+), 345 deletions(-) create mode 100644 src/windows/zwift-incline-ai-server.py create mode 100644 src/windows/zwift-incline-climb-portal-ai-server.py create mode 100644 src/windows/zwift-workout-ai-server.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e491c8a2f..bea1fde85 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -122,16 +122,20 @@ jobs: make install cd ../.. + - name: Secrets + if: github.ref == 'refs/heads/main' + run: | + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h + echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h + echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h + echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h + echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js + cd .. + - name: Build run: | qmake - cd src - echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h - echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h - echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h - echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h - echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js - cd .. make -j8 cd src/debug mkdir output @@ -159,13 +163,6 @@ jobs: - name: Build without python run: | qmake - cd src - echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h - echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h - echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h - echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h - echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js - cd .. make -j8 cd src/debug mkdir output @@ -816,16 +813,20 @@ jobs: nmake install cd ../.. + - name: Secrets + if: github.ref == 'refs/heads/main' + run: | + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h + echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h + echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h + echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h + echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js + cd .. + - name: Build run: | - qmake - cd src - echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h - echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h - echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h - echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h - echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js - cd .. + qmake nmake cd src/debug mkdir output @@ -849,14 +850,7 @@ jobs: - name: Build without python run: | - qmake - cd src - echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h - echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h - echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h - echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h - echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js - cd .. + qmake nmake cd src/debug mkdir output @@ -902,6 +896,121 @@ jobs: path: windows-msvc2019-binary-no-python.zip if: ${{ ! matrix.config.python }} + window-msvc2019-aiserver-build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: bluetiger9/SmtpClient-for-Qt + path: "src/smtpclient/" + ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c + + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: cagnulein/qmdnsengine + path: "src/qmdnsengine/" + ref: "zwift" + + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: google/googletest + path: "tst/googletest/" + ref: "release-1.12.1" + + - uses: actions/checkout@v2 + - name: Checkout qHttpServer + uses: actions/checkout@v2 + with: + repository: qt-labs/qthttpserver + path: "src/qthttpserver" + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: '5.15.2' + host: 'windows' + modules: 'qtnetworkauth qtcharts' + target: "desktop" + arch: win64_msvc2019_64 + dir: "${{github.workspace}}/qt/" + install-deps: "true" + cache: 'true' + cache-key-prefix: 'install-qt-action-windows' + + - name: Install MSVC compiler + uses: ilammy/msvc-dev-cmd@v1 + with: + # 14.1 is for vs2017, 14.2 is vs2019, following the upstream vcpkg build from Qv2ray-deps repo + toolset: 14.2 + arch: x64 + + - name: download 3rd party files for qthttpserver + run: | + cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/ + + - name: Build qthttpserver + run: | + cd src\qthttpserver + qmake + nmake + nmake install + cd ../.. + + - name: Secrets + if: github.ref == 'refs/heads/main' + run: | + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h + echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h + echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h + echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h + echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js + cd .. + + - name: Build + run: | + cd src + echo "#define AISERVER" >> aiserver.h + cd .. + qmake + nmake + cd src/debug + mkdir output + mkdir appx + cp qdomyos-zwift.exe output/ + cd output + windeployqt --qmldir ../../ qdomyos-zwift.exe + cp ../../../icons/iOS/iTunesArtwork@2x.png . + cp ../../AppxManifest.xml . + cp ../../windows/zwift-incline-ai-server.py zwift-incline.py + cp ../../windows/zwift-incline-climb-portal-ai-server.py zwift-incline-climb-portal.py + cp ../../windows/zwift-workout-ai-server.py zwift-workout.py + cp ../../windows/*.bat . + cp ../../../windows_openssl/*.* . + mkdir adb + cp ../../adb/* adb/ + cd .. + cd appx + #../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz + + - name: patching qt for bluetooth + run: cp qt-patches/windows/5.15.2/binary/msvc2019/*.* ${{ github.workspace }}/src/debug/output/ + + - name: Zip artifact for deployment + run: Compress-Archive src/debug/output windows-msvc2019-ai-server-binary.zip + + - name: Archive windows binary + uses: actions/upload-artifact@v2 + with: + name: windows-msvc2019-ai-server-binary + path: windows-msvc2019-ai-server-binary.zip + upload_to_release: permissions: write-all runs-on: ubuntu-latest @@ -931,6 +1040,7 @@ jobs: files: | windows-msvc2019-binary-no-python/* windows-msvc2019-binary/* + windows-msvc2019-ai-server-binary/* windows-binary-no-python/* windows-binary/* fdroid-android-trial/* diff --git a/src/homeform.cpp b/src/homeform.cpp index 82c6fec13..8a8e23656 100644 --- a/src/homeform.cpp +++ b/src/homeform.cpp @@ -53,7 +53,7 @@ using namespace std::chrono_literals; #if defined(WIN32) #pragma message("DEFINE STRAVA_CLIENT_ID!!!") #else -#warning "DEFINE STRAVA_CLIENT_ID!!!" +#pragma message "DEFINE STRAVA_CLIENT_ID!!!" #endif #endif #define _STR(x) #x @@ -5930,7 +5930,7 @@ QOAuth2AuthorizationCodeFlow *homeform::strava_connect() { #elif defined(WIN32) #pragma message("DEFINE STRAVA_SECRET_KEY!!!") #else -#warning "DEFINE STRAVA_SECRET_KEY!!!" +#pragma message "DEFINE STRAVA_SECRET_KEY!!!" #endif strava->setModifyParametersFunction( buildModifyParametersFunction(QUrl(QLatin1String("")), QUrl(QLatin1String("")))); @@ -6051,7 +6051,7 @@ void homeform::sendMail() { SmtpClient smtp(STRINGIFY(SMTP_SERVER), 587, SmtpClient::TlsConnection); connect(&smtp, SIGNAL(smtpError(SmtpClient::SmtpError)), this, SLOT(smtpError(SmtpClient::SmtpError))); #else -#warning "stmp server is unset!" +#pragma message "stmp server is unset!" SmtpClient smtp(QLatin1String(""), 25, SmtpClient::TlsConnection); return; #endif @@ -6063,7 +6063,7 @@ void homeform::sendMail() { #define STRINGIFY(x) _STR(x) smtp.setUser(STRINGIFY(SMTP_USERNAME)); #else -#warning "smtp username is unset!" +#pragma message "smtp username is unset!" return; #endif #ifdef SMTP_PASSWORD @@ -6071,7 +6071,7 @@ void homeform::sendMail() { #define STRINGIFY(x) _STR(x) smtp.setPassword(STRINGIFY(SMTP_PASSWORD)); #else -#warning "smtp password is unset!" +#pragma message "smtp password is unset!" return; #endif diff --git a/src/windows/zwift-incline-ai-server.py b/src/windows/zwift-incline-ai-server.py new file mode 100644 index 000000000..8aad322da --- /dev/null +++ b/src/windows/zwift-incline-ai-server.py @@ -0,0 +1,117 @@ +# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR +# Author: Al Udell + +# Revised: November 25, 2023 + +# zwift-incline.py - take Zwift screenshot, crop incline, OCR incline + +# imports +import cv2 +import numpy as np +import re +from PIL import Image, ImageGrab +import requests +import win32gui + +# Enable DPI aware on Windows +from ctypes import windll +user32 = windll.user32 +user32.SetProcessDPIAware() + +# Take Zwift screenshot - windowed mode only +hwnd = win32gui.FindWindow(None, 'Zwift') +if not hwnd: + print("Zwift is not running") + exit() +x, y, x1, y1 = win32gui.GetClientRect(hwnd) +x, y = win32gui.ClientToScreen(hwnd, (x, y)) +x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) +screenshot = ImageGrab.grab((x, y, x1, y1)) + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to incline area +screenwidth, screenheight = screenshot.size + +# Values for Zwift regular incline +col1 = int(screenwidth/3000 * 2800) +row1 = int(screenheight/2000 * 90) +col2 = int(screenwidth/3000 * 2975) +row2 = int(screenheight/2000 * 195) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Convert image to np array +cropped_np = np.array(cropped) + +# Convert np array to PIL +cropped_pil = Image.fromarray(cropped_np) + +# Convert PIL image to cv2 RGB +cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) + +# Convert cv2 RGB to HSV +result = cropped_cv2.copy() +image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) + +# Isolate white mask +lower = np.array([0,0,159]) +upper = np.array([0,0,255]) +mask0 = cv2.inRange(image, lower, upper) +result0 = cv2.bitwise_and(result, result, mask=mask0) + +# Isolate yellow mask +lower = np.array([24,239,241]) +upper = np.array([24,253,255]) +mask1 = cv2.inRange(image, lower, upper) +result1 = cv2.bitwise_and(result, result, mask=mask1) + +# Isolate orange mask +lower = np.array([8,191,243]) +upper = np.array([8,192,243]) +mask2 = cv2.inRange(image, lower, upper) +result2 = cv2.bitwise_and(result, result, mask=mask2) + +# Isolate red mask +lower = np.array([0,255,255]) +upper = np.array([10,255,255]) +mask3 = cv2.inRange(image, lower, upper) +result3 = cv2.bitwise_and(result, result, mask=mask3) + +# Join colour masks +mask = mask0+mask1+mask2+mask3 + +# Set output image to zero everywhere except mask +merge = image.copy() +merge[np.where(mask==0)] = 0 + +# Convert to grayscale +gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) + +# Convert to black/white by threshold +ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) + +# Apply gaussian blur +gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) + +# Write zwift image +cv2.imwrite('zwift.png', gaussianBlur, [cv2.IMWRITE_PNG_COMPRESSION, 0]) + +# OCR image +image_data = open("zwift.png","rb").read() +ocr = requests.post("http://localhost:32168/v1/image/ocr", files={"image":image_data}).json() + +# Extract label values from the 'predictions' list and merge into a single string +labels = [prediction['label'] for prediction in ocr.get('predictions', [])] +result = ''.join(labels) + +# Remove all characters that are not "-" and integers from OCR text +pattern = r"[^-\d]+" +ocr_text = re.sub(pattern, "", result) +if ocr_text: + incline = ocr_text +else: + incline = 'None' + +print(incline) diff --git a/src/windows/zwift-incline-climb-portal-ai-server.py b/src/windows/zwift-incline-climb-portal-ai-server.py new file mode 100644 index 000000000..1809faba1 --- /dev/null +++ b/src/windows/zwift-incline-climb-portal-ai-server.py @@ -0,0 +1,118 @@ +# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR +# Author: Al Udell +# Revised: November 25, 2023 + +# zwift-incline-climb-portal.py - take Zwift screenshot, crop incline, OCR incline + +# imports +import cv2 +import numpy as np +import re + +from PIL import Image, ImageGrab +import requests +import win32gui +#import time + +# Enable DPI aware on Windows +from ctypes import windll +user32 = windll.user32 +user32.SetProcessDPIAware() + +# Take Zwift screenshot - windowed mode only +hwnd = win32gui.FindWindow(None, 'Zwift') +if not hwnd: + print("Zwift is not running") + exit() +x, y, x1, y1 = win32gui.GetClientRect(hwnd) +x, y = win32gui.ClientToScreen(hwnd, (x, y)) +x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) +screenshot = ImageGrab.grab((x, y, x1, y1)) + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to incline area +screenwidth, screenheight = screenshot.size + +# Values for Zwift climb portal incline +col1 = int(screenwidth/3000 * 2822) +row1 = int(screenheight/2000 * 218) +col2 = int(screenwidth/3000 * 2980) +row2 = int(screenheight/2000 * 302) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Convert image to np array +cropped_np = np.array(cropped) + +# Convert np array to PIL +cropped_pil = Image.fromarray(cropped_np) + +# Convert PIL image to cv2 RGB +cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) + +# Convert cv2 RGB to HSV +result = cropped_cv2.copy() +image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) + +# Isolate white mask +lower = np.array([0,0,159]) +upper = np.array([0,0,255]) +mask0 = cv2.inRange(image, lower, upper) +result0 = cv2.bitwise_and(result, result, mask=mask0) + +# Isolate yellow mask +lower = np.array([24,239,241]) +upper = np.array([24,253,255]) +mask1 = cv2.inRange(image, lower, upper) +result1 = cv2.bitwise_and(result, result, mask=mask1) + +# Isolate orange mask +lower = np.array([8,191,243]) +upper = np.array([8,192,243]) +mask2 = cv2.inRange(image, lower, upper) +result2 = cv2.bitwise_and(result, result, mask=mask2) + +# Isolate red mask +lower = np.array([0,255,255]) +upper = np.array([10,255,255]) +mask3 = cv2.inRange(image, lower, upper) +result3 = cv2.bitwise_and(result, result, mask=mask3) + +# Join colour masks +mask = mask0+mask1+mask2+mask3 + +# Set output image to zero everywhere except mask +merge = image.copy() +merge[np.where(mask==0)] = 0 + +# Convert to grayscale +gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) + +# Convert to black/white by threshold +ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) + +# Apply gaussian blur +gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) + +# Write zwift image +cv2.imwrite('zwift.png', gaussianBlur, [cv2.IMWRITE_PNG_COMPRESSION, 0]) + +# OCR image +image_data = open("zwift.png","rb").read() +ocr = requests.post("http://localhost:32168/v1/image/ocr", files={"image":image_data}).json() + +# Extract label values from the 'predictions' list and merge into a single string +labels = [prediction['label'] for prediction in ocr.get('predictions', [])] +result = ''.join(labels) + +# Remove all characters that are not "-" and integers from OCR text +pattern = r"[^-\d]+" +ocr_text = re.sub(pattern, "", result) +if ocr_text: + incline = ocr_text +else: + incline = 'None' + +print(incline) diff --git a/src/windows/zwift-incline-climb-portal.py b/src/windows/zwift-incline-climb-portal.py index d904c6735..267473402 100644 --- a/src/windows/zwift-incline-climb-portal.py +++ b/src/windows/zwift-incline-climb-portal.py @@ -1,116 +1,116 @@ -# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR -# Author: Al Udell -# Revised: November 23, 2023 - -# zwift-incline-climb-portal.py - take Zwift screenshot, crop incline, OCR incline - -# imports -import cv2 -import numpy as np -import re -import win32gui -from datetime import datetime -from paddleocr import PaddleOCR -from PIL import Image, ImageGrab - -# Enable DPI aware on Windows -from ctypes import windll -user32 = windll.user32 -user32.SetProcessDPIAware() - -# Take Zwift screenshot - windowed mode only -hwnd = win32gui.FindWindow(None, 'Zwift') -if not hwnd: - print("Zwift is not running") - exit() -x, y, x1, y1 = win32gui.GetClientRect(hwnd) -x, y = win32gui.ClientToScreen(hwnd, (x, y)) -x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) -screenshot = ImageGrab.grab((x, y, x1, y1)) - -# Scale image to 3000 x 2000 -screenshot = screenshot.resize((3000, 2000)) - -# Crop image to incline area -screenwidth, screenheight = screenshot.size - -# Values for Zwift climb portal incline -col1 = int(screenwidth/3000 * 2822) -row1 = int(screenheight/2000 * 218) -col2 = int(screenwidth/3000 * 2980) -row2 = int(screenheight/2000 * 302) - -cropped = screenshot.crop((col1, row1, col2, row2)) - -# Convert image to np array -cropped_np = np.array(cropped) - -# Convert np array to PIL -cropped_pil = Image.fromarray(cropped_np) - -# Convert PIL image to cv2 RGB -cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) - -# Convert cv2 RGB to HSV -result = cropped_cv2.copy() -image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) - -# Isolate white mask -lower = np.array([0,0,159]) -upper = np.array([0,0,255]) -mask0 = cv2.inRange(image, lower, upper) -result0 = cv2.bitwise_and(result, result, mask=mask0) - -# Isolate yellow mask -lower = np.array([24,239,241]) -upper = np.array([24,253,255]) -mask1 = cv2.inRange(image, lower, upper) -result1 = cv2.bitwise_and(result, result, mask=mask1) - -# Isolate orange mask -lower = np.array([8,191,243]) -upper = np.array([8,192,243]) -mask2 = cv2.inRange(image, lower, upper) -result2 = cv2.bitwise_and(result, result, mask=mask2) - -# Isolate red mask -lower = np.array([0,255,255]) -upper = np.array([10,255,255]) -mask3 = cv2.inRange(image, lower, upper) -result3 = cv2.bitwise_and(result, result, mask=mask3) - -# Join colour masks -mask = mask0+mask1+mask2+mask3 - -# Set output image to zero everywhere except mask -merge = image.copy() -merge[np.where(mask==0)] = 0 - -# Convert to grayscale -gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) - -# Convert to black/white by threshold -ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) - -# Apply gaussian blur -gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) - -# OCR image -ocr = PaddleOCR(lang='en', use_gpu=False, show_log=False, det_db_unclip_ratio=2.0, det_db_box_thresh=0.40, drop_score=0.40, rec_algorithm='CRNN', cls_model_dir='paddleocr/ch_ppocr_mobile_v2.0_cls_infer', det_model_dir='paddleocr/en_PP-OCRv3_det_infer', rec_model_dir='paddleocr/en_PP-OCRv3_rec_infer') -result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True) - -# Extract OCR text -ocr_text = '' -for line in result: - for word in line: - ocr_text += f"{word[1][0]}" - -# Remove all characters that are not "-" and integers from OCR text -pattern = r"[^-\d]+" -ocr_text = re.sub(pattern, "", ocr_text) -if ocr_text: - incline = ocr_text -else: - incline = 'None' - -print(incline) +# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR +# Author: Al Udell +# Revised: November 23, 2023 + +# zwift-incline-climb-portal.py - take Zwift screenshot, crop incline, OCR incline + +# imports +import cv2 +import numpy as np +import re +import win32gui +from datetime import datetime +from paddleocr import PaddleOCR +from PIL import Image, ImageGrab + +# Enable DPI aware on Windows +from ctypes import windll +user32 = windll.user32 +user32.SetProcessDPIAware() + +# Take Zwift screenshot - windowed mode only +hwnd = win32gui.FindWindow(None, 'Zwift') +if not hwnd: + print("Zwift is not running") + exit() +x, y, x1, y1 = win32gui.GetClientRect(hwnd) +x, y = win32gui.ClientToScreen(hwnd, (x, y)) +x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) +screenshot = ImageGrab.grab((x, y, x1, y1)) + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to incline area +screenwidth, screenheight = screenshot.size + +# Values for Zwift climb portal incline +col1 = int(screenwidth/3000 * 2822) +row1 = int(screenheight/2000 * 218) +col2 = int(screenwidth/3000 * 2980) +row2 = int(screenheight/2000 * 302) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Convert image to np array +cropped_np = np.array(cropped) + +# Convert np array to PIL +cropped_pil = Image.fromarray(cropped_np) + +# Convert PIL image to cv2 RGB +cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) + +# Convert cv2 RGB to HSV +result = cropped_cv2.copy() +image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) + +# Isolate white mask +lower = np.array([0,0,159]) +upper = np.array([0,0,255]) +mask0 = cv2.inRange(image, lower, upper) +result0 = cv2.bitwise_and(result, result, mask=mask0) + +# Isolate yellow mask +lower = np.array([24,239,241]) +upper = np.array([24,253,255]) +mask1 = cv2.inRange(image, lower, upper) +result1 = cv2.bitwise_and(result, result, mask=mask1) + +# Isolate orange mask +lower = np.array([8,191,243]) +upper = np.array([8,192,243]) +mask2 = cv2.inRange(image, lower, upper) +result2 = cv2.bitwise_and(result, result, mask=mask2) + +# Isolate red mask +lower = np.array([0,255,255]) +upper = np.array([10,255,255]) +mask3 = cv2.inRange(image, lower, upper) +result3 = cv2.bitwise_and(result, result, mask=mask3) + +# Join colour masks +mask = mask0+mask1+mask2+mask3 + +# Set output image to zero everywhere except mask +merge = image.copy() +merge[np.where(mask==0)] = 0 + +# Convert to grayscale +gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) + +# Convert to black/white by threshold +ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) + +# Apply gaussian blur +gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) + +# OCR image +ocr = PaddleOCR(lang='en', use_gpu=False, show_log=False, det_db_unclip_ratio=2.0, det_db_box_thresh=0.40, drop_score=0.40, rec_algorithm='CRNN', cls_model_dir='paddleocr/ch_ppocr_mobile_v2.0_cls_infer', det_model_dir='paddleocr/en_PP-OCRv3_det_infer', rec_model_dir='paddleocr/en_PP-OCRv3_rec_infer') +result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True) + +# Extract OCR text +ocr_text = '' +for line in result: + for word in line: + ocr_text += f"{word[1][0]}" + +# Remove all characters that are not "-" and integers from OCR text +pattern = r"[^-\d]+" +ocr_text = re.sub(pattern, "", ocr_text) +if ocr_text: + incline = ocr_text +else: + incline = 'None' + +print(incline) diff --git a/src/windows/zwift-incline.py b/src/windows/zwift-incline.py index 1878bbd4d..1f121ca2a 100644 --- a/src/windows/zwift-incline.py +++ b/src/windows/zwift-incline.py @@ -1,116 +1,116 @@ -# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR -# Author: Al Udell -# Revised: November 23, 2023 - -# zwift-incline.py - take Zwift screenshot, crop incline, OCR incline - -# imports -import cv2 -import numpy as np -import re -import win32gui -from datetime import datetime -from paddleocr import PaddleOCR -from PIL import Image, ImageGrab - -# Enable DPI aware on Windows -from ctypes import windll -user32 = windll.user32 -user32.SetProcessDPIAware() - -# Take Zwift screenshot - windowed mode only -hwnd = win32gui.FindWindow(None, 'Zwift') -if not hwnd: - print("Zwift is not running") - exit() -x, y, x1, y1 = win32gui.GetClientRect(hwnd) -x, y = win32gui.ClientToScreen(hwnd, (x, y)) -x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) -screenshot = ImageGrab.grab((x, y, x1, y1)) - -# Scale image to 3000 x 2000 -screenshot = screenshot.resize((3000, 2000)) - -# Crop image to incline area -screenwidth, screenheight = screenshot.size - -# Values for Zwift regular incline -col1 = int(screenwidth/3000 * 2800) -row1 = int(screenheight/2000 * 90) -col2 = int(screenwidth/3000 * 2975) -row2 = int(screenheight/2000 * 195) - -cropped = screenshot.crop((col1, row1, col2, row2)) - -# Convert image to np array -cropped_np = np.array(cropped) - -# Convert np array to PIL -cropped_pil = Image.fromarray(cropped_np) - -# Convert PIL image to cv2 RGB -cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) - -# Convert cv2 RGB to HSV -result = cropped_cv2.copy() -image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) - -# Isolate white mask -lower = np.array([0,0,159]) -upper = np.array([0,0,255]) -mask0 = cv2.inRange(image, lower, upper) -result0 = cv2.bitwise_and(result, result, mask=mask0) - -# Isolate yellow mask -lower = np.array([24,239,241]) -upper = np.array([24,253,255]) -mask1 = cv2.inRange(image, lower, upper) -result1 = cv2.bitwise_and(result, result, mask=mask1) - -# Isolate orange mask -lower = np.array([8,191,243]) -upper = np.array([8,192,243]) -mask2 = cv2.inRange(image, lower, upper) -result2 = cv2.bitwise_and(result, result, mask=mask2) - -# Isolate red mask -lower = np.array([0,255,255]) -upper = np.array([10,255,255]) -mask3 = cv2.inRange(image, lower, upper) -result3 = cv2.bitwise_and(result, result, mask=mask3) - -# Join colour masks -mask = mask0+mask1+mask2+mask3 - -# Set output image to zero everywhere except mask -merge = image.copy() -merge[np.where(mask==0)] = 0 - -# Convert to grayscale -gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) - -# Convert to black/white by threshold -ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) - -# Apply gaussian blur -gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) - -# OCR image -ocr = PaddleOCR(lang='en', use_gpu=False, show_log=False, det_db_unclip_ratio=2.0, det_db_box_thresh=0.40, drop_score=0.40, rec_algorithm='CRNN', cls_model_dir='paddleocr/ch_ppocr_mobile_v2.0_cls_infer', det_model_dir='paddleocr/en_PP-OCRv3_det_infer', rec_model_dir='paddleocr/en_PP-OCRv3_rec_infer') -result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True) - -# Extract OCR text -ocr_text = '' -for line in result: - for word in line: - ocr_text += f"{word[1][0]}" - -# Remove all characters that are not "-" and integers from OCR text -pattern = r"[^-\d]+" -ocr_text = re.sub(pattern, "", ocr_text) -if ocr_text: - incline = ocr_text -else: - incline = 'None' - -print(incline) +# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR +# Author: Al Udell +# Revised: November 23, 2023 + +# zwift-incline.py - take Zwift screenshot, crop incline, OCR incline + +# imports +import cv2 +import numpy as np +import re +import win32gui +from datetime import datetime +from paddleocr import PaddleOCR +from PIL import Image, ImageGrab + +# Enable DPI aware on Windows +from ctypes import windll +user32 = windll.user32 +user32.SetProcessDPIAware() + +# Take Zwift screenshot - windowed mode only +hwnd = win32gui.FindWindow(None, 'Zwift') +if not hwnd: + print("Zwift is not running") + exit() +x, y, x1, y1 = win32gui.GetClientRect(hwnd) +x, y = win32gui.ClientToScreen(hwnd, (x, y)) +x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) +screenshot = ImageGrab.grab((x, y, x1, y1)) + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to incline area +screenwidth, screenheight = screenshot.size + +# Values for Zwift regular incline +col1 = int(screenwidth/3000 * 2800) +row1 = int(screenheight/2000 * 90) +col2 = int(screenwidth/3000 * 2975) +row2 = int(screenheight/2000 * 195) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Convert image to np array +cropped_np = np.array(cropped) + +# Convert np array to PIL +cropped_pil = Image.fromarray(cropped_np) + +# Convert PIL image to cv2 RGB +cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) + +# Convert cv2 RGB to HSV +result = cropped_cv2.copy() +image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) + +# Isolate white mask +lower = np.array([0,0,159]) +upper = np.array([0,0,255]) +mask0 = cv2.inRange(image, lower, upper) +result0 = cv2.bitwise_and(result, result, mask=mask0) + +# Isolate yellow mask +lower = np.array([24,239,241]) +upper = np.array([24,253,255]) +mask1 = cv2.inRange(image, lower, upper) +result1 = cv2.bitwise_and(result, result, mask=mask1) + +# Isolate orange mask +lower = np.array([8,191,243]) +upper = np.array([8,192,243]) +mask2 = cv2.inRange(image, lower, upper) +result2 = cv2.bitwise_and(result, result, mask=mask2) + +# Isolate red mask +lower = np.array([0,255,255]) +upper = np.array([10,255,255]) +mask3 = cv2.inRange(image, lower, upper) +result3 = cv2.bitwise_and(result, result, mask=mask3) + +# Join colour masks +mask = mask0+mask1+mask2+mask3 + +# Set output image to zero everywhere except mask +merge = image.copy() +merge[np.where(mask==0)] = 0 + +# Convert to grayscale +gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) + +# Convert to black/white by threshold +ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) + +# Apply gaussian blur +gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) + +# OCR image +ocr = PaddleOCR(lang='en', use_gpu=False, show_log=False, det_db_unclip_ratio=2.0, det_db_box_thresh=0.40, drop_score=0.40, rec_algorithm='CRNN', cls_model_dir='paddleocr/ch_ppocr_mobile_v2.0_cls_infer', det_model_dir='paddleocr/en_PP-OCRv3_det_infer', rec_model_dir='paddleocr/en_PP-OCRv3_rec_infer') +result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True) + +# Extract OCR text +ocr_text = '' +for line in result: + for word in line: + ocr_text += f"{word[1][0]}" + +# Remove all characters that are not "-" and integers from OCR text +pattern = r"[^-\d]+" +ocr_text = re.sub(pattern, "", ocr_text) +if ocr_text: + incline = ocr_text +else: + incline = 'None' + +print(incline) diff --git a/src/windows/zwift-workout-ai-server.py b/src/windows/zwift-workout-ai-server.py new file mode 100644 index 000000000..e8c86bcb9 --- /dev/null +++ b/src/windows/zwift-workout-ai-server.py @@ -0,0 +1,74 @@ +# iFit-Workout - Auto-incline and auto-speed control of treadmill via ADB and OCR for Zwift workouts +# Author: Al Udell +# Revised: November 25, 2023 + +# zwift-workout.py - take Zwift screenshot, crop speed/incline instruction, OCR speed/incline + +# imports +import cv2 +import numpy as np +import re +from PIL import Image, ImageGrab +import requests +import win32gui + +# Enable DPI aware on Windows +from ctypes import windll +user32 = windll.user32 +user32.SetProcessDPIAware() + +# Take Zwift screenshot - windowed mode only +hwnd = win32gui.FindWindow(None, 'Zwift') +if not hwnd: + print("Zwift is not running") + exit() +x, y, x1, y1 = win32gui.GetClientRect(hwnd) +x, y = win32gui.ClientToScreen(hwnd, (x, y)) +x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) +screenshot = ImageGrab.grab((x, y, x1, y1)) + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to workout instruction area +screenwidth, screenheight = screenshot.size + +# Values for Zwift workout instructions +col1 = int(screenwidth/3000 * 1010) +row1 = int(screenheight/2000 * 260) +col2 = int(screenwidth/3000 * 1285) +row2 = int(screenheight/2000 * 480) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Convert image to np array +cropped_np = np.array(cropped) + +# Write zwift image +cv2.imwrite('zwift.png', cropped_np, [cv2.IMWRITE_PNG_COMPRESSION, 0]) + +# OCR image +image_data = open("zwift.png","rb").read() +ocr = requests.post("http://localhost:32168/v1/image/ocr", files={"image":image_data}).json() + +# Extract label values from the 'predictions' list and merge into a single string +labels = [prediction['label'] for prediction in ocr.get('predictions', [])] +result = ' '.join(labels) + +# Find the speed number +if "kph" in result.lower(): + pattern = r'-?\d+(?:\.\d+)?' + numbers = re.findall(pattern, result) + speed = str(float(numbers[1])) +else: + speed = 'None' + +# Find the incline number +if "incline" in result.lower(): + pattern = r'-?\d+(?:\.\d+)?' + numbers = re.findall(pattern, result) + incline = str(float(numbers[0])) +else: + incline = 'None' + +print(speed + ";" + incline) diff --git a/src/windows/zwift-workout.py b/src/windows/zwift-workout.py index 37201e4af..5d822daac 100644 --- a/src/windows/zwift-workout.py +++ b/src/windows/zwift-workout.py @@ -1,75 +1,75 @@ -# iFit-Workout - Auto-incline and auto-speed control of treadmill via ADB and OCR for Zwift workouts -# Author: Al Udell -# Revised: November 23, 2023 - -# zwift-workout.py - take Zwift screenshot, crop speed/incline instruction, OCR speed/incline - -# imports -import cv2 -import numpy as np -import re -import win32gui -from datetime import datetime -from paddleocr import PaddleOCR -from PIL import Image, ImageGrab - -# Enable DPI aware on Windows -from ctypes import windll -user32 = windll.user32 -user32.SetProcessDPIAware() - -# Take Zwift screenshot - windowed mode only -hwnd = win32gui.FindWindow(None, 'Zwift') -if not hwnd: - print("Zwift is not running") - exit() -x, y, x1, y1 = win32gui.GetClientRect(hwnd) -x, y = win32gui.ClientToScreen(hwnd, (x, y)) -x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) -screenshot = ImageGrab.grab((x, y, x1, y1)) - -# Scale image to 3000 x 2000 -screenshot = screenshot.resize((3000, 2000)) - -# Crop image to workout instruction area -screenwidth, screenheight = screenshot.size - -# Values for Zwift workout instructions -col1 = int(screenwidth/3000 * 1010) -row1 = int(screenheight/2000 * 260) -col2 = int(screenwidth/3000 * 1285) -row2 = int(screenheight/2000 * 480) - -cropped = screenshot.crop((col1, row1, col2, row2)) - -# Convert image to np array -cropped_np = np.array(cropped) - -# OCR image -ocr = PaddleOCR(lang='en', use_gpu=False, show_log=False, det_db_unclip_ratio=2.0, det_db_box_thresh=0.40, drop_score=0.40, rec_algorithm='CRNN', cls_model_dir='paddleocr/ch_ppocr_mobile_v2.0_cls_infer', det_model_dir='paddleocr/en_PP-OCRv3_det_infer', rec_model_dir='paddleocr/en_PP-OCRv3_rec_infer') -result = ocr.ocr(cropped_np, cls=False, det=True, rec=True) - -# Extract OCR text -ocr_text = '' -for line in result: - for word in line: - ocr_text += f"{word[1][0]} " - -# Find the speed number -if "kph" in ocr_text.lower(): - pattern = r'-?\d+(?:\.\d+)?' - numbers = re.findall(pattern, ocr_text) - speed = str(float(numbers[1])) -else: - speed = 'None' - -# Find the incline number -if "incline" in ocr_text.lower(): - pattern = r'-?\d+(?:\.\d+)?' - numbers = re.findall(pattern, ocr_text) - incline = str(float(numbers[0])) -else: - incline = 'None' - -print(speed + ";" + incline) - +# iFit-Workout - Auto-incline and auto-speed control of treadmill via ADB and OCR for Zwift workouts +# Author: Al Udell +# Revised: November 23, 2023 + +# zwift-workout.py - take Zwift screenshot, crop speed/incline instruction, OCR speed/incline + +# imports +import cv2 +import numpy as np +import re +import win32gui +from datetime import datetime +from paddleocr import PaddleOCR +from PIL import Image, ImageGrab + +# Enable DPI aware on Windows +from ctypes import windll +user32 = windll.user32 +user32.SetProcessDPIAware() + +# Take Zwift screenshot - windowed mode only +hwnd = win32gui.FindWindow(None, 'Zwift') +if not hwnd: + print("Zwift is not running") + exit() +x, y, x1, y1 = win32gui.GetClientRect(hwnd) +x, y = win32gui.ClientToScreen(hwnd, (x, y)) +x1, y1 = win32gui.ClientToScreen(hwnd, (x1, y1)) +screenshot = ImageGrab.grab((x, y, x1, y1)) + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to workout instruction area +screenwidth, screenheight = screenshot.size + +# Values for Zwift workout instructions +col1 = int(screenwidth/3000 * 1010) +row1 = int(screenheight/2000 * 260) +col2 = int(screenwidth/3000 * 1285) +row2 = int(screenheight/2000 * 480) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Convert image to np array +cropped_np = np.array(cropped) + +# OCR image +ocr = PaddleOCR(lang='en', use_gpu=False, show_log=False, det_db_unclip_ratio=2.0, det_db_box_thresh=0.40, drop_score=0.40, rec_algorithm='CRNN', cls_model_dir='paddleocr/ch_ppocr_mobile_v2.0_cls_infer', det_model_dir='paddleocr/en_PP-OCRv3_det_infer', rec_model_dir='paddleocr/en_PP-OCRv3_rec_infer') +result = ocr.ocr(cropped_np, cls=False, det=True, rec=True) + +# Extract OCR text +ocr_text = '' +for line in result: + for word in line: + ocr_text += f"{word[1][0]} " + +# Find the speed number +if "kph" in ocr_text.lower(): + pattern = r'-?\d+(?:\.\d+)?' + numbers = re.findall(pattern, ocr_text) + speed = str(float(numbers[1])) +else: + speed = 'None' + +# Find the incline number +if "incline" in ocr_text.lower(): + pattern = r'-?\d+(?:\.\d+)?' + numbers = re.findall(pattern, ocr_text) + incline = str(float(numbers[0])) +else: + incline = 'None' + +print(speed + ";" + incline) + diff --git a/src/windows_zwift_incline_paddleocr_thread.cpp b/src/windows_zwift_incline_paddleocr_thread.cpp index 0541748d9..41e40287d 100644 --- a/src/windows_zwift_incline_paddleocr_thread.cpp +++ b/src/windows_zwift_incline_paddleocr_thread.cpp @@ -1,5 +1,8 @@ #include "windows_zwift_incline_paddleocr_thread.h" #include "elliptical.h" +#if __has_include("aiserver.h") +#include "aiserver.h" +#endif #include "treadmill.h" #include #include @@ -40,14 +43,18 @@ QString windows_zwift_incline_paddleocr_thread::runPython(QString command) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString currentPath = env.value("PATH"); - QString updatedPath = currentPath + ";" + QCoreApplication::applicationDirPath() + "\\python\\x64"; + QString updatedPath = currentPath + ";" + QCoreApplication::applicationDirPath() + "\\python\\x64;C:\\Program Files\\CodeProject\\AI\\modules\\OCR\\bin\\windows\\python37\\venv\\Scripts"; env.insert("PATH", updatedPath); QProcess process; process.setProcessEnvironment(env); - + //qDebug() << "env >> " << env.value("PATH"); qDebug() << "run >> " << command; +#ifndef AISERVER process.start("python\\x64\\python.exe", QStringList(command.split(' '))); +#else + process.start("C:\\Program Files\\CodeProject\\AI\\modules\\OCR\\bin\\windows\\python37\\venv\\Scripts\\python.exe", QStringList(command.split(' '))); +#endif process.waitForFinished(-1); // will wait forever until finished QString out = process.readAllStandardOutput(); diff --git a/src/windows_zwift_workout_paddleocr_thread.cpp b/src/windows_zwift_workout_paddleocr_thread.cpp index 7fc0fec77..29e116315 100644 --- a/src/windows_zwift_workout_paddleocr_thread.cpp +++ b/src/windows_zwift_workout_paddleocr_thread.cpp @@ -1,6 +1,9 @@ #include "windows_zwift_workout_paddleocr_thread.h" #include "elliptical.h" #include "treadmill.h" +#if __has_include("aiserver.h") +#include "aiserver.h" +#endif #include #include #include @@ -51,13 +54,18 @@ QString windows_zwift_workout_paddleocr_thread::runPython(QString command) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString currentPath = env.value("PATH"); - QString updatedPath = currentPath + ";" + QCoreApplication::applicationDirPath() + "\\python\\x64"; + QString updatedPath = currentPath + ";" + QCoreApplication::applicationDirPath() + "\\python\\x64;C:\\Program Files\\CodeProject\\AI\\modules\\OCR\\bin\\windows\\python37\\venv\\Scripts"; env.insert("PATH", updatedPath); QProcess process; process.setProcessEnvironment(env); + //qDebug() << "env >> " << env.value("PATH"); qDebug() << "run >> " << command; +#ifndef AISERVER process.start("python\\x64\\python.exe", QStringList(command.split(' '))); +#else + process.start("C:\\Program Files\\CodeProject\\AI\\modules\\OCR\\bin\\windows\\python37\\venv\\Scripts\\python.exe", QStringList(command.split(' '))); +#endif process.waitForFinished(-1); // will wait forever until finished QString out = process.readAllStandardOutput(); From 4f7cfbbfc17a3940e2dd1d80d3760c83ef3d91c6 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Sun, 24 Dec 2023 12:35:24 +0100 Subject: [PATCH 19/32] Floating window: adding total remaining time after the elapsed time (Issue #1863) --- .../qdomyoszwift.xcodeproj/project.pbxproj | 12 ++++++------ src/inner_templates/floating/floating.htm | 4 +++- src/templateinfosenderbuilder.cpp | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) 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 62e836d46..9fa6bc86c 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 @@ -3700,7 +3700,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 682; + CURRENT_PROJECT_VERSION = 691; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -3870,7 +3870,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 682; + CURRENT_PROJECT_VERSION = 691; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; @@ -4076,7 +4076,7 @@ CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 682; + CURRENT_PROJECT_VERSION = 691; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -4172,7 +4172,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 682; + CURRENT_PROJECT_VERSION = 691; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; @@ -4264,7 +4264,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 682; + CURRENT_PROJECT_VERSION = 691; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; ENABLE_PREVIEWS = YES; @@ -4378,7 +4378,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 682; + CURRENT_PROJECT_VERSION = 691; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; diff --git a/src/inner_templates/floating/floating.htm b/src/inner_templates/floating/floating.htm index 495133ab3..a6723a616 100644 --- a/src/inner_templates/floating/floating.htm +++ b/src/inner_templates/floating/floating.htm @@ -590,7 +590,7 @@

Peloton Workout in progress! keys_arr = ['speed', 'speed_lapavg', 'cadence', 'cadence_lapavg', 'heart', 'heart_lapavg', 'calories', 'distance', 'watts', 'watts_lapavg', 'elapsed_h', 'elapsed_m', 'elapsed_s', 'resistance', 'resistance_lapavg', 'peloton_resistance', 'peloton_resistance_lapavg', 'speed_lapmax', 'cadence_lapmax', 'heart_lapmax', 'watts_lapmax', 'resistance_lapmax', 'peloton_resistance_lapmax', - 'speed_color', 'cadence_color', 'heart_color', 'watts_color', 'peloton_resistance_color', 'target_resistance', 'target_peloton_resistance', + 'speed_color', 'power_zone_color', 'cadence_color', 'heart_color', 'watts_color', 'peloton_resistance_color', 'target_resistance', 'target_peloton_resistance', 'target_cadence', 'target_power', 'peloton_offset', 'peloton_ask_start', 'target_speed', 'target_pace', 'inclination', 'inclination_lapavg', 'inclination_lapmax', 'target_inclination', 'power_zone', 'power_zone_lapavg', 'power_zone_lapmax', 'target_power_zone', 'jouls', 'row_remaining_time_s', 'row_remaining_time_m', 'row_remaining_time_h' , 'autoresistance', 'gears', 'elevation', 'pace_s' , 'pace_m', @@ -776,6 +776,8 @@

Peloton Workout in progress! $('.watt-value').css('color', msg.content[key]); } else if (key === 'speed_color') { $('.speed-value').css('color', msg.content[key]); + } else if (key === 'power_zone_color') { + $('.powerzone-value').css('color', msg.content[key]); } else if (key === 'peloton_ask_start' && !peloton_ask_already_running && (msg.content[key] === true || msg.content[key] === 'true')) { peloton_ask_already_running = true; document.getElementById("overlay").hidden = false; diff --git a/src/templateinfosenderbuilder.cpp b/src/templateinfosenderbuilder.cpp index b23d57380..d7af4f155 100644 --- a/src/templateinfosenderbuilder.cpp +++ b/src/templateinfosenderbuilder.cpp @@ -1064,6 +1064,7 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("power_zone_lapavg"), ((bike *)device)->currentPowerZone().lapAverage()); obj.setProperty(QStringLiteral("power_zone_lapmax"), ((bike *)device)->currentPowerZone().lapMax()); obj.setProperty(QStringLiteral("target_power_zone"), ((bike *)device)->targetPowerZone().value()); + obj.setProperty(QStringLiteral("power_zone_color"), homeform::singleton()->target_zone->valueFontColor()); obj.setProperty(QStringLiteral("peloton_resistance"), (dep = ((bike *)device)->pelotonResistance()).value()); obj.setProperty(QStringLiteral("peloton_resistance_avg"), dep.average()); From 3b88bc2f1f474cec3d44be524541b4958b437f27 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Mon, 25 Dec 2023 11:42:08 +0100 Subject: [PATCH 20/32] Wahoo Dircon characteristics incompatibilities #1910 --- src/characteristicnotifier2acc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/characteristicnotifier2acc.cpp b/src/characteristicnotifier2acc.cpp index 643362aff..e58e97ce2 100644 --- a/src/characteristicnotifier2acc.cpp +++ b/src/characteristicnotifier2acc.cpp @@ -10,7 +10,7 @@ int CharacteristicNotifier2ACC::notify(QByteArray &value) { value.append((char)0x14); // heart rate and elapsed time value.append((char)0x00); value.append((char)0x00); - value.append((char)0x0C); // resistance and power target supported + value.append((char)0x0F); // resistance, power, speed and inclination target supported value.append((char)0xE0); // indoor simulation, wheel and spin down supported value.append((char)0x00); value.append((char)0x00); From 9ff91faf00fa24f18eeab96f0e8358d5e4ba320a Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Mon, 25 Dec 2023 12:30:25 +0100 Subject: [PATCH 21/32] Support for Pro-Form Cycle Trainer 300 ci (Issue #1892) --- src/proformbike.cpp | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/proformbike.cpp b/src/proformbike.cpp index bae6dd3f3..66043e9e8 100644 --- a/src/proformbike.cpp +++ b/src/proformbike.cpp @@ -1554,6 +1554,81 @@ void proformbike::btinit() { QThread::msleep(400); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); QThread::msleep(400); + + uint8_t noOpData0[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData1[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData2[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3[] = {0xfe, 0x02, 0x19, 0x03}; + uint8_t noOpData4[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData5[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData7[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, 0x0d, 0x00, 0x10, 0x00, 0xc0, 0x1c, 0x4c, 0x00, 0x00, 0xe0}; + uint8_t noOpData8[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData9[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData11[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData12[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData13[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData14[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData15[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData16[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData17[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(noOpData0, sizeof(noOpData0), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData8, sizeof(noOpData8), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData9, sizeof(noOpData9), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData10, sizeof(noOpData10), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData11, sizeof(noOpData11), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData12, sizeof(noOpData12), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData13, sizeof(noOpData13), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData14, sizeof(noOpData14), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData15, sizeof(noOpData15), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData16, sizeof(noOpData16), QStringLiteral("init"), false, false); + QThread::msleep(400); + + writeCharacteristic(noOpData17, sizeof(noOpData17), QStringLiteral("init"), false, false); + QThread::msleep(400); + + } else if (proform_hybrid_trainer_PFEL03815) { max_resistance = 16; uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x04, From 6157bfdcc897d94fc5a52e2a0b852f91a5b584fa Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Mon, 25 Dec 2023 12:54:40 +0100 Subject: [PATCH 22/32] Proform 225CSX resistance does not work (Issue #1903) --- src/proformbike.cpp | 48 ++++++++++++++++++++++++++++++++++++++++----- src/qzsettings.cpp | 4 +++- src/qzsettings.h | 3 +++ src/settings.qml | 16 ++++++++++++++- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/proformbike.cpp b/src/proformbike.cpp index 66043e9e8..f291d5206 100644 --- a/src/proformbike.cpp +++ b/src/proformbike.cpp @@ -165,6 +165,7 @@ void proformbike::forceResistance(resistance_t requestResistance) { bool proform_cycle_trainer_300_ci = settings.value(QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci) .toBool(); + bool proform_bike_225_csx = settings.value(QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx).toBool(); if (proform_studio || proform_tdf_10) { const uint8_t res1[] = {0xfe, 0x02, 0x16, 0x03}; @@ -268,7 +269,7 @@ void proformbike::forceResistance(resistance_t requestResistance) { writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true); break; } - } else if (nordictrack_gx_2_7) { + } else if (nordictrack_gx_2_7 || proform_bike_225_csx) { const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xc2, 0x01, 0x00, 0xda, 0x00, 0x00, 0x00, 0x00, 0x00}; const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, @@ -532,6 +533,7 @@ void proformbike::update() { bool proform_bike_PFEVEX71316_1 = settings.value(QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1) .toBool(); + bool proform_bike_225_csx = settings.value(QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx).toBool(); uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x00, @@ -545,6 +547,13 @@ void proformbike::update() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02}; + // proform_bike_225_csx + uint8_t noOpData2_proform_bike_225_csx[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, 0x0d, 0x3c, 0x96, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80}; + uint8_t noOpData3_proform_bike_225_csx[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x85, 0xb1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData5_proform_bike_225_csx[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x00, 0x0f, 0x80, 0x08, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6_proform_bike_225_csx[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + // proform_bike_sb uint8_t noOpData2_proform_bike_sb[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, 0x0d, 0x3c, 0x9e, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80}; @@ -612,7 +621,7 @@ void proformbike::update() { switch (counterPoll) { case 0: - if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb) { + if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_225_csx) { writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp")); } else { writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp")); @@ -627,9 +636,12 @@ void proformbike::update() { } else if (proform_hybrid_trainer_PFEL03815) { writeCharacteristic(noOpData2_proform_hybrid_trainer_PFEL03815, sizeof(noOpData2_proform_hybrid_trainer_PFEL03815), QStringLiteral("noOp")); - } else if (proform_tour_de_france_clc) + } else if (proform_tour_de_france_clc) { writeCharacteristic(noOpData2_proform_tour_de_france_clc, sizeof(noOpData2_proform_tour_de_france_clc), QStringLiteral("noOp")); + } else if (proform_bike_225_csx) + writeCharacteristic(noOpData2_proform_bike_225_csx, sizeof(noOpData2_proform_bike_225_csx), + QStringLiteral("noOp")); else if (proform_cycle_trainer_400) writeCharacteristic(noOpData2_proform_cycle_trainer_400, sizeof(noOpData2_proform_cycle_trainer_400), QStringLiteral("noOp")); @@ -654,9 +666,12 @@ void proformbike::update() { } else if (proform_hybrid_trainer_PFEL03815) { writeCharacteristic(noOpData3_proform_hybrid_trainer_PFEL03815, sizeof(noOpData3_proform_hybrid_trainer_PFEL03815), QStringLiteral("noOp")); - } else if (proform_cycle_trainer_400) + } else if (proform_cycle_trainer_400) { writeCharacteristic(noOpData3_proform_cycle_trainer_400, sizeof(noOpData3_proform_cycle_trainer_400), QStringLiteral("noOp")); + } else if (proform_bike_225_csx) + writeCharacteristic(noOpData3_proform_bike_225_csx, sizeof(noOpData3_proform_bike_225_csx), + QStringLiteral("noOp")); else if (proform_bike_sb) writeCharacteristic(noOpData3_proform_bike_sb, sizeof(noOpData3_proform_bike_sb), QStringLiteral("noOp")); @@ -679,6 +694,8 @@ void proformbike::update() { } else if (proform_bike_sb) { innerWriteResistance(); writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp")); + } else if(proform_bike_225_csx) { + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp")); } else writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp")); break; @@ -691,6 +708,9 @@ void proformbike::update() { } else if (proform_hybrid_trainer_PFEL03815) { writeCharacteristic(noOpData5_proform_hybrid_trainer_PFEL03815, sizeof(noOpData5_proform_hybrid_trainer_PFEL03815), QStringLiteral("noOp")); + } else if (proform_bike_225_csx) { + writeCharacteristic(noOpData5_proform_bike_225_csx, sizeof(noOpData5_proform_bike_225_csx), + QStringLiteral("noOp")); } else if (proform_bike_sb) writeCharacteristic(noOpData5_proform_bike_sb, sizeof(noOpData5_proform_bike_sb), QStringLiteral("noOp")); @@ -703,9 +723,14 @@ void proformbike::update() { case 5: if (proform_studio || proform_tdf_10) writeCharacteristic(noOpData6_proform_studio, sizeof(noOpData6_proform_studio), QStringLiteral("noOp")); - else if (proform_tour_de_france_clc) + else if (proform_tour_de_france_clc) { writeCharacteristic(noOpData6_proform_tour_de_france_clc, sizeof(noOpData6_proform_tour_de_france_clc), QStringLiteral("noOp")); + } else if (proform_bike_225_csx) { + writeCharacteristic(noOpData6_proform_bike_225_csx, sizeof(noOpData6_proform_bike_225_csx), + QStringLiteral("noOp")); + innerWriteResistance(); + } else if (proform_cycle_trainer_400) writeCharacteristic(noOpData6_proform_cycle_trainer_400, sizeof(noOpData6_proform_cycle_trainer_400), QStringLiteral("noOp")); @@ -857,6 +882,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte bool proform_bike_sb = settings.value(QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb).toBool(); bool proform_bike_PFEVEX71316_1 = settings.value(QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1).toBool(); + bool proform_bike_225_csx = settings.value(QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx).toBool(); emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); @@ -1290,6 +1316,7 @@ void proformbike::btinit() { .value(QZSettings::proform_hybrid_trainer_PFEL03815, QZSettings::default_proform_hybrid_trainer_PFEL03815) .toBool(); bool proform_bike_sb = settings.value(QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb).toBool(); + bool proform_bike_225_csx = settings.value(QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx).toBool(); if (settings.value(QZSettings::proform_studio, QZSettings::default_proform_studio).toBool()) { @@ -1628,7 +1655,18 @@ void proformbike::btinit() { writeCharacteristic(noOpData17, sizeof(noOpData17), QStringLiteral("init"), false, false); QThread::msleep(400); + } else if (proform_bike_225_csx) { + max_resistance = 10; + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07, 0x01, 0xd2, 0x74, 0x14, 0xb2, 0x5e, 0x08, 0xa0, 0x5e, 0x0a}; + uint8_t initData11[] = {0x01, 0x12, 0xbc, 0x6c, 0x1a, 0xc6, 0x90, 0x28, 0xe6, 0xa2, 0x64, 0x24, 0xe2, 0xae, 0x98, 0x50, 0x0e, 0xfa, 0xac, 0x9c}; + uint8_t initData12[] = {0xff, 0x08, 0x4a, 0x36, 0x20, 0x98, 0x02, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(400); } else if (proform_hybrid_trainer_PFEL03815) { max_resistance = 16; uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x04, diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index a9a8da8c9..f6cceae6f 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -686,8 +686,9 @@ const QString QZSettings::nordictrack_s20_treadmill = QStringLiteral("nordictrac const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotion_coachbike_b22_7"); const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci"); const QString QZSettings::kingsmith_encrypt_g1_walking_pad = QStringLiteral("kingsmith_encrypt_g1_walking_pad"); +const QString QZSettings::proform_bike_225_csx = QStringLiteral("proform_bike_225_csx"); -const uint32_t allSettingsCount = 576; +const uint32_t allSettingsCount = 577; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1270,6 +1271,7 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::freemotion_coachbike_b22_7, QZSettings::default_freemotion_coachbike_b22_7}, {QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci}, {QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad}, + {QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index 7ea9716b4..f736fe4be 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -1929,6 +1929,9 @@ class QZSettings { static const QString kingsmith_encrypt_g1_walking_pad; static constexpr bool default_kingsmith_encrypt_g1_walking_pad = false; + static const QString proform_bike_225_csx; + static constexpr bool default_proform_bike_225_csx = false; + /** * @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.qml b/src/settings.qml index d707dd428..05c766ccf 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -854,6 +854,7 @@ import QtQuick.Dialogs 1.0 // from version 2.16.29 property bool proform_cycle_trainer_300_ci: false property bool kingsmith_encrypt_g1_walking_pad: false + property bool proform_bike_225_csx: false } function paddingZeros(text, limit) { @@ -3017,7 +3018,20 @@ import QtQuick.Dialogs 1.0 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true onClicked: { settings.proform_cycle_trainer_400 = checked; window.settings_restart_to_apply = true; } - } + } + SwitchDelegate { + text: qsTr("Proform 225 CSX") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_bike_225_csx + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_bike_225_csx = checked; window.settings_restart_to_apply = true; } + } SwitchDelegate { text: qsTr("Proform SB") spacing: 0 From f32dcb4e1e7d4e7fd464a94f22e50fa330c5c4bb Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Mon, 25 Dec 2023 17:32:11 +0000 Subject: [PATCH 23/32] Floating window: adding total remaining time after the elapsed time (Issue #1863) --- src/templateinfosenderbuilder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templateinfosenderbuilder.cpp b/src/templateinfosenderbuilder.cpp index d7af4f155..083d098a9 100644 --- a/src/templateinfosenderbuilder.cpp +++ b/src/templateinfosenderbuilder.cpp @@ -1064,7 +1064,7 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("power_zone_lapavg"), ((bike *)device)->currentPowerZone().lapAverage()); obj.setProperty(QStringLiteral("power_zone_lapmax"), ((bike *)device)->currentPowerZone().lapMax()); obj.setProperty(QStringLiteral("target_power_zone"), ((bike *)device)->targetPowerZone().value()); - obj.setProperty(QStringLiteral("power_zone_color"), homeform::singleton()->target_zone->valueFontColor()); + obj.setProperty(QStringLiteral("power_zone_color"), homeform::singleton()->ftp->valueFontColor()); obj.setProperty(QStringLiteral("peloton_resistance"), (dep = ((bike *)device)->pelotonResistance()).value()); obj.setProperty(QStringLiteral("peloton_resistance_avg"), dep.average()); From c7efde82b6a9147cb620a98660b90edd3ff07a16 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Tue, 26 Dec 2023 08:42:47 +0100 Subject: [PATCH 24/32] Rower Distance is Not Included in Strava #1912 --- src/android/AndroidManifest.xml | 2 +- src/homeform.cpp | 3 ++- src/main.qml | 2 +- src/qdomyos-zwift.pri | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml index a49826579..3d3a38bf8 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/src/homeform.cpp b/src/homeform.cpp index 8a8e23656..5caca1c51 100644 --- a/src/homeform.cpp +++ b/src/homeform.cpp @@ -5163,7 +5163,8 @@ void homeform::update() { } } - bluetoothManager->device()->addCurrentDistance1s((bluetoothManager->device()->currentSpeed().value() / 3600.0)); + if(bluetoothManager->device()->currentSpeed().value() > 0 && !isinf(bluetoothManager->device()->currentSpeed().value())) + bluetoothManager->device()->addCurrentDistance1s((bluetoothManager->device()->currentSpeed().value() / 3600.0)); qDebug() << "Current Distance 1s:" << bluetoothManager->device()->currentDistance1s().value() << bluetoothManager->device()->currentSpeed().value(); diff --git a/src/main.qml b/src/main.qml index 13ec21611..42032efde 100644 --- a/src/main.qml +++ b/src/main.qml @@ -751,7 +751,7 @@ ApplicationWindow { } ItemDelegate { - text: "version 2.16.28" + text: "version 2.16.29" width: parent.width } diff --git a/src/qdomyos-zwift.pri b/src/qdomyos-zwift.pri index f37e96153..14a782291 100644 --- a/src/qdomyos-zwift.pri +++ b/src/qdomyos-zwift.pri @@ -819,4 +819,4 @@ INCLUDEPATH += purchasing/inapp WINRT_MANIFEST = AppxManifest.xml -VERSION = 2.16.28 +VERSION = 2.16.29 From f192cf6c75652a4fbe2a30aa2e009a46c28de9a8 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Tue, 26 Dec 2023 08:47:33 +0100 Subject: [PATCH 25/32] Proform 225CSX resistance does not work #1903 --- src/proformbike.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proformbike.cpp b/src/proformbike.cpp index f291d5206..2432d1c53 100644 --- a/src/proformbike.cpp +++ b/src/proformbike.cpp @@ -769,7 +769,7 @@ void proformbike::update() { requestResistance == -1) { // this bike sends the frame noOpData7 only when it needs to change the resistance counterPoll = 0; - } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb)) { + } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_225_csx)) { counterPoll = 0; } From 94eb86674096a59f0c5bda4fbcecb69df9f94612 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Tue, 26 Dec 2023 10:06:22 +0100 Subject: [PATCH 26/32] Nordictrack Treadmill / speed and inclination (Issue #1908) --- src/proformtreadmill.cpp | 162 ++++++++++++++++++++++++++++++++++++++- src/qzsettings.cpp | 4 +- src/qzsettings.h | 3 + src/settings.qml | 19 ++++- 4 files changed, 182 insertions(+), 6 deletions(-) diff --git a/src/proformtreadmill.cpp b/src/proformtreadmill.cpp index 3162d8f70..5e5703aae 100644 --- a/src/proformtreadmill.cpp +++ b/src/proformtreadmill.cpp @@ -75,7 +75,8 @@ void proformtreadmill::forceIncline(double incline) { bool proform_treadmill_z1300i = settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); bool nordictrack_s20_treadmill = settings.value(QZSettings::nordictrack_s20_treadmill, - QZSettings::default_nordictrack_s20_treadmill).toBool(); + QZSettings::default_nordictrack_s20_treadmill).toBool(); + bool proform_treadmill_l6_0s = settings.value(QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s).toBool(); if (proform_treadmill_1800i) { uint8_t i = abs(incline * 10); @@ -96,7 +97,7 @@ void proformtreadmill::forceIncline(double incline) { if (norditrack_s25i_treadmill) { write[14] = write[11] + write[12] + 0x11; - } else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_z1300i) { + } else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_z1300i || proform_treadmill_l6_0s) { write[14] = write[11] + write[12] + 0x12; } else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_s20_treadmill && !nordictrack_t65s_83_treadmill) { for (uint8_t i = 0; i < 7; i++) { @@ -135,6 +136,7 @@ void proformtreadmill::forceSpeed(double speed) { settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); bool nordictrack_s20_treadmill = settings.value(QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill).toBool(); + bool proform_treadmill_l6_0s = settings.value(QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s).toBool(); uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02}; uint8_t write[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x04, 0x09, 0x02, 0x01, @@ -146,7 +148,7 @@ void proformtreadmill::forceSpeed(double speed) { if (norditrack_s25i_treadmill) { write[14] = write[11] + write[12] + 0x11; } else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_cadence_lt || - proform_treadmill_z1300i) { + proform_treadmill_z1300i || proform_treadmill_l6_0s) { write[14] = write[11] + write[12] + 0x11; } else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_s20_treadmill && !nordictrack_t65s_83_treadmill) { for (uint8_t i = 0; i < 7; i++) { @@ -213,6 +215,7 @@ void proformtreadmill::update() { settings.value(QZSettings::proform_pro_1000_treadmill, QZSettings::default_proform_pro_1000_treadmill).toBool(); bool nordictrack_s20_treadmill = settings.value(QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill).toBool(); + bool proform_treadmill_l6_0s = settings.value(QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s).toBool(); // bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i, // QZSettings::default_proform_treadmill_995i).toBool(); @@ -696,6 +699,83 @@ void proformtreadmill::update() { if (counterPoll > 5) { counterPoll = 0; } + } else if (proform_treadmill_l6_0s) { + uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00, 0x0d, 0x80, 0x0a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x84, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00, 0x0d, 0x1b, 0x94, 0x31, 0x00, 0x00, 0x40, 0x50, 0x00, 0x80}; + uint8_t noOpData6[] = {0xff, 0x05, 0x18, 0x00, 0x00, 0x01, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + switch (counterPoll) { + case 0: + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp")); + break; + case 1: + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp")); + break; + case 2: + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"), false, true); + break; + case 3: + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp")); + break; + case 4: + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp")); + break; + case 5: + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp")); + if (requestInclination != -100) { + if (requestInclination < 0) + requestInclination = 0; + if (requestInclination != currentInclination().value() && requestInclination >= 0 && + requestInclination <= 15) { + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + forceIncline(requestInclination); + } + requestInclination = -100; + } + if (requestSpeed != -1) { + if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { + emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); + forceSpeed(requestSpeed); + } + requestSpeed = -1; + } + + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + uint8_t start1[] = {0xfe, 0x02, 0x20, 0x03}; + uint8_t start2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x1c, 0x04, 0x1c, 0x02, 0x09, + 0x00, 0x00, 0x40, 0x02, 0x18, 0x40, 0x00, 0x00, 0x80, 0x30}; + uint8_t start3[] = {0xff, 0x0e, 0x2a, 0x00, 0x00, 0xef, 0x1a, 0x58, 0x02, 0x00, + 0xb4, 0x00, 0x58, 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00}; + uint8_t start4[] = {0xfe, 0x02, 0x11, 0x02}; + uint8_t start5[] = {0xff, 0x11, 0x02, 0x04, 0x02, 0x0d, 0x04, 0x0d, 0x02, 0x02, + 0x03, 0x10, 0xa0, 0x00, 0x00, 0x00, 0x0a, 0x00, 0xd2, 0x00}; + writeCharacteristic(start1, sizeof(start1), QStringLiteral("start1")); + writeCharacteristic(start2, sizeof(start2), QStringLiteral("start2")); + writeCharacteristic(start3, sizeof(start3), QStringLiteral("start3"), false, true); + writeCharacteristic(start4, sizeof(start4), QStringLiteral("start4")); + writeCharacteristic(start5, sizeof(start5), QStringLiteral("start5"), false, true); + + requestStart = -1; + emit tapeStarted(); + } + if (requestStop != -1 || requestPause != -1) { + forceSpeed(0); + + emit debug(QStringLiteral("stopping...")); + requestStop = -1; + requestPause = -1; + } + break; + } + counterPoll++; + if (counterPoll > 5) { + counterPoll = 0; + } } else if (proform_treadmill_se) { uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00, @@ -1301,6 +1381,7 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); bool nordictrack_s20_treadmill = settings.value(QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill).toBool(); + bool proform_treadmill_l6_0s = settings.value(QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s).toBool(); double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); @@ -1314,7 +1395,7 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha proform_treadmill_8_0 || proform_treadmill_9_0 || nordictrack_incline_trainer_x7i) && (newValue.at(4) != 0x02 || (newValue.at(5) != 0x31 && newValue.at(5) != 0x34))) || ((norditrack_s25i_treadmill) && (newValue.at(4) != 0x02 || (newValue.at(5) != 0x2f))) || - ((nordictrack_t65s_treadmill || proform_pro_1000_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill || + ((nordictrack_t65s_treadmill || proform_treadmill_l6_0s || proform_pro_1000_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill || nordictrack_s20_treadmill || proform_treadmill_se || proform_cadence_lt) && (newValue.at(4) != 0x02 || newValue.at(5) != 0x2e)) || (((uint8_t)newValue.at(12)) == 0xFF && ((uint8_t)newValue.at(13)) == 0xFF && @@ -1414,6 +1495,7 @@ void proformtreadmill::btinit() { settings.value(QZSettings::proform_pro_1000_treadmill, QZSettings::default_proform_pro_1000_treadmill).toBool(); bool nordictrack_s20_treadmill = settings.value(QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill).toBool(); + bool proform_treadmill_l6_0s = settings.value(QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s).toBool(); // bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i, // QZSettings::default_proform_treadmill_995i).toBool(); @@ -2217,6 +2299,78 @@ void proformtreadmill::btinit() { QThread::msleep(sleepms); writeCharacteristic(noOpData10, sizeof(noOpData10), QStringLiteral("init"), false, false); QThread::msleep(sleepms); + } else if (proform_treadmill_l6_0s) { + uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; + uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x80, 0x88, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x88, 0x90, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData5[] = {0xfe, 0x02, 0x0a, 0x02}; + uint8_t initData6[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x82, 0x00, + 0x00, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData7[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00, + 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04}; + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28, 0x90, 0x04, 0x00, 0xee, 0x44, 0x90, 0xea, 0x42, 0xa8, 0xf4, 0x56, 0xb6}; + uint8_t initData11[] = {0x01, 0x12, 0x2c, 0x88, 0xe2, 0x5a, 0xd0, 0x3c, 0x8e, 0x1e, 0x94, 0xe0, 0x7a, 0xf2, 0x78, 0xc4, 0x46, 0xc6, 0x7c, 0xf8}; + uint8_t initData12[] = {0xff, 0x08, 0x72, 0xea, 0xa0, 0x80, 0x02, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00, 0x0d, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x48, 0x00, 0x00, 0xe0}; + uint8_t noOpData3[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x0c, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); } else if (nordictrack_t65s_treadmill) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index f6cceae6f..1cc2abf92 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -687,8 +687,9 @@ const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotio const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci"); const QString QZSettings::kingsmith_encrypt_g1_walking_pad = QStringLiteral("kingsmith_encrypt_g1_walking_pad"); const QString QZSettings::proform_bike_225_csx = QStringLiteral("proform_bike_225_csx"); +const QString QZSettings::proform_treadmill_l6_0s = QStringLiteral("proform_treadmill_l6_0s"); -const uint32_t allSettingsCount = 577; +const uint32_t allSettingsCount = 578; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1272,6 +1273,7 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci}, {QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad}, {QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx}, + {QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index f736fe4be..534951fe2 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -1932,6 +1932,9 @@ class QZSettings { static const QString proform_bike_225_csx; static constexpr bool default_proform_bike_225_csx = false; + static const QString proform_treadmill_l6_0s; + static constexpr bool default_proform_treadmill_l6_0s = false; + /** * @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.qml b/src/settings.qml index 05c766ccf..d816eedc2 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -855,6 +855,9 @@ import QtQuick.Dialogs 1.0 property bool proform_cycle_trainer_300_ci: false property bool kingsmith_encrypt_g1_walking_pad: false property bool proform_bike_225_csx: false + + // from version 2.16.30 + property bool proform_treadmill_l6_0s: false } function paddingZeros(text, limit) { @@ -5462,7 +5465,21 @@ import QtQuick.Dialogs 1.0 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true onClicked: {settings.proform_pro_1000_treadmill = checked; window.settings_restart_to_apply = true; } - } + } + SwitchDelegate { + text: qsTr("Nordictrack L6.0S") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_treadmill_l6_0s + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_treadmill_l6_0s = checked; window.settings_restart_to_apply = true; } + } + SwitchDelegate { id: nordictrackT65SDelegate text: qsTr("Nordictrack T6.5S v81") From 868d5cc3a8fb2135bbeb358c1ea2cb2494c9d232 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 27 Dec 2023 04:34:18 +0000 Subject: [PATCH 27/32] ProForm Gen 1 Connection Issue #1825 (#1844) --- src/QTelnet.cpp | 522 ++++++++++++++++++++++++++++++++++++++ src/QTelnet.h | 136 ++++++++++ src/bluetooth.cpp | 23 ++ src/bluetooth.h | 2 + src/proformtelnetbike.cpp | 487 +++++++++++++++++++++++++++++++++++ src/proformtelnetbike.h | 110 ++++++++ src/qdomyos-zwift.pri | 4 + src/qzsettings.cpp | 7 +- src/qzsettings.h | 3 + src/settings.qml | 24 ++ 10 files changed, 1316 insertions(+), 2 deletions(-) create mode 100644 src/QTelnet.cpp create mode 100644 src/QTelnet.h create mode 100644 src/proformtelnetbike.cpp create mode 100644 src/proformtelnetbike.h diff --git a/src/QTelnet.cpp b/src/QTelnet.cpp new file mode 100644 index 000000000..e148d2733 --- /dev/null +++ b/src/QTelnet.cpp @@ -0,0 +1,522 @@ +#include "QTelnet.h" + +#include "QTelnet.h" +#include + +const char QTelnet::IACWILL[2] = { IAC, WILL }; +const char QTelnet::IACWONT[2] = { IAC, WONT }; +const char QTelnet::IACDO[2] = { IAC, DO }; +const char QTelnet::IACDONT[2] = { IAC, DONT }; +const char QTelnet::IACSB[2] = { IAC, SB }; +const char QTelnet::IACSE[2] = { IAC, SE }; + +char QTelnet::_sendCodeArray[2] = { IAC, 0 }; +char QTelnet::_arrCRLF[2] = { 13, 10 }; +char QTelnet::_arrCR[2] = { 13, 0 }; + +QTelnet::QTelnet(QObject *parent) : + QTcpSocket(parent), m_actualSB(0) +{ + connect( this, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError)) ); + connect( this, SIGNAL(readyRead()), this, SLOT(onReadyRead()) ); +} + +QString QTelnet::peerInfo() const +{ + return QString("%1 (%2):%3").arg(peerName()).arg(peerAddress().toString()).arg(peerPort()); +} + +bool QTelnet::isConnected() const +{ + return state() == QAbstractSocket::ConnectedState; +} + +bool QTelnet::testBinaryMode() const +{ + return m_receivedDX[(unsigned char)TELOPT_BINARY] == DO; +} + +void QTelnet::connectToHost(const QString &host, quint16 port) +{ + if( !isConnected() ) + { + resetProtocol(); + abort(); + QTcpSocket::connectToHost(host, port); + } +} + +void QTelnet::sendData(const QByteArray &ba) +{ + if( isConnected() ) + transpose( ba.constData(), ba.count() ); +} + +void QTelnet::socketError(QAbstractSocket::SocketError err) +{ + Q_UNUSED(err); + disconnectFromHost(); +} + +void QTelnet::write(const char c) +{ + QTcpSocket::write( (char*)&c, 1 ); +} + +void QTelnet::setCustomCR(char cr, char cr2) +{ + _arrCR[0] = cr; + _arrCR[1] = cr2; +} + +void QTelnet::setCustomCRLF(char lf, char cr) +{ + _arrCR[0] = lf; + _arrCR[1] = cr; +} + +// Envia el codigo de control al servidor. +void QTelnet::sendTelnetControl(char codigo) +{ + _sendCodeArray[1] = codigo; + QTcpSocket::write(_sendCodeArray, 2); +} + +void QTelnet::writeCustomCRLF() +{ + QTcpSocket::write(_arrCRLF, 2); +} + +void QTelnet::writeCustomCR() +{ + QTcpSocket::write(_arrCR, 2); +} + +/// Resetea los datos del protocolo. Debe llamarse cada vez que se inicia una conexión nueva. +void QTelnet::resetProtocol() +{ + for( int i = 0; i < 256; i++ ) + { + m_receivedDX[i] = + m_receivedWX[i] = + m_sentDX[i] = + m_sentWX[i] = 0; + m_negotiationState = STATE_DATA; + m_buffSB.clear(); + m_actualSB = 0; + } + m_oldWinSize.setHeight(-1); + m_oldWinSize.setWidth(-1); +} + +void QTelnet::sendSB(char code, char *arr, int iLen) +{ + write(IAC); + write(SB); + write(code); + + QTcpSocket::write(arr, iLen); + + write(IAC); + write(SE); +} +void QTelnet::sendWindowSize() +{ + if( isConnected() && (m_receivedDX[TELOPT_NAWS] == DO) && (m_oldWinSize != m_winSize) ) + { + char size[4]; + + m_oldWinSize = m_winSize; + size[0] = (m_winSize.width()>>8) & 0xFF; + size[1] = m_winSize.width() & 0xFF; + size[2] = (m_winSize.height()>>8) & 0xFF; + size[3] = m_winSize.height() & 0xFF; + sendSB(TELOPT_NAWS, size, 4); + } +} + +// Handle an incoming IAC SB type chars IAC SE +void QTelnet::handleSB() +{ + switch( m_actualSB ) + { + case TELOPT_TTYPE: + if( (m_buffSB.count() > 0) && ((unsigned char)m_buffSB[0] == (unsigned char)TELQUAL_SEND) ) + { + QTcpSocket::write(IACSB, 2); + write(TELOPT_TTYPE); + write(TELQUAL_IS); + /* FIXME: need more logic here if we use + * more than one terminal type + */ + QTcpSocket::write("SiraggaTerminal", 15); + QTcpSocket::write(IACSE, 2); + } + break; + } +} + +// Analiza el texto saliente para que cumpla las normas del protocolo. +// Además ya lo escribe en el socket. +void QTelnet::transpose(const char *buf, int iLen) +{ + for( int i = 0; i < iLen; i++ ) + { + switch( buf[i] ) + { + case IAC: + // Escape IAC twice in stream ... to be telnet protocol compliant + // this is there in binary and non-binary mode. + write(IAC); + write(IAC); + break; + case 10: // \n + // We need to heed RFC 854. LF (\n) is 10, CR (\r) is 13 + // we assume that the Terminal sends \n for lf+cr and \r for just cr + // linefeed+carriage return is CR LF + + // En modo binario no se traduce nada. + if( testBinaryMode() ) + write(buf[i]); + else + writeCustomCRLF(); + break; + case 13: // \r + // carriage return is CR NUL */ + + // En modo binario no se traduce nada. + if( testBinaryMode() ) + write(buf[i]); + else + writeCustomCR(); + break; + default: + // all other characters are just copied + write(buf[i]); + break; + } + } +} + +void QTelnet::willsReply(char action, char reply) +{ + if( (reply != m_sentDX[(unsigned char)action]) || (WILL != m_receivedWX[(unsigned char)action]) ) + { + write(IAC); + write(reply); + write(action); + + m_sentDX[(unsigned char)action] = reply; + m_receivedWX[(unsigned char)action] = WILL; + } +} + +void QTelnet::wontsReply(char action, char reply) +{ + if( (reply != m_sentDX[(unsigned char)action]) || (WONT != m_receivedWX[(unsigned char)action]) ) + { + write(IAC); + write(reply); + write(action); + + m_sentDX[(unsigned char)action] = reply; + m_receivedWX[(unsigned char)action] = WONT; + } +} + +void QTelnet::doesReply(char action, char reply) +{ + if( (reply != m_sentWX[(unsigned char)action]) || (DO != m_receivedDX[(unsigned char)action]) ) + { + write(IAC); + write(reply); + write(action); + + m_sentWX[(unsigned char)action] = reply; + m_receivedDX[(unsigned char)action] = DO; + } +} + +void QTelnet::dontsReply(char action, char reply) +{ + if( (reply != m_sentWX[(unsigned char)action]) || (DONT != m_receivedDX[(unsigned char)action]) ) + { + write(IAC); + write(reply); + write(action); + + m_sentWX[(unsigned char)action] = reply; + m_receivedDX[(unsigned char)action] = DONT; + } +} + +// Analiza el buffer de entrada colocá ndolo en el buffer de procesado usando el protocolo telnet. +qint64 QTelnet::doTelnetInProtocol(qint64 buffSize) +{ + qint64 iIn, iOut; + char b; + + for( iIn = 0, iOut = 0; iIn < buffSize; iIn++ ) + { + b = m_buffIncoming[iIn]; + + switch( m_negotiationState ) + { + case STATE_DATA: + switch( b ) + { + case IAC: + m_negotiationState = STATE_IAC; + break; + case '\r': + m_negotiationState = STATE_DATAR; + break; + case '\n': + m_negotiationState = STATE_DATAN; + break; + default: + m_buffProcessed[iOut++] = b; + break; + } + break; + case STATE_DATAN: + case STATE_DATAR: + switch( b ) + { + case IAC: + m_negotiationState = STATE_IAC; + break; + case '\r': + case '\n': + m_buffProcessed[iOut++] = '\n'; + m_negotiationState = STATE_DATA; + break; + default: + m_buffProcessed[iOut++] = b; + m_negotiationState = STATE_DATA; + break; + } + break; + case STATE_IAC: + switch( b ) + { + case IAC: // Dos IAC seguidos, se intenta enviar un caracter con el valor IAC. + m_negotiationState = STATE_DATA; + m_buffProcessed[iOut++] = IAC; + break; + case WILL: + m_negotiationState = STATE_IACWILL; + break; + case WONT: + m_negotiationState = STATE_IACWONT; + break; + case DONT: + m_negotiationState = STATE_IACDONT; + break; + case DO: + m_negotiationState = STATE_IACDO; + break; + case EOR: + emitEndOfRecord(); + m_negotiationState = STATE_DATA; + break; + case SB: + m_negotiationState = STATE_IACSB; + m_buffSB.clear(); + break; + default: + m_negotiationState = STATE_DATA; + break; + } + break; + case STATE_IACWILL: + switch( b ) + { + case TELOPT_ECHO: + emitEchoLocal(false); + willsReply(b, DO); + break; + case TELOPT_SGA: + willsReply(b, DO); + break; + case TELOPT_EOR: + willsReply(b, DO); + break; + case TELOPT_BINARY: + willsReply(b, DO); + break; + default: + willsReply(b, DONT); + break; + } + m_negotiationState = STATE_DATA; + break; + case STATE_IACWONT: + switch(b) + { + case TELOPT_ECHO: + emitEchoLocal(true); + wontsReply(b, DONT); + break; + case TELOPT_SGA: + wontsReply(b, DONT); + break; + case TELOPT_EOR: + wontsReply(b, DONT); + break; + case TELOPT_BINARY: + wontsReply(b, DONT); + break; + default: + wontsReply(b, DONT); + break; + } + m_negotiationState = STATE_DATA; + break; + case STATE_IACDO: + switch( b ) + { + case TELOPT_ECHO: + doesReply(b, WILL); + emitEchoLocal(true); + break; + case TELOPT_SGA: + doesReply(b, WILL); + break; + case TELOPT_TTYPE: + doesReply(b, WILL); + break; + case TELOPT_BINARY: + doesReply(b, WILL); + break; + case TELOPT_NAWS: + m_receivedDX[(unsigned char)b] = (unsigned char)DO; + m_sentWX[(unsigned char)b] = (unsigned char)WILL; + write(IAC); + write(WILL); + write(b); + + // Enviamos el tamaño de la pantalla. + sendWindowSize(); + break; + default: + doesReply(b, WONT); + break; + } + m_negotiationState = STATE_DATA; + break; + case STATE_IACDONT: + switch (b) + { + case TELOPT_ECHO: + dontsReply(b, WONT); + emitEchoLocal(false); + break; + case TELOPT_SGA: + dontsReply(b, WONT); + break; + case TELOPT_NAWS: + dontsReply(b, WONT); + break; + case TELOPT_BINARY: + dontsReply(b, WONT); + break; + default: + dontsReply(b, WONT); + break; + } + m_negotiationState = STATE_DATA; + break; + case STATE_IACSB: + switch( b ) + { + case IAC: + // Entramos en estado IAC en la sub-negociación. + m_negotiationState = STATE_IACSBIAC; + break; + default: + // Iniciamos la sub-negociación. + m_buffSB.clear(); + m_actualSB = b; + m_negotiationState = STATE_IACSBDATA; + break; + } + break; + case STATE_IACSBDATA: // Estamos en datos de la subnegociación. + switch( b ) + { + case IAC: + m_negotiationState = STATE_IACSBDATAIAC; + break; + default: + m_buffSB.append(b); + break; + } + break; + case STATE_IACSBIAC: + switch( b ) + { + case IAC: + // Reiniciamos la sub-negociación. + m_buffSB.clear(); + m_actualSB = b; + m_negotiationState = STATE_IACSBDATA; + default: + // Salimos de la sub-negociación. + m_negotiationState = STATE_DATA; + } + break; + case STATE_IACSBDATAIAC: + switch( b ) + { + case IAC: + m_negotiationState = STATE_IACSBDATA; + m_buffSB.append(IAC); + break; + case SE: + handleSB(); + m_actualSB = 0; + m_buffSB.clear(); + m_negotiationState = STATE_DATA; + break; + case SB: + handleSB(); + m_buffSB.clear(); + m_negotiationState = STATE_IACSB; + break; + default: + m_buffSB.clear(); + m_actualSB = 0; + m_negotiationState = STATE_DATA; + break; + } + break; + default: + m_negotiationState = STATE_DATA; + break; + } + } + return iOut; +} + +void QTelnet::onReadyRead() +{ + qint64 readed; + qint64 processed; + + while( (readed = read(m_buffIncoming, IncommingBufferSize)) != 0 ) + { + switch( readed ) + { + case -1: + disconnectFromHost(); + break; + default: + processed = doTelnetInProtocol(readed); + if( processed > 0 ) + Q_EMIT(newData(m_buffProcessed, processed)); + + break; + } + } +} diff --git a/src/QTelnet.h b/src/QTelnet.h new file mode 100644 index 000000000..0807a8081 --- /dev/null +++ b/src/QTelnet.h @@ -0,0 +1,136 @@ +#ifndef QTELNET_H +#define QTELNET_H + +#include +#include +#include +#include + +#define IncommingBufferSize (1500) + +class QTelnet : public QTcpSocket +{ +Q_OBJECT + +public: + enum SocketStatus + { + Disconnected, + Resolving, // Resolving host + Connecting, // Connecting to host. + Connected // Connected to host. + }; + +protected: + enum TelnetStateCodes + { + STATE_DATA = (char)0, + STATE_IAC = (char)1, + STATE_IACSB = (char)2, + STATE_IACWILL = (char)3, + STATE_IACDO = (char)4, + STATE_IACWONT = (char)5, + STATE_IACDONT = (char)6, + STATE_IACSBIAC = (char)7, + STATE_IACSBDATA = (char)8, + STATE_IACSBDATAIAC = (char)9, + STATE_DATAR = (char)10, + STATE_DATAN = (char)11 + }; + enum TelnetCodes + { + // Negociación entrada/salida (cliente<->servidor) + IAC = (char)255, // Inicia la secuencia para la negociación telnet. + EOR = (char)239, // Estando en la negociación, End Of Record. + WILL = (char)251, // Estando en la negociación, Acepta el protocolo? + WONT = (char)252, // Estando en la negociación, Acepta el protocolo? + DO = (char)253, // Estando en la negociación, Protocolo aceptado. + DONT = (char)254, // Estando en la negociación, Protocolo denegado. + SB = (char)250, // Estando en la negociación, inicia secuencia de sub-negociación. + SE = (char)240, // Estando en la sub-negociación, fin de sub-negociación. + + // Negociación de salida (cliente->servidor) + TELOPT_BINARY = (char)0, // Estando en la negociación, pide modo binario. + TELOPT_ECHO = (char)1, // Estando en la negociación, pide echo local. + TELOPT_SGA = (char)2, // Estando en la negociación, pide Supress Go Ahead. + TELOPT_EOR = (char)25, // Estando en la negociación, informa End Of Record. + TELOPT_NAWS = (char)31, // Estando en la negociación, Negotiate Abaut Window Size. + TELOPT_TTYPE = (char)24 // Estando en la negociación, Terminal Type. + }; + enum TelnetQualifiers + { + TELQUAL_IS = (char)0, + TELQUAL_SEND = (char)1 + }; + +private: + static const char IACWILL[2]; + static const char IACWONT[2]; + static const char IACDO[2]; + static const char IACDONT[2]; + static const char IACSB[2]; + static const char IACSE[2]; + static char _sendCodeArray[2]; + static char _arrCRLF[2]; + static char _arrCR[2]; + + QSize m_winSize; // Tamaño de la pantalla en caracteres. + QSize m_oldWinSize; // Tamaño de la pantalla que se envió por última vez al server. Para no enviar el mismo dato. + enum TelnetStateCodes m_negotiationState; + char m_receivedDX[256]; // What IAC DO(NT) request do we have received already ? + char m_receivedWX[256]; // What IAC WILL/WONT request do we have received already ? + char m_sentDX[256]; // What IAC DO/DONT request do we have sent already ? + char m_sentWX[256]; // What IAC WILL/WONT request do we have sent already ? + void resetProtocol(); + + char m_buffIncoming[IncommingBufferSize]; + char m_buffProcessed[IncommingBufferSize]; + QByteArray m_buffSB; + int m_actualSB; + + void emitEndOfRecord() { Q_EMIT(endOfRecord()); } + void emitEchoLocal(bool bEcho) { Q_EMIT(echoLocal(bEcho)); } + + void sendTelnetControl(char codigo); + void handleSB(void); + void transpose(const char *buf, int iLen); + + void willsReply(char action, char reply); + void wontsReply(char action, char reply); + void doesReply(char action, char reply); + void dontsReply(char action, char reply); + + void sendSB(char code, char *arr, int iLen); + qint64 doTelnetInProtocol(qint64 buffSize); + +public: + explicit QTelnet(QObject *parent = 0); + + virtual void connectToHost(const QString &host, quint16 port); + void sendData(const QByteArray &ba); + void setCustomCRLF(char lf = 13, char cr = 10); + void setCustomCR(char cr = 10, char cr2 = 0); + + void writeCustomCRLF(); + void writeCustomCR(); + + void write(const char c); + + bool isConnected() const; + bool testBinaryMode() const; + void setWindSize(QSize s) {m_winSize = s;} + void sendWindowSize(); + + QString peerInfo()const; + +signals: + void newData(const char *buff, int len); + void endOfRecord(); + void echoLocal(bool echo); + +private slots: + void socketError(QAbstractSocket::SocketError err); + void onReadyRead(); +}; + +#endif // QTELNET_H diff --git a/src/bluetooth.cpp b/src/bluetooth.cpp index be29981b2..4f6e1bff1 100644 --- a/src/bluetooth.cpp +++ b/src/bluetooth.cpp @@ -399,6 +399,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool(); bool pafers_treadmill = settings.value(QZSettings::pafers_treadmill, QZSettings::default_pafers_treadmill).toBool(); QString proformtdf4ip = settings.value(QZSettings::proformtdf4ip, QZSettings::default_proformtdf4ip).toString(); + QString proformtdf1ip = settings.value(QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip).toString(); QString proformtreadmillip = settings.value(QZSettings::proformtreadmillip, QZSettings::default_proformtreadmillip).toString(); QString nordictrack_2950_ip = @@ -656,6 +657,21 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit searchingStop(); } this->signalBluetoothDeviceConnected(proformWifiBike); + } else if (!proformtdf1ip.isEmpty() && !proformTelnetBike) { + this->stopDiscovery(); + proformTelnetBike = + new proformtelnetbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + emit deviceConnected(b); + connect(proformTelnetBike, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); + connect(proformTelnetBike, &proformtelnetbike::debug, this, &bluetooth::debug); + proformTelnetBike->deviceDiscovered(b); + // connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop())); //NOTE: Commented due to #358 + if (this->discoveryAgent && !this->discoveryAgent->isActive()) { + emit searchingStop(); + } + this->signalBluetoothDeviceConnected(proformTelnetBike); #ifndef Q_OS_IOS } else if (!computrainerSerialPort.isEmpty() && !computrainerBike) { this->stopDiscovery(); @@ -2552,6 +2568,11 @@ void bluetooth::restart() { delete proformWifiBike; proformWifiBike = nullptr; } + if (proformTelnetBike) { + + delete proformTelnetBike; + proformTelnetBike = nullptr; + } if (proformWifiTreadmill) { delete proformWifiTreadmill; @@ -2965,6 +2986,8 @@ bluetoothdevice *bluetooth::device() { return cscBike; } else if (proformWifiBike) { return proformWifiBike; + } else if (proformTelnetBike) { + return proformTelnetBike; } else if (proformWifiTreadmill) { return proformWifiTreadmill; } else if (nordictrackifitadbTreadmill) { diff --git a/src/bluetooth.h b/src/bluetooth.h index a4b378bac..881186f42 100644 --- a/src/bluetooth.h +++ b/src/bluetooth.h @@ -86,6 +86,7 @@ #include "proformellipticaltrainer.h" #include "proformrower.h" #include "proformtreadmill.h" +#include "proformtelnetbike.h" #include "proformwifibike.h" #include "proformwifitreadmill.h" #include "schwinn170bike.h" @@ -187,6 +188,7 @@ class bluetooth : public QObject, public SignalHandler { pelotonbike *pelotonBike = nullptr; proformrower *proformRower = nullptr; proformbike *proformBike = nullptr; + proformtelnetbike *proformTelnetBike = nullptr; proformwifibike *proformWifiBike = nullptr; proformwifitreadmill *proformWifiTreadmill = nullptr; proformelliptical *proformElliptical = nullptr; diff --git a/src/proformtelnetbike.cpp b/src/proformtelnetbike.cpp new file mode 100644 index 000000000..ab3a490ea --- /dev/null +++ b/src/proformtelnetbike.cpp @@ -0,0 +1,487 @@ +#include "proformtelnetbike.h" +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#endif +#include "virtualbike.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +proformtelnetbike::proformtelnetbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain) { + QSettings settings; + m_watt.setType(metric::METRIC_WATT); + target_watts.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->bikeResistanceGain = bikeResistanceGain; + this->bikeResistanceOffset = bikeResistanceOffset; + initDone = false; + connect(refresh, &QTimer::timeout, this, &proformtelnetbike::update); + refresh->start(200ms); + + bool ok = connect(&telnet, &QTelnet::newData, this, &proformtelnetbike::characteristicChanged); + + ergModeSupported = true; // IMPORTANT, only for this bike + + connectToDevice(); + + initRequest = true; + + // ******************************************* virtual bike init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice() +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + && !h +#endif +#endif + ) { + QSettings settings; + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else +#endif +#endif + if (virtual_device_enabled) { + emit debug(QStringLiteral("creating virtual bike interface...")); + auto virtualBike = + new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + // connect(virtualBike,&virtualbike::debug ,this,& proformtelnetbike::debug); + connect(virtualBike, &virtualbike::changeInclination, this, &proformtelnetbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + firstStateChanged = 1; + // ******************************************************************************************************** +} + +void proformtelnetbike::connectToDevice() { + QSettings settings; + // https://github.com/dawsontoth/zwifit/blob/e846501149a6c8fbb03af8d7b9eab20474624883/src/ifit.js + telnet.connectToHost(settings.value(QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip).toString(), 23); + telnet.waitForConnected(); + telnet.sendData("./utconfig\n"); + QThread::sleep(1); + telnet.sendData("2\n"); // modify variables + +} + +/* +void proformtelnetbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, + bool wait_for_response) { + QEventLoop loop; + QTimer timeout; + if (wait_for_response) { + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } else { + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, + QByteArray((const char *)data, data_len)); + + if (!disable_log) { + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + + QStringLiteral(" // ") + info); + } + + loop.exec(); +}*/ + +resistance_t proformtelnetbike::resistanceFromPowerRequest(uint16_t power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value(); + + QSettings settings; + + double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble(); + double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); + + for (resistance_t i = 1; i < max_resistance; i++) { + if (((wattsFromResistance(i) * watt_gain) + watt_offset) <= power && + ((wattsFromResistance(i + 1) * watt_gain) + watt_offset) >= power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") + << ((wattsFromResistance(i) * watt_gain) + watt_offset) + << ((wattsFromResistance(i + 1) * watt_gain) + watt_offset) << power; + return i; + } + } + if (power < ((wattsFromResistance(1) * watt_gain) + watt_offset)) + return 1; + else + return max_resistance; +} + +uint16_t proformtelnetbike::wattsFromResistance(resistance_t resistance) { + + if (currentCadence().value() == 0) + return 0; + + switch (resistance) { + case 0: + case 1: + // -13.5 + 0.999x + 0.00993x² + return (-13.5 + (0.999 * currentCadence().value()) + (0.00993 * pow(currentCadence().value(), 2))); + case 2: + // -17.7 + 1.2x + 0.0116x² + return (-17.7 + (1.2 * currentCadence().value()) + (0.0116 * pow(currentCadence().value(), 2))); + + case 3: + // -17.5 + 1.24x + 0.014x² + return (-17.5 + (1.24 * currentCadence().value()) + (0.014 * pow(currentCadence().value(), 2))); + + case 4: + // -20.9 + 1.43x + 0.016x² + return (-20.9 + (1.43 * currentCadence().value()) + (0.016 * pow(currentCadence().value(), 2))); + + case 5: + // -27.9 + 1.75x+0.0172x² + return (-27.9 + (1.75 * currentCadence().value()) + (0.0172 * pow(currentCadence().value(), 2))); + + case 6: + // -26.7 + 1.9x + 0.0201x² + return (-26.7 + (1.9 * currentCadence().value()) + (0.0201 * pow(currentCadence().value(), 2))); + + case 7: + // -33.5 + 2.23x + 0.0225x² + return (-33.5 + (2.23 * currentCadence().value()) + (0.0225 * pow(currentCadence().value(), 2))); + + case 8: + // -36.5+2.5x+0.0262x² + return (-36.5 + (2.5 * currentCadence().value()) + (0.0262 * pow(currentCadence().value(), 2))); + + case 9: + // -38+2.62x+0.0305x² + return (-38.0 + (2.62 * currentCadence().value()) + (0.0305 * pow(currentCadence().value(), 2))); + + case 10: + // -41.2+2.85x+0.0327x² + return (-41.2 + (2.85 * currentCadence().value()) + (0.0327 * pow(currentCadence().value(), 2))); + + case 11: + // -43.4+3.01x+0.0359x² + return (-43.4 + (3.01 * currentCadence().value()) + (0.0359 * pow(currentCadence().value(), 2))); + + case 12: + // -46.8+3.23x+0.0364x² + return (-46.8 + (3.23 * currentCadence().value()) + (0.0364 * pow(currentCadence().value(), 2))); + + case 13: + // -49+3.39x+0.0371x² + return (-49.0 + (3.39 * currentCadence().value()) + (0.0371 * pow(currentCadence().value(), 2))); + + case 14: + // -53.4+3.55x+0.0383x² + return (-53.4 + (3.55 * currentCadence().value()) + (0.0383 * pow(currentCadence().value(), 2))); + + case 15: + // -49.9+3.37x+0.0429x² + return (-49.9 + (3.37 * currentCadence().value()) + (0.0429 * pow(currentCadence().value(), 2))); + + case 16: + default: + // -47.1+3.25x+0.0464x² + return (-47.1 + (3.25 * currentCadence().value()) + (0.0464 * pow(currentCadence().value(), 2))); + } +} + +void proformtelnetbike::sendFrame(QByteArray frame) { + telnet.sendData(frame); + qDebug() << " >> " << frame; +} + +void proformtelnetbike::update() { + qDebug() << "websocket.state()" << telnet.isConnected(); + + if (initRequest) { + initRequest = false; + btinit(); + emit connectedAndDiscovered(); + } else if (telnet.isConnected()) { + update_metrics(false, watts()); + + // updating the treadmill console every second + if (sec1Update++ == (500 / refresh->interval())) { + sec1Update = 0; + // updateDisplay(elapsed); + } + + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + emit bikeStarted(); + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } + } +} + +bool proformtelnetbike::inclinationAvailableByHardware() { return true; } + +resistance_t proformtelnetbike::pelotonToBikeResistance(int pelotonResistance) { + if (pelotonResistance <= 10) { + return 1; + } + if (pelotonResistance <= 20) { + return 2; + } + if (pelotonResistance <= 25) { + return 3; + } + if (pelotonResistance <= 30) { + return 4; + } + if (pelotonResistance <= 35) { + return 5; + } + if (pelotonResistance <= 40) { + return 6; + } + if (pelotonResistance <= 45) { + return 7; + } + if (pelotonResistance <= 50) { + return 8; + } + if (pelotonResistance <= 55) { + return 9; + } + if (pelotonResistance <= 60) { + return 10; + } + if (pelotonResistance <= 65) { + return 11; + } + if (pelotonResistance <= 70) { + return 12; + } + if (pelotonResistance <= 75) { + return 13; + } + if (pelotonResistance <= 80) { + return 14; + } + if (pelotonResistance <= 85) { + return 15; + } + if (pelotonResistance <= 100) { + return 16; + } + return Resistance.value(); +} + +void proformtelnetbike::serviceDiscovered(const QBluetoothUuid &gatt) { + emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); +} + +void proformtelnetbike::characteristicChanged(const char *buff, int len) { + // qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length(); + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); + + QByteArray newValue = QByteArray::fromRawData(buff, len); + emit debug(QStringLiteral(" << ") + newValue); + + if(newValue.contains("Shared Memory Management Utility")) { + emit debug(QStringLiteral("Ready to start the poll")); + sendFrame("2\n"); // current watt + } else if(newValue.contains("Enter New Value")) { + if(poolIndex >= 4) { + if(!erg_mode) { + sendFrame((QString::number(requestInclination) + "\n").toLocal8Bit()); // target incline + qDebug() << "forceInclination" << requestInclination; + requestInclination = -100; + } else { + double r = requestPower; + if (settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble() <= 2.00) { + if (settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble() != 1.0) { + qDebug() << QStringLiteral("request watt value was ") << r + << QStringLiteral("but it will be transformed to") + << r / settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble(); + } + r /= settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble(); + } + if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() < 0) { + if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() != 0.0) { + qDebug() << QStringLiteral("request watt value was ") << r + << QStringLiteral("but it will be transformed to") + << r - settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); + } + r -= settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); + } + sendFrame((QString::number(r) + "\n").toLocal8Bit()); // target watt + qDebug() << "forceWatt" << r; + requestPower = -1; + } + poolIndex = 0; + } else { + sendFrame("q\n"); // quit + } + } else if(newValue.contains("Enter Variable Offset")) { + qDebug() << "poolIndex" << poolIndex; + bool done = false; + do { + switch (poolIndex) + { + case 0: + sendFrame("124\n"); // current watt + done = true; + break; + case 1: + sendFrame("40\n"); // current rpm + done = true; + break; + case 2: + sendFrame("34\n"); // current speed + done = true; + break; + case 3: + if(!erg_mode) { + if(requestInclination != -100) { + sendFrame("45\n"); // target incline + done = true; + } + else + poolIndex = 99; + } else { + if(requestPower != -1) { + sendFrame("125\n"); // target watt + done = true; + } + else + poolIndex = 99; + } + break; + default: + break; + } + poolIndex++; + if(poolIndex > 4) + poolIndex = 0; + } while(!done); + } + + QStringList packet = QString::fromLocal8Bit(newValue).split(" "); + qDebug() << packet; + if (newValue.contains("Current Watts")) { + double watt = packet[3].toDouble(); + if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) + m_watt = watt; + emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); + } else if (newValue.contains("Cur RPM")) { + double RPM = packet[3].toDouble(); + Cadence = RPM; + emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); + + if (Cadence.value() > 0) { + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + } else if (newValue.contains("Cur KPH")) { + if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + double kph = packet[3].toDouble(); + Speed = kph; + emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); + } else { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } + } + + if (watts()) { + KCal += + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg + //* 3.5) / 200 ) / 60 + Distance += ((Speed.value() / (double)3600.0) / + ((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); + } + /* + Resistance = resistance; + m_pelotonResistance = (100 / 32) * Resistance.value(); + emit resistanceRead(Resistance.value()); */ + + /* + if (!disable_hr_frommachinery && !values[QStringLiteral("Chest Pulse")].isUndefined()) { + Heart = values[QStringLiteral("Chest Pulse")].toString().toDouble(); + // index += 1; // NOTE: clang-analyzer-deadcode.DeadStores + emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); + }*/ + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else +#endif + { + if (disable_hr_frommachinery && heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); + } + } + + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } +#endif +#endif + + /* + emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); + emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value())); + emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs)); + emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); */ +} + +void proformtelnetbike::btinit() { initDone = true; } + +void proformtelnetbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { + emit debug(QStringLiteral("Found new device: ") + device.name() + " (" + device.address().toString() + ')'); +} + +bool proformtelnetbike::connected() { return telnet.isConnected(); } + +uint16_t proformtelnetbike::watts() { return m_watt.value(); } diff --git a/src/proformtelnetbike.h b/src/proformtelnetbike.h new file mode 100644 index 000000000..c62347695 --- /dev/null +++ b/src/proformtelnetbike.h @@ -0,0 +1,110 @@ +#ifndef PROFORMTELNETBIKE_H +#define PROFORMTELNETBIKE_H + + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include + +#include "bike.h" + +#include "QTelnet.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class proformtelnetbike : public bike { + Q_OBJECT + public: + proformtelnetbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override { return max_resistance; } + bool inclinationAvailableByHardware() override; + bool connected() override; + + private: + QTelnet telnet; + resistance_t max_resistance = 100; + resistance_t min_resistance = -20; + double max_incline_supported = 20; + void connectToDevice(); + uint16_t wattsFromResistance(resistance_t resistance); + double GetDistanceFromPacket(QByteArray packet); + QTime GetElapsedFromPacket(QByteArray packet); + void btinit(); + void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, + bool wait_for_response = false); + void startDiscover(); + void sendPoll(); + uint16_t watts() override; + void sendFrame(QByteArray frame); + + QTimer *refresh; + uint8_t counterPoll = 0; + uint8_t bikeResistanceOffset = 4; + double bikeResistanceGain = 1.0; + + uint8_t sec1Update = 0; + QString lastPacket; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + metric target_watts; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + + uint8_t poolIndex = 0; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + signals: + void disconnected(); + void debug(QString string); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); + + private slots: + + void characteristicChanged(const char *buff, int len); + + void serviceDiscovered(const QBluetoothUuid &gatt); + void update(); +}; +#endif // PROFORMTELNETBIKE_H diff --git a/src/qdomyos-zwift.pri b/src/qdomyos-zwift.pri index 14a782291..f8ea47b64 100644 --- a/src/qdomyos-zwift.pri +++ b/src/qdomyos-zwift.pri @@ -72,11 +72,13 @@ DEFINES += QT_DEPRECATED_WARNINGS IO_UNDER_QT SMTP_BUILD NOMINMAX # include(../qtzeroconf/qtzeroconf.pri) SOURCES += \ + $$PWD/QTelnet.cpp \ $$PWD/bkoolbike.cpp \ $$PWD/csafe.cpp \ $$PWD/csaferower.cpp \ $$PWD/eliteariafan.cpp \ $$PWD/fakerower.cpp \ + $$PWD/proformtelnetbike.cpp \ $$PWD/virtualdevice.cpp \ $$PWD/androidactivityresultreceiver.cpp \ $$PWD/androidadblog.cpp \ @@ -281,10 +283,12 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin INCLUDEPATH += fit-sdk/ HEADERS += \ + $$PWD/QTelnet.h \ $$PWD/bkoolbike.h \ $$PWD/csafe.h \ $$PWD/csaferower.h \ $$PWD/eliteariafan.h \ + $$PWD/proformtelnetbike.h \ $$PWD/windows_zwift_workout_paddleocr_thread.h \ $$PWD/fakerower.h \ virtualdevice.h \ diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index 1cc2abf92..cf87dec50 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -686,10 +686,12 @@ const QString QZSettings::nordictrack_s20_treadmill = QStringLiteral("nordictrac const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotion_coachbike_b22_7"); const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci"); const QString QZSettings::kingsmith_encrypt_g1_walking_pad = QStringLiteral("kingsmith_encrypt_g1_walking_pad"); +const QString QZSettings::proformtdf1ip = QStringLiteral("proformtdf1ip"); +const QString QZSettings::default_proformtdf1ip = QStringLiteral(""); const QString QZSettings::proform_bike_225_csx = QStringLiteral("proform_bike_225_csx"); const QString QZSettings::proform_treadmill_l6_0s = QStringLiteral("proform_treadmill_l6_0s"); -const uint32_t allSettingsCount = 578; +const uint32_t allSettingsCount = 579; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1271,9 +1273,10 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill}, {QZSettings::freemotion_coachbike_b22_7, QZSettings::default_freemotion_coachbike_b22_7}, {QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci}, - {QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad}, + {QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad}, {QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx}, {QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s}, + {QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index 534951fe2..fae850935 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -1935,6 +1935,9 @@ class QZSettings { static const QString proform_treadmill_l6_0s; static constexpr bool default_proform_treadmill_l6_0s = false; + static const QString proformtdf1ip; + static const QString default_proformtdf1ip; + /** * @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.qml b/src/settings.qml index d816eedc2..f6916c8dd 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -858,6 +858,7 @@ import QtQuick.Dialogs 1.0 // from version 2.16.30 property bool proform_treadmill_l6_0s: false + property string proformtdf1ip: "" } function paddingZeros(text, limit) { @@ -3049,6 +3050,29 @@ import QtQuick.Dialogs 1.0 onClicked: { settings.proform_bike_sb = checked; window.settings_restart_to_apply = true; } } + RowLayout { + spacing: 10 + Label { + text: qsTr("TDF1 IP:") + Layout.fillWidth: true + } + TextField { + id: proformTDF1IPTextField + text: settings.proformtdf1ip + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + //inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.proformtdf1ip = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.proformtdf1ip = proformTDF1IPTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } + } + } + RowLayout { spacing: 10 Label { From e3be4ffa0e0378dcc2709f5e340cf4cef5ecb35c Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 27 Dec 2023 05:37:46 +0000 Subject: [PATCH 28/32] distance with 1 decimal to the summary view (#1914) --- src/inner_templates/chartjs/dochart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inner_templates/chartjs/dochart.js b/src/inner_templates/chartjs/dochart.js index beb922c88..fa475824d 100644 --- a/src/inner_templates/chartjs/dochart.js +++ b/src/inner_templates/chartjs/dochart.js @@ -177,7 +177,7 @@ function process_arr(arr) { $('.summary_watts_avg').text(Math.floor(watts_avg) + ' W'); $('.summary_jouls').text(Math.floor(jouls / 1000.0) + ' kJ'); $('.summary_calories').text(Math.floor(calories) + ' kcal'); - $('.summary_distance').text(Math.floor(distance * miles) + (miles === 1 ? ' km' : ' mi')); + $('.summary_distance').text((distance * miles).toFixed(1) + (miles === 1 ? ' km' : ' mi')); $('.summary_cadence_avg').text(Math.floor(cadence_avg) + ' rpm'); $('.summary_resistance_avg').text(Math.floor(peloton_resistance_avg) + ' lvl'); From 2c71206327b4ac5906f7673bcf1712dff63aa687 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 27 Dec 2023 07:04:33 +0100 Subject: [PATCH 29/32] Update project.pbxproj --- .../qdomyoszwift.xcodeproj/project.pbxproj | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) 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 9fa6bc86c..a0ff480fb 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 @@ -170,6 +170,10 @@ 872261F0289EA887006A6F75 /* moc_nordictrackelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872261EF289EA887006A6F75 /* moc_nordictrackelliptical.cpp */; }; 8727A47727849EA600019B5D /* paferstreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727A47627849EA600019B5D /* paferstreadmill.cpp */; }; 8727A47927849EB200019B5D /* moc_paferstreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727A47827849EB200019B5D /* moc_paferstreadmill.cpp */; }; + 8727C7D02B3BF1B8005429EB /* proformtelnetbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727C7CC2B3BF1B8005429EB /* proformtelnetbike.cpp */; }; + 8727C7D12B3BF1B8005429EB /* QTelnet.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727C7CF2B3BF1B8005429EB /* QTelnet.cpp */; }; + 8727C7D42B3BF1E4005429EB /* moc_QTelnet.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727C7D22B3BF1E4005429EB /* moc_QTelnet.cpp */; }; + 8727C7D52B3BF1E4005429EB /* moc_proformtelnetbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727C7D32B3BF1E4005429EB /* moc_proformtelnetbike.cpp */; }; 872A20DA28C5EC380037774D /* faketreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872A20D928C5EC380037774D /* faketreadmill.cpp */; }; 872A20DC28C5F5CE0037774D /* moc_faketreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872A20DB28C5F5CE0037774D /* moc_faketreadmill.cpp */; }; 872BAB4E261750EE006A59AB /* libQt5Charts.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 872BAB4D261750EE006A59AB /* libQt5Charts.a */; }; @@ -887,6 +891,12 @@ 8727A47527849EA600019B5D /* paferstreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = paferstreadmill.h; path = ../src/paferstreadmill.h; sourceTree = ""; }; 8727A47627849EA600019B5D /* paferstreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = paferstreadmill.cpp; path = ../src/paferstreadmill.cpp; sourceTree = ""; }; 8727A47827849EB200019B5D /* moc_paferstreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_paferstreadmill.cpp; sourceTree = ""; }; + 8727C7CC2B3BF1B8005429EB /* proformtelnetbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtelnetbike.cpp; path = ../src/proformtelnetbike.cpp; sourceTree = ""; }; + 8727C7CD2B3BF1B8005429EB /* proformtelnetbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtelnetbike.h; path = ../src/proformtelnetbike.h; sourceTree = ""; }; + 8727C7CE2B3BF1B8005429EB /* QTelnet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = QTelnet.h; path = ../src/QTelnet.h; sourceTree = ""; }; + 8727C7CF2B3BF1B8005429EB /* QTelnet.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = QTelnet.cpp; path = ../src/QTelnet.cpp; sourceTree = ""; }; + 8727C7D22B3BF1E4005429EB /* moc_QTelnet.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_QTelnet.cpp; sourceTree = ""; }; + 8727C7D32B3BF1E4005429EB /* moc_proformtelnetbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtelnetbike.cpp; sourceTree = ""; }; 8729149E2B2B010600565E33 /* qdomyoszwift-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "qdomyoszwift-Bridging-Header.h"; sourceTree = ""; }; 872A20D828C5EC380037774D /* faketreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = faketreadmill.h; path = ../src/faketreadmill.h; sourceTree = ""; }; 872A20D928C5EC380037774D /* faketreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = faketreadmill.cpp; path = ../src/faketreadmill.cpp; sourceTree = ""; }; @@ -1907,6 +1917,12 @@ 2EB56BE3C2D93CDAB0C52E67 /* Sources */ = { isa = PBXGroup; children = ( + 8727C7D32B3BF1E4005429EB /* moc_proformtelnetbike.cpp */, + 8727C7D22B3BF1E4005429EB /* moc_QTelnet.cpp */, + 8727C7CC2B3BF1B8005429EB /* proformtelnetbike.cpp */, + 8727C7CD2B3BF1B8005429EB /* proformtelnetbike.h */, + 8727C7CF2B3BF1B8005429EB /* QTelnet.cpp */, + 8727C7CE2B3BF1B8005429EB /* QTelnet.h */, 87A2E0212B2B053E00E6168F /* swiftDebug.mm */, 8729149E2B2B010600565E33 /* qdomyoszwift-Bridging-Header.h */, 8752C0E42B15D85600C3D1A5 /* eliteariafan.cpp */, @@ -3027,6 +3043,7 @@ 8752C0E82B15D85600C3D1A5 /* eliteariafan.cpp in Compile Sources */, 87917A7328E768D200F8D9AC /* Browser.swift in Compile Sources */, 873CD20B27EF8D8A000131BC /* inapptransaction.cpp in Compile Sources */, + 8727C7D52B3BF1E4005429EB /* moc_proformtelnetbike.cpp in Compile Sources */, 873824EF27E647A9004F1B46 /* query.cpp in Compile Sources */, 876F45FF279350D9003CDA5A /* moc_concept2skierg.cpp in Compile Sources */, BE93C6EF2C2A6BFEEC9EA565 /* fit_buffered_mesg_broadcaster.cpp in Compile Sources */, @@ -3262,6 +3279,7 @@ 873824E727E647A8004F1B46 /* record.cpp in Compile Sources */, B38F3288D4AE4025465C1953 /* moc_bike.cpp in Compile Sources */, 87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */, + 8727C7D42B3BF1E4005429EB /* moc_QTelnet.cpp in Compile Sources */, C3D1FD2587BF6F15B58BA675 /* moc_bluetooth.cpp in Compile Sources */, 87062648259480B700D06586 /* WorkoutTracking.swift in Compile Sources */, 8C3422A825EF7ECD78951307 /* moc_bluetoothdevice.cpp in Compile Sources */, @@ -3310,6 +3328,7 @@ 87D269A025F535200076AA48 /* skandikawiribike.cpp in Compile Sources */, 8738249427E646E3004F1B46 /* characteristicnotifier2a5b.cpp in Compile Sources */, 8768D1FB285081FE00F58E3A /* nordictrackifitadbtreadmill.cpp in Compile Sources */, + 8727C7D12B3BF1B8005429EB /* QTelnet.cpp in Compile Sources */, 8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */, 87B187BD29B8C577007EEF9D /* moc_ziprotreadmill.cpp in Compile Sources */, 877FBA2B276E684E00F6C0C9 /* moc_bowflextreadmill.cpp in Compile Sources */, @@ -3341,6 +3360,7 @@ 8703BAED273C67B60058E206 /* moc_pafersbike.cpp in Compile Sources */, 873824E627E647A8004F1B46 /* hostname.cpp in Compile Sources */, 74C43649C9C4E2E5F9378019 /* moc_domyosbike.cpp in Compile Sources */, + 8727C7D02B3BF1B8005429EB /* proformtelnetbike.cpp in Compile Sources */, 87E0761D277A081A00FDA0F9 /* technogymmyruntreadmillrfcomm.cpp in Compile Sources */, 873824B327E64707004F1B46 /* moc_dirconprocessor.cpp in Compile Sources */, 87A0771229B6420200A368BF /* moc_wahookickrheadwind.cpp in Compile Sources */, @@ -3700,7 +3720,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 691; + CURRENT_PROJECT_VERSION = 693; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -3870,7 +3890,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 691; + CURRENT_PROJECT_VERSION = 693; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; @@ -4076,7 +4096,7 @@ CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 691; + CURRENT_PROJECT_VERSION = 693; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -4172,7 +4192,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 691; + CURRENT_PROJECT_VERSION = 693; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; @@ -4264,7 +4284,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 691; + CURRENT_PROJECT_VERSION = 693; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; ENABLE_PREVIEWS = YES; @@ -4378,7 +4398,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 691; + CURRENT_PROJECT_VERSION = 693; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; From 76deb91b0bcbc94efeb9904f51da4157ff714f2f Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 27 Dec 2023 17:43:27 +0000 Subject: [PATCH 30/32] Update proformtelnetbike.cpp --- src/proformtelnetbike.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proformtelnetbike.cpp b/src/proformtelnetbike.cpp index ab3a490ea..f428f7cd1 100644 --- a/src/proformtelnetbike.cpp +++ b/src/proformtelnetbike.cpp @@ -411,7 +411,7 @@ void proformtelnetbike::characteristicChanged(const char *buff, int len) { } } else if (newValue.contains("Cur KPH")) { if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { - double kph = packet[3].toDouble(); + double kph = packet[3].toDouble() / 10.0; Speed = kph; emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); } else { From 2cb4187951887b4b8288b2060f30511d8d57c96a Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 28 Dec 2023 08:53:30 +0100 Subject: [PATCH 31/32] Proform 225CSX resistance does not work #1903 --- src/proformbike.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/proformbike.cpp b/src/proformbike.cpp index 2432d1c53..8dc2d2500 100644 --- a/src/proformbike.cpp +++ b/src/proformbike.cpp @@ -764,12 +764,14 @@ void proformbike::update() { counterPoll++; if (counterPoll > 6) { counterPoll = 0; + } else if(counterPoll == 6 && proform_bike_225_csx) { + counterPoll = 0; } else if (counterPoll == 6 && (proform_tour_de_france_clc || proform_cycle_trainer_400 || proform_bike_PFEVEX71316_1) && requestResistance == -1) { // this bike sends the frame noOpData7 only when it needs to change the resistance counterPoll = 0; - } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_225_csx)) { + } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb)) { counterPoll = 0; } From 48345dc2d23959e9e7344bdc00587aa9367711c7 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Thu, 28 Dec 2023 08:55:41 +0100 Subject: [PATCH 32/32] MERACH-667 bike #1920 --- src/bluetooth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bluetooth.cpp b/src/bluetooth.cpp index 4f6e1bff1..1cbc106d9 100644 --- a/src/bluetooth.cpp +++ b/src/bluetooth.cpp @@ -1355,7 +1355,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { (b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) || (b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) || (b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) || - (b.name().toUpper().startsWith("DIRETO XR")) || + (b.name().toUpper().startsWith("DIRETO XR")) || (b.name().toUpper().startsWith("MERACH-667-")) || !b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) || (b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) && !ftmsBike && !snodeBike && !fitPlusBike && !stagesBike && filter) {