mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
13 Commits
Mobi-Rower
...
build-1276
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e728ef3aa | ||
|
|
2e500e9c09 | ||
|
|
9efd62d3e9 | ||
|
|
80faa062e1 | ||
|
|
edf89fd44c | ||
|
|
51808cc8a4 | ||
|
|
802d6f58fa | ||
|
|
35115da097 | ||
|
|
c859a3e3af | ||
|
|
a8f1fc076b | ||
|
|
99dac32de4 | ||
|
|
c770ab6b80 | ||
|
|
89cd6b93e9 |
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git log:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -557,6 +557,8 @@
|
||||
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
|
||||
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
|
||||
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
|
||||
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */; };
|
||||
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */; };
|
||||
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
|
||||
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
|
||||
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
|
||||
@@ -1660,6 +1662,9 @@
|
||||
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = thinkridercontroller.h; path = ../src/devices/thinkridercontroller/thinkridercontroller.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = thinkridercontroller.cpp; path = ../src/devices/thinkridercontroller/thinkridercontroller.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_thinkridercontroller.cpp; sourceTree = "<group>"; };
|
||||
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
@@ -2335,6 +2340,9 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */,
|
||||
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */,
|
||||
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */,
|
||||
87A892572F0C173600811D95 /* sportsplusrower.cpp */,
|
||||
87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */,
|
||||
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */,
|
||||
@@ -3892,6 +3900,7 @@
|
||||
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
|
||||
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
|
||||
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
|
||||
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */,
|
||||
87097D2F275EA9A30020EE6F /* sportsplusbike.cpp in Compile Sources */,
|
||||
333C629F93DB3941862924F7 /* fit_field_base.cpp in Compile Sources */,
|
||||
87473A9827ECAA0500C203F5 /* moc_proformrower.cpp in Compile Sources */,
|
||||
@@ -4198,6 +4207,7 @@
|
||||
874D272029AFA11F0007C079 /* apexbike.cpp in Compile Sources */,
|
||||
8798C8872733E103003148B3 /* strydrunpowersensor.cpp in Compile Sources */,
|
||||
87C5F0B626285E5F0067A1B5 /* quotedprintable.cpp in Compile Sources */,
|
||||
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */,
|
||||
87310B23266FBB78008BA0D6 /* moc_smartrowrower.cpp in Compile Sources */,
|
||||
EE29228550794460E7654533 /* moc_trxappgateusbtreadmill.cpp in Compile Sources */,
|
||||
3DB7B5F0CE1E2390CEFFC1E8 /* moc_virtualbike.cpp in Compile Sources */,
|
||||
@@ -4573,7 +4583,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4774,7 +4784,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5011,7 +5021,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5107,7 +5117,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5199,7 +5209,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5315,7 +5325,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5425,7 +5435,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5516,7 +5526,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1276;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
||||
@@ -201,12 +201,15 @@ void bluetooth::finished() {
|
||||
|
||||
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
|
||||
|
||||
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
|
||||
|
||||
if ((!heartRateBeltFound && !heartRateBeltAvaiable()) || (!ftmsAccessoryFound && !ftmsAccessoryAvaiable()) ||
|
||||
(!cscFound && !cscSensorAvaiable()) || (!powerSensorFound && !powerSensorAvaiable()) ||
|
||||
(!eliteRizerFound && !eliteRizerAvaiable()) || (!eliteSterzoSmartFound && !eliteSterzoSmartAvaiable()) ||
|
||||
(!fitmetriaFanfitFound && !fitmetriaFanfitAvaiable()) ||
|
||||
(!zwiftDeviceFound && !zwiftDeviceAvaiable()) ||
|
||||
(!sramDeviceFound && !sramDeviceAvaiable())) {
|
||||
(!sramDeviceFound && !sramDeviceAvaiable()) ||
|
||||
(!thinkriderDeviceFound && !thinkriderDeviceAvaiable())) {
|
||||
|
||||
// force heartRateBelt off
|
||||
forceHeartBeltOffForTimeout = true;
|
||||
@@ -336,6 +339,16 @@ bool bluetooth::sramDeviceAvaiable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bluetooth::thinkriderDeviceAvaiable() {
|
||||
|
||||
Q_FOREACH (QBluetoothDeviceInfo b, devices) {
|
||||
if (b.name().toUpper().startsWith("VS") || b.name().toUpper().startsWith("THINKRIDER")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool bluetooth::powerSensorAvaiable() {
|
||||
|
||||
@@ -437,6 +450,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
|
||||
bool zwiftDeviceFound =
|
||||
!settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool() && !settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool();
|
||||
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
|
||||
bool fitmetriaFanfitFound =
|
||||
!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
|
||||
bool toorx_ftms = settings.value(QZSettings::toorx_ftms, QZSettings::default_toorx_ftms).toBool();
|
||||
@@ -549,6 +563,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
|
||||
sramDeviceFound = sramDeviceAvaiable();
|
||||
}
|
||||
if(!thinkriderDeviceFound) {
|
||||
|
||||
thinkriderDeviceFound = thinkriderDeviceAvaiable();
|
||||
}
|
||||
if (!ftmsAccessoryFound) {
|
||||
|
||||
ftmsAccessoryFound = ftmsAccessoryAvaiable();
|
||||
@@ -681,7 +699,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
#endif
|
||||
|
||||
bool searchDevices = (heartRateBeltFound && ftmsAccessoryFound && cscFound && powerSensorFound && eliteRizerFound &&
|
||||
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound) ||
|
||||
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound && sramDeviceFound && thinkriderDeviceFound) ||
|
||||
forceHeartBeltOffForTimeout;
|
||||
|
||||
if (searchDevices) {
|
||||
@@ -2094,15 +2112,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(lifespanTreadmill, &lifespantreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
lifespanTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(lifespanTreadmill);
|
||||
} else if (b.name().startsWith(QStringLiteral("AT-R")) && !mobiRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
mobiRower = new mobirower(noWriteResistance, noHeartService);
|
||||
emit deviceConnected(b);
|
||||
connect(mobiRower, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
mobiRower->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(mobiRower);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("ECH-ROW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROWSPORT")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROW-S"))) &&
|
||||
@@ -3124,6 +3133,24 @@ void bluetooth::connectedAndDiscovered() {
|
||||
}
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("VS")) || (b.name().toUpper().startsWith("THINKRIDER"))) && !thinkriderController && this->device() &&
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
thinkriderController = new thinkridercontroller(this->device());
|
||||
|
||||
connect(thinkriderController, &thinkridercontroller::debug, this, &bluetooth::debug);
|
||||
connect(thinkriderController, &thinkridercontroller::plus, (bike*)this->device(), &bike::gearUp);
|
||||
connect(thinkriderController, &thinkridercontroller::minus, (bike*)this->device(), &bike::gearDown);
|
||||
thinkriderController->deviceDiscovered(b);
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Thinkrider Controller Connected!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
|
||||
@@ -3614,11 +3641,6 @@ void bluetooth::restart() {
|
||||
delete echelonRower;
|
||||
echelonRower = nullptr;
|
||||
}
|
||||
if (mobiRower) {
|
||||
|
||||
delete mobiRower;
|
||||
mobiRower = nullptr;
|
||||
}
|
||||
if (echelonStride) {
|
||||
|
||||
delete echelonStride;
|
||||
@@ -4054,8 +4076,6 @@ bluetoothdevice *bluetooth::device() {
|
||||
return echelonConnectSport;
|
||||
} else if (echelonRower) {
|
||||
return echelonRower;
|
||||
} else if (mobiRower) {
|
||||
return mobiRower;
|
||||
} else if (echelonStride) {
|
||||
return echelonStride;
|
||||
} else if (echelonStairclimber) {
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
#include "zwift_play/zwiftPlayDevice.h"
|
||||
#include "zwift_play/zwiftclickremote.h"
|
||||
#include "devices/mobirower/mobirower.h"
|
||||
#include "devices/thinkridercontroller/thinkridercontroller.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
@@ -270,7 +270,6 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
echelonrower *echelonRower = nullptr;
|
||||
ftmsrower *ftmsRower = nullptr;
|
||||
smartrowrower *smartrowRower = nullptr;
|
||||
mobirower *mobiRower = nullptr;
|
||||
echelonstride *echelonStride = nullptr;
|
||||
echelonstairclimber *echelonStairclimber = nullptr;
|
||||
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
|
||||
@@ -308,6 +307,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
QList<eliteariafan *> eliteAriaFan;
|
||||
QList<zwiftclickremote* > zwiftPlayDevice;
|
||||
zwiftclickremote* zwiftClickRemote = nullptr;
|
||||
thinkridercontroller* thinkriderController = nullptr;
|
||||
sramaxscontroller* sramAXSController = nullptr;
|
||||
elitesquarecontroller* eliteSquareController = nullptr;
|
||||
QString filterDevice = QLatin1String("");
|
||||
@@ -345,6 +345,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
bool fitmetriaFanfitAvaiable();
|
||||
bool zwiftDeviceAvaiable();
|
||||
bool sramDeviceAvaiable();
|
||||
bool thinkriderDeviceAvaiable();
|
||||
bool fitmetria_fanfit_isconnected(QString name);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
#include "mobirower.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <chrono>
|
||||
#include <math.h>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
extern quint8 QZ_EnableDiscoveryCharsAndDescripttors;
|
||||
#endif
|
||||
|
||||
mobirower::mobirower(bool noWriteResistance, bool noHeartService) {
|
||||
#ifdef Q_OS_IOS
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
initDone = false;
|
||||
connect(refresh, &QTimer::timeout, this, &mobirower::update);
|
||||
refresh->start(200ms);
|
||||
}
|
||||
|
||||
void mobirower::update() {
|
||||
if (m_control == nullptr)
|
||||
return;
|
||||
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
|
||||
gattCommunicationChannelService && gattNotifyCharacteristic.isValid() && initDone) {
|
||||
update_metrics(true, watts());
|
||||
|
||||
if (requestStart != -1) {
|
||||
qDebug() << QStringLiteral("starting...");
|
||||
requestStart = -1;
|
||||
emit bikeStarted();
|
||||
}
|
||||
if (requestStop != -1) {
|
||||
qDebug() << QStringLiteral("stopping...");
|
||||
requestStop = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mobirower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
|
||||
}
|
||||
|
||||
void mobirower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
|
||||
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
|
||||
|
||||
// Validate packet: 13 bytes, starts with 0xab 0x04
|
||||
if (newValue.length() < 13 ||
|
||||
(uint8_t)newValue.at(0) != 0xab ||
|
||||
(uint8_t)newValue.at(1) != 0x04) {
|
||||
qDebug() << QStringLiteral("Invalid packet format");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse power from bytes 9-10 (big-endian uint16)
|
||||
uint16_t power = ((uint8_t)newValue.at(9) << 8) | (uint8_t)newValue.at(10);
|
||||
|
||||
// Parse stroke count from bytes 11-12 (big-endian uint16)
|
||||
uint16_t strokeCount = ((uint8_t)newValue.at(11) << 8) | (uint8_t)newValue.at(12);
|
||||
|
||||
// Calculate cadence from stroke delta
|
||||
double timeDelta = lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime());
|
||||
if (timeDelta > 0 && strokeCount >= lastStrokeCount) {
|
||||
uint16_t strokeDelta = strokeCount - lastStrokeCount;
|
||||
// Convert to strokes per minute (SPM)
|
||||
double cadence = (strokeDelta / (timeDelta / 60000.0));
|
||||
if (cadence < 200) { // sanity check
|
||||
Cadence = cadence;
|
||||
}
|
||||
}
|
||||
lastStrokeCount = strokeCount;
|
||||
|
||||
m_watt = power;
|
||||
StrokesCount = strokeCount;
|
||||
|
||||
// Calculate speed from strokes (standard rower formula)
|
||||
// Using a simplified formula: speed in km/h derived from cadence
|
||||
if (Cadence.value() > 0) {
|
||||
// Typical rower: ~10m per stroke at normal pace
|
||||
// Speed = (cadence * meters_per_stroke * 60) / 1000 for km/h
|
||||
double metersPerStroke = 8.0; // approximate
|
||||
Speed = (Cadence.value() * metersPerStroke * 60.0) / 1000.0;
|
||||
} else {
|
||||
Speed = 0;
|
||||
}
|
||||
|
||||
StrokesLength =
|
||||
((Speed.value() / 60.0) * 1000.0) /
|
||||
Cadence.value(); // this is just to fill the tile
|
||||
|
||||
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()))));
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
|
||||
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
Heart = (uint8_t)KeepAwakeHelper::heart();
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
|
||||
update_hr_from_external();
|
||||
}
|
||||
}
|
||||
|
||||
#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();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
if (ios_peloton_workaround && cadence && !virtual_device_rower && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
qDebug() << QStringLiteral("Current Power: ") + QString::number(m_watt.value());
|
||||
qDebug() << QStringLiteral("Current Stroke Count: ") + QString::number(StrokesCount.value());
|
||||
qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value());
|
||||
qDebug() << QStringLiteral("Current Cadence: ") + QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current Distance: ") + QString::number(Distance.value());
|
||||
qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts());
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
void mobirower::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
// Find the notify characteristic (0xffe4)
|
||||
QBluetoothUuid notifyCharUuid((quint16)0xffe4);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(notifyCharUuid);
|
||||
|
||||
if (!gattNotifyCharacteristic.isValid()) {
|
||||
qDebug() << QStringLiteral("gattNotifyCharacteristic not valid, trying to find by properties");
|
||||
auto characteristics_list = gattCommunicationChannelService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("c -> ") << c.uuid() << c.properties();
|
||||
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
|
||||
gattNotifyCharacteristic = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!gattNotifyCharacteristic.isValid()) {
|
||||
qDebug() << QStringLiteral("gattNotifyCharacteristic still not valid");
|
||||
return;
|
||||
}
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&mobirower::characteristicChanged);
|
||||
connect(gattCommunicationChannelService,
|
||||
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &mobirower::errorService);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&mobirower::descriptorWritten);
|
||||
|
||||
// ******************************************* virtual bike/rower 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();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).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 && !virtual_device_rower) {
|
||||
qDebug() << "ios_peloton_workaround activated!";
|
||||
h = new lockscreen();
|
||||
h->virtualbike_ios();
|
||||
} else
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_rower) {
|
||||
qDebug() << QStringLiteral("creating virtual bike interface...");
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
void mobirower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
qDebug() << QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ');
|
||||
|
||||
initDone = true;
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void mobirower::serviceScanDone(void) {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
// Service UUID 0xffe0
|
||||
QBluetoothUuid serviceUuid((quint16)0xffe0);
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(serviceUuid);
|
||||
if (!gattCommunicationChannelService) {
|
||||
qDebug() << "service 0xffe0 not found, trying to find any service";
|
||||
auto services = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services)) {
|
||||
qDebug() << QStringLiteral("service ") << s.toString();
|
||||
}
|
||||
if (!services.isEmpty()) {
|
||||
gattCommunicationChannelService = m_control->createServiceObject(services.first());
|
||||
}
|
||||
}
|
||||
|
||||
if (!gattCommunicationChannelService) {
|
||||
qDebug() << "no service found";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &mobirower::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void mobirower::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
qDebug() << QStringLiteral("mobirower::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString();
|
||||
}
|
||||
|
||||
void mobirower::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
qDebug() << "mobirower::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString();
|
||||
}
|
||||
|
||||
void mobirower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << "Found new device: " + device.name() + " (" + device.address().toString() + ')';
|
||||
bluetoothDevice = device;
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &mobirower::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &mobirower::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &mobirower::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &mobirower::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("Cannot connect to remote device.");
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("Controller connected. Search services...");
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("LowEnergy controller disconnected");
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
|
||||
bool mobirower::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
uint16_t mobirower::watts() {
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
void mobirower::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
225
src/devices/thinkridercontroller/thinkridercontroller.cpp
Normal file
225
src/devices/thinkridercontroller/thinkridercontroller.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "thinkridercontroller.h"
|
||||
#include "homeform.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Thinkrider VS200 UUIDs
|
||||
const QBluetoothUuid thinkridercontroller::SERVICE_UUID =
|
||||
QBluetoothUuid(QStringLiteral("0000fea0-0000-1000-8000-00805f9b34fb"));
|
||||
const QBluetoothUuid thinkridercontroller::CHARACTERISTIC_UUID =
|
||||
QBluetoothUuid(QStringLiteral("0000fea1-0000-1000-8000-00805f9b34fb"));
|
||||
|
||||
// Button patterns (from swiftcontrol implementation)
|
||||
const QByteArray thinkridercontroller::SHIFT_UP_PATTERN = QByteArray::fromHex("f3050301fc");
|
||||
const QByteArray thinkridercontroller::SHIFT_DOWN_PATTERN = QByteArray::fromHex("f3050300fb");
|
||||
|
||||
thinkridercontroller::thinkridercontroller(bluetoothdevice *parentDevice) {
|
||||
this->parentDevice = parentDevice;
|
||||
}
|
||||
|
||||
void thinkridercontroller::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::disconnectBluetooth() {
|
||||
qDebug() << QStringLiteral("thinkridercontroller::disconnect") << m_control;
|
||||
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
|
||||
const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
|
||||
qDebug() << QStringLiteral("thinkridercontroller << ") << newValue.toHex(' ');
|
||||
|
||||
// Check for shift up pattern
|
||||
if (newValue == SHIFT_UP_PATTERN) {
|
||||
qDebug() << QStringLiteral("Thinkrider: Shift UP detected");
|
||||
emit plus();
|
||||
}
|
||||
// Check for shift down pattern
|
||||
else if (newValue == SHIFT_DOWN_PATTERN) {
|
||||
qDebug() << QStringLiteral("Thinkrider: Shift DOWN detected");
|
||||
emit minus();
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state();
|
||||
if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) {
|
||||
qDebug() << QStringLiteral("not all services discovered");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (state != QLowEnergyService::ServiceState::ServiceDiscovered) {
|
||||
qDebug() << QStringLiteral("ignoring this state");
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("all services discovered!");
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
if (s->state() == QLowEnergyService::ServiceDiscovered) {
|
||||
// establish hook into notifications
|
||||
connect(s, &QLowEnergyService::characteristicChanged, this, &thinkridercontroller::characteristicChanged);
|
||||
connect(s, &QLowEnergyService::characteristicRead, this, &thinkridercontroller::characteristicChanged);
|
||||
connect(
|
||||
s, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &thinkridercontroller::errorService);
|
||||
connect(s, &QLowEnergyService::descriptorWritten, this, &thinkridercontroller::descriptorWritten);
|
||||
|
||||
qDebug() << s->serviceUuid() << QStringLiteral("connected!");
|
||||
|
||||
auto characteristics_list = s->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle();
|
||||
auto descriptors_list = c.descriptors();
|
||||
for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) {
|
||||
qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle();
|
||||
}
|
||||
|
||||
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
|
||||
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle()
|
||||
<< QStringLiteral(" is not valid");
|
||||
}
|
||||
|
||||
qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("notification subscribed!");
|
||||
} else if ((c.properties() & QLowEnergyCharacteristic::Indicate) ==
|
||||
QLowEnergyCharacteristic::Indicate) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x02);
|
||||
descriptor.append((char)0x00);
|
||||
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
|
||||
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle()
|
||||
<< QStringLiteral(" is not valid");
|
||||
}
|
||||
|
||||
qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("indication subscribed!");
|
||||
}
|
||||
|
||||
if (c.uuid() == CHARACTERISTIC_UUID) {
|
||||
qDebug() << QStringLiteral("Thinkrider characteristic found");
|
||||
gattNotifyCharacteristic = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void thinkridercontroller::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void thinkridercontroller::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
if (gattCommunicationChannelService.constLast()) {
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&thinkridercontroller::stateChanged);
|
||||
gattCommunicationChannelService.constLast()->discoverDetails();
|
||||
} else {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("thinkridercontroller::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("thinkridercontroller::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &thinkridercontroller::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &thinkridercontroller::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &thinkridercontroller::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &thinkridercontroller::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("LowEnergy controller disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool thinkridercontroller::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void thinkridercontroller::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
|
||||
if (m_control)
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#ifndef MOBIROWER_H
|
||||
#define MOBIROWER_H
|
||||
#ifndef THINKRIDERCONTROLLER_H
|
||||
#define THINKRIDERCONTROLLER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
@@ -22,61 +22,52 @@
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTime>
|
||||
|
||||
#include "rower.h"
|
||||
#include "devices/bluetoothdevice.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class mobirower : public rower {
|
||||
class thinkridercontroller : public bluetoothdevice {
|
||||
Q_OBJECT
|
||||
public:
|
||||
mobirower(bool noWriteResistance, bool noHeartService);
|
||||
thinkridercontroller(bluetoothdevice *parentDevice);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
void startDiscover();
|
||||
uint16_t watts() override;
|
||||
// Thinkrider VS200 UUIDs
|
||||
static const QBluetoothUuid SERVICE_UUID;
|
||||
static const QBluetoothUuid CHARACTERISTIC_UUID;
|
||||
|
||||
QTimer *refresh;
|
||||
// Button patterns
|
||||
static const QByteArray SHIFT_UP_PATTERN;
|
||||
static const QByteArray SHIFT_DOWN_PATTERN;
|
||||
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QList<QLowEnergyService *> gattCommunicationChannelService;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
|
||||
uint8_t firstStateChanged = 0;
|
||||
uint16_t lastStrokeCount = 0;
|
||||
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
bluetoothdevice *parentDevice = nullptr;
|
||||
|
||||
bool initDone = false;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
Q_SIGNALS:
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void plus();
|
||||
void minus();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
void disconnectBluetooth();
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
private slots:
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // MOBIROWER_H
|
||||
#endif // THINKRIDERCONTROLLER_H
|
||||
@@ -391,6 +391,9 @@ bool GarminConnect::fetchCsrfToken()
|
||||
bool GarminConnect::performLogin(const QString &email, const QString &password, bool suppressMfaSignal)
|
||||
{
|
||||
qDebug() << "GarminConnect: Performing login...";
|
||||
qDebug() << "GarminConnect: Using domain:" << m_domain;
|
||||
qDebug() << "GarminConnect: SSO URL:" << ssoUrl();
|
||||
qDebug() << "GarminConnect: Connect API URL:" << connectApiUrl();
|
||||
|
||||
QString ssoEmbedUrl = ssoUrl() + SSO_EMBED_PATH;
|
||||
|
||||
@@ -452,15 +455,54 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
qDebug() << "GarminConnect: Login response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(300);
|
||||
|
||||
// Check for success title (like Python garth library)
|
||||
// Check page title (like Python garth library)
|
||||
// garth checks ONLY the title for MFA detection, not the body
|
||||
// This is important because some servers (like garmin.cn) may have "MFA" text
|
||||
// in their Success page HTML body, which would cause false positives
|
||||
QString pageTitle;
|
||||
QRegularExpression titleRegex("<title>(.+?)</title>");
|
||||
QRegularExpressionMatch titleMatch = titleRegex.match(response);
|
||||
if (titleMatch.hasMatch()) {
|
||||
QString title = titleMatch.captured(1);
|
||||
qDebug() << "GarminConnect: Page title:" << title;
|
||||
if (title == "Success") {
|
||||
qDebug() << "GarminConnect: Login successful (Success page detected)";
|
||||
pageTitle = titleMatch.captured(1);
|
||||
qDebug() << "GarminConnect: Page title:" << pageTitle;
|
||||
}
|
||||
|
||||
// Check if MFA is required by looking at the TITLE (garth approach)
|
||||
// This is more reliable than checking the body which may contain "MFA" in scripts/URLs
|
||||
if (pageTitle.contains("MFA", Qt::CaseInsensitive)) {
|
||||
m_lastError = "MFA Required";
|
||||
qDebug() << "GarminConnect: MFA detected in page title";
|
||||
|
||||
// Extract new CSRF token from MFA page - try multiple patterns
|
||||
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
|
||||
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
|
||||
|
||||
QRegularExpressionMatch match = csrfRegex1.match(response);
|
||||
if (!match.hasMatch()) {
|
||||
match = csrfRegex2.match(response);
|
||||
}
|
||||
if (match.hasMatch()) {
|
||||
m_csrfToken = match.captured(1);
|
||||
qDebug() << "GarminConnect: CSRF token from MFA page:" << m_csrfToken.left(20) << "...";
|
||||
}
|
||||
|
||||
// Update cookies
|
||||
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
|
||||
|
||||
if (!suppressMfaSignal) {
|
||||
qDebug() << "GarminConnect: Emitting mfaRequired signal";
|
||||
emit mfaRequired();
|
||||
} else {
|
||||
qDebug() << "GarminConnect: MFA required but signal suppressed (retrying with MFA code)";
|
||||
}
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if login was successful (title is "Success")
|
||||
if (pageTitle == "Success") {
|
||||
qDebug() << "GarminConnect: Login successful (Success page detected)";
|
||||
// Continue to extract ticket below
|
||||
}
|
||||
|
||||
// Check for error messages in response
|
||||
@@ -549,39 +591,17 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if MFA is required (legacy check for non-redirect MFA)
|
||||
if (response.contains("MFA", Qt::CaseInsensitive) ||
|
||||
response.contains("Enter MFA Code", Qt::CaseInsensitive)) {
|
||||
m_lastError = "MFA Required";
|
||||
qDebug() << "GarminConnect: MFA content detected in response";
|
||||
|
||||
// Extract new CSRF token from MFA page - try multiple patterns
|
||||
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
|
||||
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
|
||||
|
||||
QRegularExpressionMatch match = csrfRegex1.match(response);
|
||||
if (!match.hasMatch()) {
|
||||
match = csrfRegex2.match(response);
|
||||
}
|
||||
if (match.hasMatch()) {
|
||||
m_csrfToken = match.captured(1);
|
||||
}
|
||||
|
||||
// Update cookies
|
||||
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
|
||||
|
||||
if (!suppressMfaSignal) {
|
||||
emit mfaRequired();
|
||||
}
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract ticket from response URL (already declared above)
|
||||
if (responseUrl.isEmpty()) {
|
||||
responseUrl = reply->url();
|
||||
}
|
||||
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response URL:" << responseUrl.toString();
|
||||
qDebug() << "GarminConnect: Response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Full response body:" << response;
|
||||
}
|
||||
|
||||
QUrlQuery responseQuery(responseUrl);
|
||||
QString ticket = responseQuery.queryItemValue("ticket");
|
||||
|
||||
@@ -599,6 +619,8 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
if (match.hasMatch()) {
|
||||
ticket = match.captured(1);
|
||||
qDebug() << "GarminConnect: Found ticket with fallback pattern:" << ticket.left(20) << "...";
|
||||
} else if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: No ticket patterns matched in response body";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -608,6 +630,9 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
if (ticket.isEmpty()) {
|
||||
m_lastError = "Failed to extract ticket from login response";
|
||||
qDebug() << "GarminConnect:" << m_lastError;
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -708,8 +733,12 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
qDebug() << "GarminConnect: MFA response status code:" << statusCode;
|
||||
qDebug() << "GarminConnect: MFA response redirect URL:" << responseUrl.toString();
|
||||
|
||||
// If no redirect, log response body to understand what happened
|
||||
if (responseUrl.isEmpty()) {
|
||||
// Log detailed response information
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: MFA response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Full MFA response body:" << response;
|
||||
} else if (responseUrl.isEmpty()) {
|
||||
// If no redirect, log response body to understand what happened (non-verbose)
|
||||
qDebug() << "GarminConnect: MFA response body (first 500 chars):" << response.left(500);
|
||||
}
|
||||
|
||||
@@ -748,6 +777,9 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
|
||||
// If not found in redirect URL, try response body
|
||||
if (ticket.isEmpty() && !response.isEmpty()) {
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Attempting to extract ticket from MFA response body";
|
||||
}
|
||||
// Try multiple patterns for ticket extraction
|
||||
QRegularExpression ticketRegex1("embed\\?ticket=([^\"]+)\"");
|
||||
QRegularExpression ticketRegex2("ticket=([^&\"']+)");
|
||||
@@ -761,6 +793,16 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
if (match.hasMatch()) {
|
||||
ticket = match.captured(1);
|
||||
qDebug() << "GarminConnect: Found ticket in response body (pattern 2):" << ticket.left(20) << "...";
|
||||
} else if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: No MFA ticket patterns matched. Checking for other patterns...";
|
||||
// Check for JSON format
|
||||
if (response.contains("ticket")) {
|
||||
qDebug() << "GarminConnect: Response contains 'ticket' keyword, may be JSON or different format";
|
||||
}
|
||||
// Check for common response patterns
|
||||
if (response.contains("\"")) {
|
||||
qDebug() << "GarminConnect: Response contains quoted strings (may be JSON)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,6 +812,9 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
if (ticket.isEmpty()) {
|
||||
m_lastError = "Failed to extract ticket after MFA";
|
||||
qDebug() << "GarminConnect:" << m_lastError;
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
|
||||
}
|
||||
emit authenticationFailed(m_lastError);
|
||||
return;
|
||||
}
|
||||
@@ -1401,6 +1446,7 @@ void GarminConnect::loadTokensFromSettings()
|
||||
m_oauth1Token.oauth_token = settings.value(QZSettings::garmin_oauth1_token, QZSettings::default_garmin_oauth1_token).toString();
|
||||
m_oauth1Token.oauth_token_secret = settings.value(QZSettings::garmin_oauth1_token_secret, QZSettings::default_garmin_oauth1_token_secret).toString();
|
||||
m_domain = settings.value(QZSettings::garmin_domain, QZSettings::default_garmin_domain).toString();
|
||||
qDebug() << "GarminConnect: Loaded Garmin domain from settings:" << m_domain;
|
||||
|
||||
if (!m_oauth2Token.access_token.isEmpty()) {
|
||||
qDebug() << "GarminConnect: Loaded tokens from settings (OAuth1 + OAuth2)";
|
||||
|
||||
@@ -176,6 +176,7 @@ private:
|
||||
static constexpr const char* SSO_URL_PATH = "/sso/signin";
|
||||
static constexpr const char* SSO_EMBED_PATH = "/sso/embed";
|
||||
static constexpr const char* OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json";
|
||||
static constexpr bool DEBUG_GARMIN_VERBOSE = false; // Set to true for detailed response logging (may contain sensitive data)
|
||||
|
||||
// Private methods
|
||||
QString ssoUrl() const { return QString("https://sso.%1").arg(m_domain); }
|
||||
|
||||
@@ -101,9 +101,9 @@ SOURCES += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.cpp \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.cpp \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.cpp \
|
||||
$$PWD/devices/mobirower/mobirower.cpp \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.cpp \
|
||||
$$PWD/devices/thinkridercontroller/thinkridercontroller.cpp \
|
||||
$$PWD/devices/stairclimber.cpp \
|
||||
$$PWD/devices/echelonstairclimber/echelonstairclimber.cpp \
|
||||
$$PWD/devices/technogymbike/technogymbike.cpp \
|
||||
@@ -379,9 +379,9 @@ HEADERS += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.h \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.h \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.h \
|
||||
$$PWD/devices/mobirower/mobirower.h \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.h \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.h \
|
||||
$$PWD/devices/thinkridercontroller/thinkridercontroller.h \
|
||||
$$PWD/devices/stairclimber.h \
|
||||
$$PWD/devices/technogymbike/technogymbike.h \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.h \
|
||||
|
||||
@@ -814,7 +814,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
lapMesg.SetMessageIndex(lap_index++);
|
||||
lapMesg.SetLapTrigger(FIT_LAP_TRIGGER_DISTANCE);
|
||||
if (type == JUMPROPE)
|
||||
lapMesg.SetRepetitionNum(session.at(i - 1).inclination);
|
||||
lapMesg.SetRepetitionNum(lap_index);
|
||||
lastLapTimer = sl.elapsedTime;
|
||||
lastLapOdometer = sl.distance;
|
||||
|
||||
|
||||
@@ -776,6 +776,7 @@ const QString QZSettings::proform_treadmill_505_cst = QStringLiteral("proform_tr
|
||||
const QString QZSettings::nordictrack_treadmill_t8_5s = QStringLiteral("nordictrack_treadmill_t8_5s");
|
||||
const QString QZSettings::proform_treadmill_705_cst = QStringLiteral("proform_treadmill_705_cst");
|
||||
const QString QZSettings::zwift_click = QStringLiteral("zwift_click");
|
||||
const QString QZSettings::thinkrider_controller = QStringLiteral("thinkrider_controller");
|
||||
const QString QZSettings::hop_sport_hs_090h_bike = QStringLiteral("hop_sport_hs_090h_bike");
|
||||
const QString QZSettings::zwift_play = QStringLiteral("zwift_play");
|
||||
const QString QZSettings::zwift_play_vibration = QStringLiteral("zwift_play_vibration");
|
||||
@@ -1050,7 +1051,7 @@ const QString QZSettings::trainprogram_auto_lap_on_segment = QStringLiteral("tra
|
||||
const QString QZSettings::kingsmith_r2_enable_hw_buttons = QStringLiteral("kingsmith_r2_enable_hw_buttons");
|
||||
|
||||
|
||||
const uint32_t allSettingsCount = 856;
|
||||
const uint32_t allSettingsCount = 857;
|
||||
|
||||
QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
|
||||
@@ -1696,6 +1697,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::nordictrack_treadmill_t8_5s, QZSettings::default_nordictrack_treadmill_t8_5s},
|
||||
{QZSettings::proform_treadmill_705_cst, QZSettings::default_proform_treadmill_705_cst},
|
||||
{QZSettings::zwift_click, QZSettings::default_zwift_click},
|
||||
{QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller},
|
||||
{QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike},
|
||||
{QZSettings::zwift_play, QZSettings::default_zwift_play},
|
||||
{QZSettings::zwift_play_vibration, QZSettings::default_zwift_play_vibration},
|
||||
|
||||
@@ -2140,6 +2140,9 @@ class QZSettings {
|
||||
static const QString zwift_click;
|
||||
static constexpr bool default_zwift_click = false;
|
||||
|
||||
static const QString thinkrider_controller;
|
||||
static constexpr bool default_thinkrider_controller = false;
|
||||
|
||||
static const QString proform_treadmill_705_cst;
|
||||
static constexpr bool default_proform_treadmill_705_cst = false;
|
||||
|
||||
|
||||
@@ -1273,6 +1273,7 @@ import Qt.labs.platform 1.1
|
||||
property bool kingsmith_r2_enable_hw_buttons: false
|
||||
property bool treadmill_direct_distance: false
|
||||
property bool domyos_treadmill_ts100: false
|
||||
property bool thinkrider_controller: false
|
||||
}
|
||||
|
||||
|
||||
@@ -6726,6 +6727,29 @@ import Qt.labs.platform 1.1
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
Label {
|
||||
text: qsTr("Garmin Server:")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ComboBox {
|
||||
id: garminServerComboBox
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
model: ["Global (garmin.com)", "China (garmin.cn)"]
|
||||
currentIndex: settings.garmin_domain === "garmin.cn" ? 1 : 0
|
||||
onCurrentIndexChanged: {
|
||||
var newDomain = currentIndex === 1 ? "garmin.cn" : "garmin.com";
|
||||
if (newDomain !== settings.garmin_domain) {
|
||||
rootItem.garmin_connect_logout();
|
||||
settings.garmin_domain = newDomain;
|
||||
window.settings_restart_to_apply = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Test Garmin Login"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -12744,6 +12768,43 @@ import Qt.labs.platform 1.1
|
||||
}
|
||||
}*/
|
||||
|
||||
AccordionElement {
|
||||
title: qsTr("Thinkrider Options")
|
||||
indicatRectColor: Material.color(Material.Grey)
|
||||
textColor: Material.color(Material.Yellow)
|
||||
color: Material.backgroundColor
|
||||
|
||||
accordionContent: ColumnLayout {
|
||||
spacing: 0
|
||||
IndicatorOnlySwitch {
|
||||
text: qsTr("Thinkrider Controller")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.thinkrider_controller
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: { settings.thinkrider_controller = checked; window.settings_restart_to_apply = true; }
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Thinkrider VS200 remote controller. Use it to change gears on QZ!")
|
||||
font.bold: true
|
||||
font.italic: true
|
||||
font.pixelSize: Qt.application.font.pixelSize - 2
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccordionElement {
|
||||
title: qsTr("Zwift Devices Options")
|
||||
indicatRectColor: Material.color(Material.Grey)
|
||||
|
||||
Reference in New Issue
Block a user