mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
26 Commits
Mobi-Rower
...
1280
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d4f266ccf | ||
|
|
aee47698f8 | ||
|
|
5aa2a310d3 | ||
|
|
8816fd105a | ||
|
|
4fb046d9dc | ||
|
|
1578e25aca | ||
|
|
bceabd916a | ||
|
|
009c806189 | ||
|
|
c22ee74ff4 | ||
|
|
0772026b7b | ||
|
|
5bd74260ab | ||
|
|
35f7ab636e | ||
|
|
4d8fd1ce1a | ||
|
|
b2a28d71e4 | ||
|
|
2db5683dd2 | ||
|
|
3b81b6d4ee | ||
|
|
2e0bd25a4a | ||
|
|
323c169067 | ||
|
|
1ff9da34db | ||
|
|
361874f1ea | ||
|
|
33b686bf3e | ||
|
|
74e1aba909 | ||
|
|
bf75b2bda0 | ||
|
|
80faa062e1 | ||
|
|
51808cc8a4 | ||
|
|
eb123d5d8c |
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(tshark:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(git log:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
50
CLAUDE.md
50
CLAUDE.md
@@ -368,7 +368,55 @@ The ProForm 995i implementation serves as the reference example:
|
||||
- Test device detection thoroughly using the existing test infrastructure
|
||||
- Consider platform differences when adding new features
|
||||
|
||||
## Updating Version Numbers
|
||||
|
||||
When releasing a new version of QDomyos-Zwift, you must update the version number in **3 files**:
|
||||
|
||||
### 1. Android Manifest
|
||||
**File**: `src/android/AndroidManifest.xml`
|
||||
|
||||
Update both `versionName` and `versionCode`:
|
||||
```xml
|
||||
<manifest ... android:versionName="X.XX.XX" android:versionCode="XXXX" ...>
|
||||
```
|
||||
|
||||
- `versionName`: The human-readable version (e.g., "2.20.26")
|
||||
- `versionCode`: Integer build number that must be incremented (e.g., 1274)
|
||||
|
||||
### 2. Main QML File
|
||||
**File**: `src/main.qml`
|
||||
|
||||
Update the version text displayed in the UI (around line 938):
|
||||
```qml
|
||||
ItemDelegate {
|
||||
text: "version X.XX.XX"
|
||||
width: parent.width
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Qt Project Include File
|
||||
**File**: `src/qdomyos-zwift.pri`
|
||||
|
||||
Update the VERSION variable (around line 1011):
|
||||
```pri
|
||||
VERSION = X.XX.XX
|
||||
```
|
||||
|
||||
### Version Numbering Convention
|
||||
|
||||
- **Major.Minor.Patch** format (e.g., 2.20.26)
|
||||
- **Build number** must always increment, never reuse
|
||||
- Update all 3 files together to keep versions synchronized
|
||||
|
||||
### iOS Version (Optional)
|
||||
|
||||
iOS version is managed through Xcode project variables:
|
||||
- `MARKETING_VERSION` in project.pbxproj (corresponds to versionName)
|
||||
- `CURRENT_PROJECT_VERSION` in project.pbxproj (corresponds to versionCode)
|
||||
|
||||
These are typically updated via Xcode IDE rather than manually editing files.
|
||||
|
||||
## Additional Memories
|
||||
|
||||
- When adding a new setting in QML (setting-tiles.qml), you must:
|
||||
* Add the property at the END of the properties list
|
||||
* Add the property at the END of the properties list
|
||||
@@ -557,6 +557,10 @@
|
||||
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 */; };
|
||||
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */; };
|
||||
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.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 +1664,12 @@
|
||||
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>"; };
|
||||
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sunnyfitstepper.cpp; sourceTree = "<group>"; };
|
||||
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sunnyfitstepper.h; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sunnyfitstepper.cpp; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.cpp; sourceTree = SOURCE_ROOT; };
|
||||
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 +2345,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 */,
|
||||
@@ -2883,6 +2896,9 @@
|
||||
87F1BD652DBFBCE700416506 /* android_antbike.h */,
|
||||
87F1BD662DBFBCE700416506 /* android_antbike.cpp */,
|
||||
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */,
|
||||
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */,
|
||||
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */,
|
||||
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */,
|
||||
);
|
||||
name = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -3892,6 +3908,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 */,
|
||||
@@ -4134,6 +4151,8 @@
|
||||
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */,
|
||||
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */,
|
||||
87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */,
|
||||
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */,
|
||||
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */,
|
||||
877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */,
|
||||
877758B62C98629B00BB1697 /* sportstechelliptical.cpp in Compile Sources */,
|
||||
8762D5102601F7EA00F6F049 /* M3iNSQT.cpp in Compile Sources */,
|
||||
@@ -4198,6 +4217,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 +4593,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 = 1280;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4774,7 +4794,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 = 1280;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5011,7 +5031,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 = 1280;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5107,7 +5127,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 = 1280;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5199,7 +5219,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 = 1280;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5315,7 +5335,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 = 1280;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5425,7 +5445,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1280;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5516,7 +5536,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1280;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
||||
@@ -34,6 +34,7 @@ Page {
|
||||
property string heart_rate_belt_name: "Disabled"
|
||||
property bool garmin_companion: false
|
||||
property string filter_device: "Disabled"
|
||||
property bool weight_kg_unit: false
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
@@ -1181,7 +1182,7 @@ Page {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Weight (" + (settings.miles_unit ? "lbs" : "kg") + ")")
|
||||
text: qsTr("Weight (" + ((settings.miles_unit && !settings.weight_kg_unit) ? "lbs" : "kg") + ")")
|
||||
font.pixelSize: 20
|
||||
color: "white"
|
||||
}
|
||||
@@ -1189,13 +1190,13 @@ Page {
|
||||
SpinBox {
|
||||
id: weightSpinBox
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: settings.miles_unit ? 660 : 300 // 66.0 lbs or 30.0 kg
|
||||
to: settings.miles_unit ? 4400 : 2000 // 440.0 lbs or 200.0 kg
|
||||
value: settings.miles_unit ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
|
||||
from: (settings.miles_unit && !settings.weight_kg_unit) ? 660 : 300 // 66.0 lbs or 30.0 kg
|
||||
to: (settings.miles_unit && !settings.weight_kg_unit) ? 4400 : 2000 // 440.0 lbs or 200.0 kg
|
||||
value: (settings.miles_unit && !settings.weight_kg_unit) ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
|
||||
stepSize: 1
|
||||
editable: true
|
||||
|
||||
property real realValue: settings.miles_unit ? value / 22.0462 : value / 10
|
||||
property real realValue: (settings.miles_unit && !settings.weight_kg_unit) ? value / 22.0462 : value / 10
|
||||
|
||||
textFromValue: function(value, locale) {
|
||||
return Number(value / 10).toLocaleString(locale, 'f', 1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.23" android:versionCode="1264" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.26" android:versionCode="1274" android:installLocation="auto">
|
||||
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default permissions. -->
|
||||
<!-- %%INSERT_PERMISSIONS -->
|
||||
|
||||
@@ -57,6 +57,7 @@ dependencies {
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.60'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
|
||||
implementation("com.garmin.connectiq:ciq-companion-app-sdk:2.2.0@aar")
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
Binary file not shown.
@@ -82,10 +82,13 @@ public class Garmin {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
connectIQ = ConnectIQ.getInstance(c, ConnectIQ.IQConnectType.WIRELESS);
|
||||
// Create wrapped context BEFORE getInstance to ensure all SDK operations use it
|
||||
context = createWrappedContext(c);
|
||||
|
||||
connectIQ = ConnectIQ.getInstance(context, ConnectIQ.IQConnectType.WIRELESS);
|
||||
|
||||
// init a wrapped SDK with fix for "Cannot cast to Long" issue viz https://forums.garmin.com/forum/developers/connect-iq/connect-iq-bug-reports/158068-?p=1278464#post1278464
|
||||
context = initializeConnectIQWrapped(c, connectIQ, false, new ConnectIQ.ConnectIQListener() {
|
||||
initializeConnectIQWithContext(connectIQ, false, new ConnectIQ.ConnectIQListener() {
|
||||
|
||||
@Override
|
||||
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
|
||||
@@ -158,12 +161,8 @@ public class Garmin {
|
||||
connectIQ.sendMessage(getDevice(), getApp(), message, listener);
|
||||
}
|
||||
|
||||
private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
if (connectIQ instanceof ConnectIQAdbStrategy) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
return context;
|
||||
}
|
||||
Context wrappedContext = new ContextWrapper(context) {
|
||||
private static Context createWrappedContext(Context context) {
|
||||
return new ContextWrapper(context) {
|
||||
private HashMap<BroadcastReceiver, BroadcastReceiver> receiverToWrapper = new HashMap<>();
|
||||
|
||||
@Override
|
||||
@@ -190,6 +189,18 @@ public class Garmin {
|
||||
if (wrappedReceiver != null) super.unregisterReceiver(wrappedReceiver);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void initializeConnectIQWithContext(ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
}
|
||||
|
||||
private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
if (connectIQ instanceof ConnectIQAdbStrategy) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
return context;
|
||||
}
|
||||
Context wrappedContext = createWrappedContext(context);
|
||||
connectIQ.initialize(wrappedContext, autoUI, listener);
|
||||
return wrappedContext;
|
||||
}
|
||||
|
||||
@@ -20,32 +20,73 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
QLog.d(TAG, "onReceive intent " + intent.getAction());
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
}
|
||||
|
||||
try {
|
||||
QLog.d(TAG, "=== GARMIN INTENT DEBUG START ===");
|
||||
QLog.d(TAG, "Action: " + intent.getAction());
|
||||
|
||||
// Log all extras in the intent
|
||||
if (intent.getExtras() != null) {
|
||||
QLog.d(TAG, "Extras bundle: " + intent.getExtras());
|
||||
try {
|
||||
for (String key : intent.getExtras().keySet()) {
|
||||
Object value = intent.getExtras().get(key);
|
||||
QLog.d(TAG, " Extra[" + key + "] = " + value + " (type: " + (value != null ? value.getClass().getName() : "null") + ")");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error iterating extras: " + e.toString());
|
||||
}
|
||||
} else {
|
||||
QLog.d(TAG, "No extras in intent");
|
||||
}
|
||||
|
||||
// Process known actions
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing SEND_MESSAGE_STATUS");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing OPEN_APPLICATION");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing DEVICE_STATUS");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.INCOMING_MESSAGE".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing INCOMING_MESSAGE");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else {
|
||||
QLog.d(TAG, "Unknown action, no processing");
|
||||
}
|
||||
|
||||
QLog.d(TAG, "Calling wrapped receiver.onReceive()");
|
||||
receiver.onReceive(context, intent);
|
||||
} catch (IllegalArgumentException | BufferUnderflowException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
QLog.d(TAG, "=== GARMIN INTENT DEBUG END (success) ===");
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "=== EXCEPTION in wrapper (BEFORE or DURING receiver call) ===");
|
||||
QLog.e(TAG, "Exception type: " + e.getClass().getName());
|
||||
QLog.e(TAG, "Exception message: " + e.getMessage());
|
||||
QLog.e(TAG, "Stack trace:", e);
|
||||
QLog.e(TAG, "=== GARMIN INTENT DEBUG END (error) ===");
|
||||
}
|
||||
}
|
||||
|
||||
private static void replaceIQDeviceById(Intent intent, String extraName) {
|
||||
try {
|
||||
QLog.d(TAG, " Attempting to get Parcelable for extra: " + extraName);
|
||||
IQDevice device = intent.getParcelableExtra(extraName);
|
||||
if (device != null) {
|
||||
// Logger.logDebug("Replacing " + device.describeContents() + " " + device.getFriendlyName() + " by " + device.getDeviceIdentifier() );
|
||||
intent.putExtra(extraName, device.getDeviceIdentifier());
|
||||
QLog.d(TAG, " Found IQDevice: " + device.getFriendlyName() + " (ID: " + device.getDeviceIdentifier() + ")");
|
||||
long deviceId = device.getDeviceIdentifier();
|
||||
intent.putExtra(extraName, deviceId);
|
||||
QLog.d(TAG, " Replaced IQDevice with Long ID: " + deviceId);
|
||||
} else {
|
||||
QLog.d(TAG, " Extra '" + extraName + "' is null or not an IQDevice");
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
// It's already a long, i.e. on the simulator.
|
||||
QLog.d(TAG, " ClassCastException for '" + extraName + "': " + e.toString());
|
||||
QLog.d(TAG, " (Extra is already a Long, probably on simulator)");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, " Unexpected exception in replaceIQDeviceById: " + e.toString());
|
||||
QLog.e(TAG, " Stack trace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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("THINK 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) {
|
||||
@@ -1486,6 +1504,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(lifefitnessTreadmill);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("HORIZON")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("HZ_T101-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("HZ_7.0AT-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("AFG SPORT")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT2541")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && (gem_module_inclination || deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) ||
|
||||
@@ -1520,7 +1539,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM4500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM6500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("RUNN ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BFX_T")) ||
|
||||
(b.name().toUpper().startsWith("3G PRO ")) ||
|
||||
@@ -1836,8 +1855,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
|
||||
(b.name().toUpper().startsWith("INCONDI")) || // inCondi S150i
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10) ||
|
||||
(b.name().toUpper().startsWith("JFICCYCLE"))) &&
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10)) &&
|
||||
ftms_rower.contains(QZSettings::default_ftms_rower) &&
|
||||
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -2016,6 +2034,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(echelonStairclimber, &echelonstairclimber::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
echelonStairclimber->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(echelonStairclimber);
|
||||
} else if (b.name().toUpper().startsWith(QLatin1String("SF-S")) &&
|
||||
!sunnyfitStepper && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
sunnyfitStepper = new sunnyfitstepper(this->pollDeviceTime, noConsole, noHeartService);
|
||||
emit deviceConnected(b);
|
||||
connect(sunnyfitStepper, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
connect(sunnyfitStepper, &sunnyfitstepper::debug, this, &bluetooth::debug);
|
||||
connect(sunnyfitStepper, &sunnyfitstepper::speedChanged, this, &bluetooth::speedChanged);
|
||||
connect(sunnyfitStepper, &sunnyfitstepper::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
sunnyfitStepper->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(sunnyfitStepper);
|
||||
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) ||
|
||||
@@ -2094,15 +2125,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 +3146,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("THINK 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 +3654,6 @@ void bluetooth::restart() {
|
||||
delete echelonRower;
|
||||
echelonRower = nullptr;
|
||||
}
|
||||
if (mobiRower) {
|
||||
|
||||
delete mobiRower;
|
||||
mobiRower = nullptr;
|
||||
}
|
||||
if (echelonStride) {
|
||||
|
||||
delete echelonStride;
|
||||
@@ -3629,6 +3664,11 @@ void bluetooth::restart() {
|
||||
delete echelonStairclimber;
|
||||
echelonStairclimber = nullptr;
|
||||
}
|
||||
if (sunnyfitStepper) {
|
||||
|
||||
delete sunnyfitStepper;
|
||||
sunnyfitStepper = nullptr;
|
||||
}
|
||||
if (octaneTreadmill) {
|
||||
|
||||
delete octaneTreadmill;
|
||||
@@ -4054,12 +4094,12 @@ bluetoothdevice *bluetooth::device() {
|
||||
return echelonConnectSport;
|
||||
} else if (echelonRower) {
|
||||
return echelonRower;
|
||||
} else if (mobiRower) {
|
||||
return mobiRower;
|
||||
} else if (echelonStride) {
|
||||
return echelonStride;
|
||||
} else if (echelonStairclimber) {
|
||||
return echelonStairclimber;
|
||||
} else if (sunnyfitStepper) {
|
||||
return sunnyfitStepper;
|
||||
} else if (octaneTreadmill) {
|
||||
return octaneTreadmill;
|
||||
} else if (ziproTreadmill) {
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
#include "signalhandler.h"
|
||||
#include "devices/skandikawiribike/skandikawiribike.h"
|
||||
#include "devices/smartrowrower/smartrowrower.h"
|
||||
#include "devices/sunnyfitstepper/sunnyfitstepper.h"
|
||||
#include "devices/smartspin2k/smartspin2k.h"
|
||||
#include "devices/snodebike/snodebike.h"
|
||||
#include "devices/strydrunpowersensor/strydrunpowersensor.h"
|
||||
@@ -154,7 +155,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 +271,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
echelonrower *echelonRower = nullptr;
|
||||
ftmsrower *ftmsRower = nullptr;
|
||||
smartrowrower *smartrowRower = nullptr;
|
||||
mobirower *mobiRower = nullptr;
|
||||
sunnyfitstepper *sunnyfitStepper = nullptr;
|
||||
echelonstride *echelonStride = nullptr;
|
||||
echelonstairclimber *echelonStairclimber = nullptr;
|
||||
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
|
||||
@@ -308,6 +309,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 +347,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,5 +1,6 @@
|
||||
#include "cscbike.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
@@ -459,6 +460,8 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
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 =
|
||||
@@ -473,11 +476,17 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
if (virtual_device_rower) {
|
||||
emit debug(QStringLiteral("creating virtual rower interface..."));
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
} else {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
|
||||
@@ -160,22 +160,24 @@ uint8_t deerruntreadmill::calculatePitPatChecksum(uint8_t arr[], size_t size) {
|
||||
}
|
||||
|
||||
|
||||
void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
void deerruntreadmill::forceSpeedAndInclination(double requestSpeed, double requestInclination) {
|
||||
QSettings settings;
|
||||
|
||||
if (pitpat) {
|
||||
// PitPat speed template
|
||||
// Pattern: 6a 17 00 00 00 00 [speed_high] [speed_low] 01 00 8a 00 04 00 00 00 00 00 12 2e 0c [checksum] 43
|
||||
// Speed encoding: speed value * 1000 (e.g., 2.0 km/h = 2000 = 0x07d0)
|
||||
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x07, 0x6c, 0x01, 0x00, 0x8a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x2e, 0x0c, 0xc3, 0x43};
|
||||
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x01, 0x08, 0x64, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x7a, 0x67, 0x96, 0x43};
|
||||
|
||||
uint16_t speed = (uint16_t)(requestSpeed * 1000.0);
|
||||
uint16_t incline = (uint16_t)(requestInclination);
|
||||
writeSpeed[6] = (speed >> 8) & 0xFF; // High byte
|
||||
writeSpeed[7] = speed & 0xFF; // Low byte
|
||||
writeSpeed[9] = incline & 0xFF; // Low byte
|
||||
writeSpeed[21] = calculatePitPatChecksum(writeSpeed, sizeof(writeSpeed)); // Checksum at byte 21
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
|
||||
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed), false, true);
|
||||
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed) + QStringLiteral(" incline=") + QString::number(requestInclination), false, true);
|
||||
} else if (superun_ba04) {
|
||||
// Superun BA04 speed template
|
||||
uint8_t writeSpeed[] = {0x4d, 0x00, 0x14, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0xdb, 0x43};
|
||||
@@ -201,8 +203,12 @@ void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::forceIncline(double requestIncline) {
|
||||
void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
forceSpeedAndInclination(requestSpeed, currentInclination().value());
|
||||
}
|
||||
|
||||
void deerruntreadmill::forceIncline(double requestIncline) {
|
||||
forceSpeedAndInclination(currentSpeed().value(), requestIncline);
|
||||
}
|
||||
|
||||
void deerruntreadmill::changeInclinationRequested(double grade, double percentage) {
|
||||
@@ -385,6 +391,9 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
speed = ((double)((value[3] << 8) | ((uint8_t)value[4])) / 1000.0);
|
||||
}
|
||||
double incline = 0.0;
|
||||
if(pitpat) {
|
||||
incline = (double)(value[11] & 0xFF);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
|
||||
@@ -46,6 +46,7 @@ class deerruntreadmill : public treadmill {
|
||||
private:
|
||||
void forceSpeed(double requestSpeed);
|
||||
void forceIncline(double requestIncline);
|
||||
void forceSpeedAndInclination(double requestSpeed, double requestInclination);
|
||||
void btinit(bool startTape);
|
||||
void writeCharacteristic(const QLowEnergyCharacteristic characteristic, uint8_t *data, uint8_t data_len,
|
||||
const QString &info, bool disable_log = false, bool wait_for_response = false);
|
||||
|
||||
@@ -753,11 +753,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
} else if (MRK_S26C) {
|
||||
m_watt = Cadence.value() * (Resistance.value() * 1.16);
|
||||
emit debug(QStringLiteral("Current Watt (MRK-S26C formula): ") + QString::number(m_watt.value()));
|
||||
} else if (JFICCYCLE) {
|
||||
// JFICCYCLE sends power but always at 0, so calculate from cadence or heart rate
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt (JFICCYCLE calculated): ") + QString::number(m_watt.value()));
|
||||
} else if (LYDSTO && watt_ignore_builtin) {
|
||||
} else if ((LYDSTO || DMASUN) && watt_ignore_builtin) {
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else {
|
||||
@@ -1751,6 +1747,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("LYDSTO"))) {
|
||||
qDebug() << QStringLiteral("LYDSTO found");
|
||||
LYDSTO = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("DMASUN-") && bluetoothDevice.name().toUpper().endsWith("-BIKE"))) {
|
||||
qDebug() << QStringLiteral("DMASUN bike found");
|
||||
DMASUN = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("SL010-"))) {
|
||||
qDebug() << QStringLiteral("SL010 found");
|
||||
SL010 = true;
|
||||
@@ -1829,9 +1828,6 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("S18 found");
|
||||
S18 = true;
|
||||
max_resistance = 24;
|
||||
} else if(device.name().toUpper().startsWith("JFICCYCLE")) {
|
||||
qDebug() << QStringLiteral("JFICCYCLE found");
|
||||
JFICCYCLE = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ class ftmsbike : public bike {
|
||||
bool BIKE_ = false;
|
||||
bool SMB1 = false;
|
||||
bool LYDSTO = false;
|
||||
bool DMASUN = false;
|
||||
bool SL010 = false;
|
||||
bool REEBOK = false;
|
||||
bool TITAN_7000 = false;
|
||||
@@ -172,7 +173,6 @@ class ftmsbike : public bike {
|
||||
bool SPORT01 = false;
|
||||
bool FS_YK = false;
|
||||
bool S18 = false;
|
||||
bool JFICCYCLE = false;
|
||||
bool ZIPRO_RAVE = false;
|
||||
|
||||
uint8_t secondsToResetTimer = 5;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
389
src/devices/sunnyfitstepper/sunnyfitstepper.cpp
Normal file
389
src/devices/sunnyfitstepper/sunnyfitstepper.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
#include "sunnyfitstepper.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualtreadmill.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
sunnyfitstepper::sunnyfitstepper(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
this->pollDeviceTime = pollDeviceTime;
|
||||
|
||||
refresh = new QTimer(this);
|
||||
initDone = false;
|
||||
frameBuffer.clear();
|
||||
expectingSecondPart = false;
|
||||
|
||||
connect(refresh, &QTimer::timeout, this, &sunnyfitstepper::update);
|
||||
refresh->start(pollDeviceTime);
|
||||
}
|
||||
|
||||
bool sunnyfitstepper::connected() {
|
||||
if (!m_control)
|
||||
return false;
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::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(this, &sunnyfitstepper::packetReceived, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit debug(QStringLiteral("writeCharacteristic error because the connection is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::sendPoll() {
|
||||
// Alternate between two poll commands
|
||||
|
||||
counterPoll++;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::changeInclinationRequested(double grade, double percentage) {
|
||||
if (percentage < 0)
|
||||
percentage = 0;
|
||||
changeInclination(grade, percentage);
|
||||
}
|
||||
|
||||
void sunnyfitstepper::processDataFrame(const QByteArray &completeFrame) {
|
||||
if (completeFrame.length() != 32) {
|
||||
qDebug() << "ERROR: Frame length is not 32 bytes:" << completeFrame.length();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint8_t)completeFrame.at(0) != 0x5a) {
|
||||
qDebug() << "ERROR: Frame doesn't start with 0x5a";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint8_t)completeFrame.at(1) != 0x05) {
|
||||
qDebug() << "WARNING: Expected 0x05 at byte 1, got:" << QString::number((uint8_t)completeFrame.at(1), 16);
|
||||
}
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QSettings settings;
|
||||
|
||||
// Extract cadence (bytes 6-7, little-endian)
|
||||
uint16_t rawCadence = ((uint8_t)completeFrame.at(7) << 8) | (uint8_t)completeFrame.at(6);
|
||||
Cadence = (double)rawCadence;
|
||||
|
||||
// Extract step count (bytes 10-12, little-endian)
|
||||
uint32_t steps = ((uint32_t)(uint8_t)completeFrame.at(12) << 16) |
|
||||
((uint32_t)(uint8_t)completeFrame.at(11) << 8) |
|
||||
(uint32_t)(uint8_t)completeFrame.at(10);
|
||||
StepCount = steps;
|
||||
|
||||
// Calculate elevation manually (0.2 meters per step)
|
||||
elevationAcc = (double)steps * 0.20;
|
||||
|
||||
// Calculate speed from cadence (stairclimber convention)
|
||||
Speed = Cadence.value() / 3.2;
|
||||
|
||||
qDebug() << QStringLiteral("Current Cadence (SPM): ") + QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current StepCount: ") + QString::number(StepCount.value());
|
||||
qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value());
|
||||
qDebug() << QStringLiteral("Current Elevation: ") + QString::number(elevationAcc.value());
|
||||
|
||||
// Calculate metrics
|
||||
if (!firstCharacteristicChanged) {
|
||||
if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) {
|
||||
KCal += ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) +
|
||||
1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
Distance += ((Speed.value() / 3600.0) / (1000.0 / (lastTimeCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Current Distance: ") + QString::number(Distance.value());
|
||||
qDebug() << QStringLiteral("Current KCal: ") + QString::number(KCal.value());
|
||||
qDebug() << QStringLiteral("Current Watt: ") +
|
||||
QString::number(watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError)
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
|
||||
lastTimeCharacteristicChanged = now;
|
||||
firstCharacteristicChanged = false;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::update() {
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
btinit();
|
||||
} else if (m_control->state() == QLowEnergyController::DiscoveredState && gattCommunicationChannelService &&
|
||||
gattWriteCharacteristic.isValid() && gattNotify1Characteristic.isValid() &&
|
||||
gattNotify4Characteristic.isValid() && initDone) {
|
||||
QSettings settings;
|
||||
|
||||
// *********** virtual treadmill init *************************************
|
||||
if (!this->hasVirtualDevice()) {
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_force_bike =
|
||||
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike)
|
||||
.toBool();
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_force_bike) {
|
||||
debug("creating virtual treadmill interface...");
|
||||
auto virtualTreadMill = new virtualtreadmill(this, noHeartService);
|
||||
connect(virtualTreadMill, &virtualtreadmill::debug, this, &sunnyfitstepper::debug);
|
||||
connect(virtualTreadMill, &virtualtreadmill::changeInclination, this,
|
||||
&sunnyfitstepper::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
debug("creating virtual bike interface...");
|
||||
auto virtualBike = new virtualbike(this);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this,
|
||||
&sunnyfitstepper::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ************************************************************
|
||||
|
||||
update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
// Send poll every 2 seconds
|
||||
if (sec1Update++ >= (2000 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
//sendPoll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
|
||||
|
||||
// Handle command responses (Notify 1)
|
||||
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("fd710003-e950-458e-8a4d-a1cbc5aa4cce"))) {
|
||||
qDebug() << "Command response:" << newValue.toHex(' ');
|
||||
emit packetReceived();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle main data stream (Notify 4) - SPLIT FRAME LOGIC
|
||||
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("fd710006-e950-458e-8a4d-a1cbc5aa4cce"))) {
|
||||
// First part: 20 bytes starting with 0x5a
|
||||
if (newValue.length() == 20 && (uint8_t)newValue.at(0) == 0x5a) {
|
||||
frameBuffer.clear();
|
||||
frameBuffer.append(newValue);
|
||||
expectingSecondPart = true;
|
||||
qDebug() << "First part of frame received (20 bytes)";
|
||||
return;
|
||||
}
|
||||
|
||||
// Second part: 12 bytes
|
||||
if (newValue.length() == 12 && expectingSecondPart) {
|
||||
frameBuffer.append(newValue);
|
||||
expectingSecondPart = false;
|
||||
|
||||
if (frameBuffer.length() == 32) {
|
||||
emit debug(QStringLiteral(" << COMPLETE FRAME >> ") + frameBuffer.toHex(' '));
|
||||
processDataFrame(frameBuffer);
|
||||
frameBuffer.clear();
|
||||
} else {
|
||||
qDebug() << "ERROR: Complete frame size mismatch:" << frameBuffer.length();
|
||||
frameBuffer.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Unexpected frame structure
|
||||
qDebug() << "Unexpected frame - length:" << newValue.length() << "expecting second part:" << expectingSecondPart;
|
||||
frameBuffer.clear();
|
||||
expectingSecondPart = false;
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::btinit() {
|
||||
uint8_t init1[] = {0x5a, 0x02, 0x00, 0x08, 0x07, 0xa0, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0xe6, 0xa5};
|
||||
uint8_t init2[] = {0x5a, 0x02, 0x00, 0x03, 0x02, 0xa3, 0x00, 0xaa, 0xa5};
|
||||
uint8_t init3[] = {0x5a, 0x02, 0x00, 0x03, 0x02, 0xb4, 0x00, 0xbb, 0xa5};
|
||||
uint8_t init4[] = {0x5a, 0x04, 0x00, 0x03, 0x02, 0xf1, 0x00, 0xfa, 0xa5};
|
||||
|
||||
writeCharacteristic(init1, sizeof(init1), QStringLiteral("init1"), false, true);
|
||||
writeCharacteristic(init2, sizeof(init2), QStringLiteral("init2"), false, true);
|
||||
writeCharacteristic(init3, sizeof(init3), QStringLiteral("init3"), false, false);
|
||||
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, false);
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("fd710002-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("fd710003-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
QBluetoothUuid _gattNotify4CharacteristicId(QStringLiteral("fd710006-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
gattNotify4Characteristic = gattCommunicationChannelService->characteristic(_gattNotify4CharacteristicId);
|
||||
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
Q_ASSERT(gattNotify4Characteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&sunnyfitstepper::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&sunnyfitstepper::characteristicWritten);
|
||||
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)), this,
|
||||
SLOT(errorService(QLowEnergyService::ServiceError)));
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&sunnyfitstepper::descriptorWritten);
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify4Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
|
||||
initRequest = true;
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void sunnyfitstepper::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void sunnyfitstepper::serviceScanDone(void) {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
qDebug() << s << "service found!";
|
||||
}
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("fd710001-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
if (gattCommunicationChannelService == nullptr) {
|
||||
qDebug() << "invalid service";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &sunnyfitstepper::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void sunnyfitstepper::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("sunnyfitstepper::errorService ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("sunnyfitstepper::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("sunnyfitstepper::controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &sunnyfitstepper::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &sunnyfitstepper::serviceScanDone);
|
||||
connect(m_control, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(error(QLowEnergyController::Error)));
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &sunnyfitstepper::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("QLowEnergyController disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::startDiscover() {
|
||||
m_control->discoverServices();
|
||||
}
|
||||
99
src/devices/sunnyfitstepper/sunnyfitstepper.h
Normal file
99
src/devices/sunnyfitstepper/sunnyfitstepper.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef SUNNYFITSTEPPER_H
|
||||
#define SUNNYFITSTEPPER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "stairclimber.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class sunnyfitstepper : public stairclimber {
|
||||
Q_OBJECT
|
||||
public:
|
||||
sunnyfitstepper(uint32_t pollDeviceTime = 200, bool noConsole = false, bool noHeartService = false,
|
||||
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
void btinit();
|
||||
void sendPoll();
|
||||
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
|
||||
bool wait_for_response = false);
|
||||
void processDataFrame(const QByteArray &completeFrame);
|
||||
void startDiscover();
|
||||
|
||||
// Bluetooth
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotify1Characteristic;
|
||||
QLowEnergyCharacteristic gattNotify4Characteristic;
|
||||
|
||||
// Split-frame handling (CRITICAL)
|
||||
QByteArray frameBuffer;
|
||||
bool expectingSecondPart = false;
|
||||
|
||||
// State
|
||||
QTimer *refresh;
|
||||
uint8_t sec1Update = 0;
|
||||
uint8_t counterPoll = 0;
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
bool noConsole = false;
|
||||
bool noHeartService = false;
|
||||
uint32_t pollDeviceTime = 200;
|
||||
QDateTime lastTimeCharacteristicChanged;
|
||||
bool firstCharacteristicChanged = true;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void speedChanged(double speed);
|
||||
void packetReceived();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(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 changeInclinationRequested(double grade, double percentage);
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // SUNNYFITSTEPPER_H
|
||||
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
|
||||
@@ -37,10 +37,12 @@ void treadmill::changeSpeed(double speed) {
|
||||
if(stryd_speed_instead_treadmill && Speed.value() > 0) {
|
||||
double delta = (Speed.value() - rawSpeed.value());
|
||||
double maxAllowedDelta = speed * 0.20; // 20% of the speed request
|
||||
double correction_gain = settings.value(QZSettings::stryd_speed_correction_gain, QZSettings::default_stryd_speed_correction_gain).toDouble();
|
||||
|
||||
if (std::abs(delta) <= maxAllowedDelta) {
|
||||
qDebug() << "stryd_speed_instead_treadmill so override speed by " << delta;
|
||||
speed -= delta;
|
||||
double correctedDelta = delta * correction_gain;
|
||||
qDebug() << "stryd_speed_instead_treadmill so override speed by " << correctedDelta << "(delta:" << delta << "gain:" << correction_gain << ")";
|
||||
speed -= correctedDelta;
|
||||
} else {
|
||||
qDebug() << "Delta" << delta << "exceeds 20% threshold of" << maxAllowedDelta << "- not applying correction";
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -558,6 +558,10 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
&homeform::pelotonOffset_Minus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Plus, this, &homeform::gearUp);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Minus, this, &homeform::gearDown);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::speed_Plus, this, &homeform::speedPlus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::speed_Minus, this, &homeform::speedMinus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::inclination_Plus, this, &homeform::inclinationPlus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::inclination_Minus, this, &homeform::inclinationMinus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonOffset, this, &homeform::pelotonOffset);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonAskStart, this, &homeform::pelotonAskStart);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::peloton_start_workout, this,
|
||||
@@ -1610,6 +1614,22 @@ void homeform::gearDown() {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::speedPlus() {
|
||||
Plus(QStringLiteral("speed"));
|
||||
}
|
||||
|
||||
void homeform::speedMinus() {
|
||||
Minus(QStringLiteral("speed"));
|
||||
}
|
||||
|
||||
void homeform::inclinationPlus() {
|
||||
Plus(QStringLiteral("inclination"));
|
||||
}
|
||||
|
||||
void homeform::inclinationMinus() {
|
||||
Minus(QStringLiteral("inclination"));
|
||||
}
|
||||
|
||||
void homeform::ftmsAccessoryConnected(smartspin2k *d) {
|
||||
connect(this, &homeform::autoResistanceChanged, d, &smartspin2k::autoResistanceChanged);
|
||||
connect(d, &smartspin2k::gearUp, this, &homeform::gearUp);
|
||||
@@ -5396,6 +5416,7 @@ void homeform::update() {
|
||||
double stepCount = 0;
|
||||
|
||||
bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
bool weight_kg_unit = settings.value(QZSettings::weight_kg_unit, QZSettings::default_weight_kg_unit).toBool();
|
||||
double ftpSetting = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble();
|
||||
double unit_conversion = 1.0;
|
||||
double meter_feet_conversion = 1.0;
|
||||
@@ -5679,7 +5700,7 @@ void homeform::update() {
|
||||
datetime->setValue(formattedTime);
|
||||
watts = bluetoothManager->device()->wattsMetricforUI();
|
||||
watt->setValue(QString::number(watts, 'f', 0));
|
||||
weightLoss->setValue(QString::number(miles ? bluetoothManager->device()->weightLoss() * 35.274
|
||||
weightLoss->setValue(QString::number((miles && !weight_kg_unit) ? bluetoothManager->device()->weightLoss() * 35.274
|
||||
: bluetoothManager->device()->weightLoss(),
|
||||
'f', 2));
|
||||
|
||||
|
||||
@@ -1056,6 +1056,10 @@ class homeform : public QObject {
|
||||
void sortTilesTimeout();
|
||||
void gearUp();
|
||||
void gearDown();
|
||||
void speedPlus();
|
||||
void speedMinus();
|
||||
void inclinationPlus();
|
||||
void inclinationMinus();
|
||||
void changeTimestamp(QTime source, QTime actual);
|
||||
void pelotonOffset_Plus();
|
||||
void pelotonOffset_Minus();
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.treadmill-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -73,20 +77,24 @@
|
||||
<tr class="speed" sort-order="0">
|
||||
<td class="icon">🏃</td>
|
||||
<td style="text-align: left">SPEED</td>
|
||||
<td class="speed-avg-title"><small>AVG</small></td>
|
||||
<td class="speed-avg">0.0</td>
|
||||
<td class="speed-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedMinus()">-</button></td>
|
||||
<td class="speed-avg"><span class="speed-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="speed-value values"><b>0.0</b></td>
|
||||
<td class="speed-max-title"><small>MAX</small></td>
|
||||
<td class="speed-max">0.0</td>
|
||||
<td class="speed-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedPlus()">+</button></td>
|
||||
<td class="speed-max"><span class="speed-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="inclination" sort-order="1">
|
||||
<td class="icon">📐</td>
|
||||
<td style="text-align: left">INCLINE</td>
|
||||
<td><small>AVG</small></td>
|
||||
<td class="inclination-avg">0.0</td>
|
||||
<td class="inclination-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationMinus()">-</button></td>
|
||||
<td class="inclination-avg"><span class="inclination-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="inclination-value values"><b>0.0</b></td>
|
||||
<td><small>MAX</small></td>
|
||||
<td class="inclination-max">0.0</td>
|
||||
<td class="inclination-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationPlus()">+</button></td>
|
||||
<td class="inclination-max"><span class="inclination-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="pace" sort-order="2">
|
||||
<td class="icon">🏃</td>
|
||||
@@ -306,6 +314,62 @@
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function Lap() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'lap',
|
||||
@@ -611,6 +675,7 @@
|
||||
'speed_color', 'pace_color', 'power_zone_color', 'target_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_h', 'target_pace_m', 'target_pace_s',
|
||||
'deviceType', 'TREADMILL_TYPE',
|
||||
'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',
|
||||
@@ -673,6 +738,8 @@
|
||||
var peloton_offset = 0;
|
||||
var gears = 0;
|
||||
var nextrow = "";
|
||||
var deviceType = -1;
|
||||
var TREADMILL_TYPE = -1;
|
||||
|
||||
for (let key of keys_arr) {
|
||||
if (msg.content[key] === undefined || msg.content[key] === null)
|
||||
@@ -789,6 +856,10 @@
|
||||
peloton_offset = msg.content[key];
|
||||
} else if (key === 'gears') {
|
||||
gears = msg.content[key];
|
||||
} else if (key === 'deviceType') {
|
||||
deviceType = msg.content[key];
|
||||
} else if (key === 'TREADMILL_TYPE') {
|
||||
TREADMILL_TYPE = msg.content[key];
|
||||
} else if (key === 'peloton_resistance_color') {
|
||||
$('.pelotonresistance-value').css('color', msg.content[key]);
|
||||
} else if (key === 'heart_color') {
|
||||
@@ -837,14 +908,24 @@
|
||||
|
||||
$('.speed-value').html("<b>" + speed.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.speed-avg').html(speed_lapavg.toFixed(1));
|
||||
$('.speed-max').html(speed_lapmax.toFixed(1));
|
||||
$('.speed-avg-value').html(speed_lapavg.toFixed(1));
|
||||
$('.speed-max-value').html(speed_lapmax.toFixed(1));
|
||||
if (tile_target_inclination_enabled && target_inclination > 0)
|
||||
$('.inclination-value').html("<b>" + inclination.toFixed(1) + "/" + target_inclination.toFixed(1) + "</b>");
|
||||
else
|
||||
$('.inclination-value').html("<b>" + inclination.toFixed(1) + "</b>");
|
||||
$('.inclination-avg').html(inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max').html(inclination_lapmax.toFixed(1));
|
||||
$('.inclination-avg-value').html(inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max-value').html(inclination_lapmax.toFixed(1));
|
||||
|
||||
// Show/hide treadmill-only controls based on device type
|
||||
if (deviceType === TREADMILL_TYPE && TREADMILL_TYPE !== -1) {
|
||||
$('.treadmill-only').show();
|
||||
$('.non-treadmill').hide();
|
||||
} else {
|
||||
$('.treadmill-only').hide();
|
||||
$('.non-treadmill').show();
|
||||
}
|
||||
|
||||
$('.elevation-value').html("<b>" + elevation.toFixed(1) + "</b>");
|
||||
if (tile_target_cadence_enabled && target_cadence > 0)
|
||||
$('.cadence-value').html("<b>" + cadence.toFixed(0) + "/" + target_cadence.toFixed(0) + "</b>");
|
||||
|
||||
@@ -197,6 +197,10 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.treadmill-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Metric selector panel */
|
||||
.metric-selector-panel {
|
||||
display: none;
|
||||
@@ -247,20 +251,24 @@
|
||||
<tr class="speed" sort-order="0">
|
||||
<td class="icon">🏃</td>
|
||||
<td style="text-align: left">SPEED</td>
|
||||
<td class="speed-avg-title"><small>AVG</small></td>
|
||||
<td class="speed-avg">0.0</td>
|
||||
<td class="speed-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedMinus()">-</button></td>
|
||||
<td class="speed-avg"><span class="speed-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="speed-value values"><b>0.0</b></td>
|
||||
<td class="speed-max-title"><small>MAX</small></td>
|
||||
<td class="speed-max">0.0</td>
|
||||
<td class="speed-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedPlus()">+</button></td>
|
||||
<td class="speed-max"><span class="speed-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="inclination" sort-order="1">
|
||||
<td class="icon">📐</td>
|
||||
<td style="text-align: left">INCLINE</td>
|
||||
<td><small>AVG</small></td>
|
||||
<td class="inclination-avg">0.0</td>
|
||||
<td class="inclination-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationMinus()">-</button></td>
|
||||
<td class="inclination-avg"><span class="inclination-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="inclination-value values"><b>0.0</b></td>
|
||||
<td><small>MAX</small></td>
|
||||
<td class="inclination-max">0.0</td>
|
||||
<td class="inclination-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationPlus()">+</button></td>
|
||||
<td class="inclination-max"><span class="inclination-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="pace" sort-order="2">
|
||||
<td class="icon">🏃</td>
|
||||
@@ -826,6 +834,62 @@
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to clear/lap
|
||||
function Lap() {
|
||||
let el = new MainWSQueueElement({
|
||||
@@ -1072,6 +1136,7 @@
|
||||
'speed_color', 'pace_color', 'power_zone_color', 'target_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_h', 'target_pace_m', 'target_pace_s',
|
||||
'deviceType', 'TREADMILL_TYPE',
|
||||
'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',
|
||||
@@ -1137,6 +1202,8 @@
|
||||
var peloton_offset = 0;
|
||||
var gears = 0;
|
||||
var nextrow = "";
|
||||
var deviceType = -1;
|
||||
var TREADMILL_TYPE = -1;
|
||||
|
||||
// Get values from message
|
||||
for (let key of keys_arr) {
|
||||
@@ -1255,6 +1322,10 @@
|
||||
peloton_offset = msg.content[key];
|
||||
} else if (key === 'gears') {
|
||||
gears = msg.content[key];
|
||||
} else if (key === 'deviceType') {
|
||||
deviceType = msg.content[key];
|
||||
} else if (key === 'TREADMILL_TYPE') {
|
||||
TREADMILL_TYPE = msg.content[key];
|
||||
} else if (key === 'peloton_resistance_color') {
|
||||
$('.pelotonresistance-value').css('color', msg.content[key]);
|
||||
} else if (key === 'heart_color') {
|
||||
@@ -1336,7 +1407,9 @@
|
||||
target_power: target_power,
|
||||
peloton_offset: peloton_offset,
|
||||
gears: gears,
|
||||
nextrow: nextrow
|
||||
nextrow: nextrow,
|
||||
deviceType: deviceType,
|
||||
TREADMILL_TYPE: TREADMILL_TYPE
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@@ -1356,8 +1429,8 @@
|
||||
} else {
|
||||
$('.speed-value').html("<b>" + data.speed.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.speed-avg').html(data.speed_lapavg.toFixed(1));
|
||||
$('.speed-max').html(data.speed_lapmax.toFixed(1));
|
||||
$('.speed-avg-value').html(data.speed_lapavg.toFixed(1));
|
||||
$('.speed-max-value').html(data.speed_lapmax.toFixed(1));
|
||||
|
||||
// Inclination
|
||||
if (tile_target_inclination_enabled && data.target_inclination > 0) {
|
||||
@@ -1365,8 +1438,17 @@
|
||||
} else {
|
||||
$('.inclination-value').html("<b>" + data.inclination.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.inclination-avg').html(data.inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max').html(data.inclination_lapmax.toFixed(1));
|
||||
$('.inclination-avg-value').html(data.inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max-value').html(data.inclination_lapmax.toFixed(1));
|
||||
|
||||
// Show/hide treadmill-only controls based on device type
|
||||
if (data.deviceType === data.TREADMILL_TYPE && data.TREADMILL_TYPE !== undefined) {
|
||||
$('.treadmill-only').show();
|
||||
$('.non-treadmill').hide();
|
||||
} else {
|
||||
$('.treadmill-only').hide();
|
||||
$('.non-treadmill').show();
|
||||
}
|
||||
|
||||
// Elevation
|
||||
$('.elevation-value').html("<b>" + data.elevation.toFixed(1) + "</b>");
|
||||
|
||||
@@ -71,13 +71,13 @@ viewer.trackedEntity = bike;
|
||||
</body>
|
||||
<body>
|
||||
<div id="cesiumContainer" class="cesiumContainer"></div>
|
||||
<div id="metricsContainer" style="position: absolute; bottom: 0px; right: 0px; width: 200px; height: 250px; touch-action: none; user-select: none;">
|
||||
<div class="metrics" style="color: #FFFFFF; width: 100%; height: 100%; margin: 0; border-radius: 25px; border: 2px solid #73AD21; background: #73AD21; padding: 8px; box-sizing: border-box; overflow: hidden; position: relative;">
|
||||
<div id="metricsContainer" style="position: absolute; bottom: 0px; right: 0px; width: 200px; height: 250px; touch-action: none; user-select: none; z-index: 1000;">
|
||||
<div class="metrics" style="color: #FFFFFF; width: 100%; height: 100%; margin: 0; border-radius: 25px; border: 2px solid #73AD21; background: #73AD21; padding: 8px; box-sizing: border-box; overflow: hidden; position: relative; touch-action: none;">
|
||||
<div id="metricsText" style="font-size: 12px; line-height: 1.4;">🏃Speed: 0.00<br>🚴Cadence:0<br>💓Heart:0<br>🔥Calories:0.0<br>📏Odometer:0.00<br>⚡Watt:0<br>⏲️Elapsed:0:00:00<br>📐Inclination:0.0<br>🧲Resistance:0<br>✈️Altitude:0.0<br>⛰️Elevation:0.0</div>
|
||||
<div id="resizeHandle" style="position: absolute; bottom: 0; right: 0; width: 20px; height: 20px; background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.5) 50%); cursor: nwse-resize; border-bottom-right-radius: 23px;"></div>
|
||||
<div id="resizeHandle" style="position: absolute; bottom: 0; right: 0; width: 50px; height: 50px; background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.8) 50%); cursor: nwse-resize; border-bottom-right-radius: 23px; display: flex; align-items: flex-end; justify-content: flex-end; font-size: 24px; color: rgba(0,0,0,0.6); padding-bottom: 2px; padding-right: 4px;">⤡</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border: 0px solid #aaa; border-radius: 10px; overflow: hidden; position:absolute; bottom: 0px; right: 150px; width=150px; height=75px"><canvas id="canvasChart" style="width=150px; height=75px; border-right: 0pt solid #ffff00;"></canvas></div>
|
||||
<div id="chartContainer" style="border: 0px solid #aaa; border-radius: 10px; overflow: hidden; position:absolute; bottom: 0px; right: 150px; width=150px; height=75px; z-index: 999; touch-action: none;"><canvas id="canvasChart" style="width=150px; height=75px; border-right: 0pt solid #ffff00;"></canvas></div>
|
||||
<script type="text/javascript">
|
||||
let cameraComplete = true
|
||||
let lastAzimuth = 0
|
||||
@@ -325,12 +325,29 @@ console.error('Error is ' + err);
|
||||
const container = document.getElementById('metricsContainer');
|
||||
const resizeHandle = document.getElementById('resizeHandle');
|
||||
const metricsText = document.getElementById('metricsText');
|
||||
const chartContainer = document.getElementById('chartContainer');
|
||||
|
||||
let isDragging = false;
|
||||
let isResizing = false;
|
||||
let startX, startY, startLeft, startTop, startWidth, startHeight;
|
||||
let resizeTimeout = null;
|
||||
|
||||
// Update chart position to follow metrics container
|
||||
function updateChartPosition() {
|
||||
// Position chart to the left of metrics container, aligned at bottom
|
||||
const metricsLeft = container.offsetLeft;
|
||||
const metricsTop = container.offsetTop;
|
||||
const metricsHeight = container.offsetHeight;
|
||||
const chartWidth = chartContainer.offsetWidth;
|
||||
const chartHeight = chartContainer.offsetHeight;
|
||||
|
||||
// Position chart: 10px to the left of metrics, aligned at bottom
|
||||
chartContainer.style.left = (metricsLeft - chartWidth - 10) + 'px';
|
||||
chartContainer.style.top = (metricsTop + metricsHeight - chartHeight) + 'px';
|
||||
chartContainer.style.right = 'auto';
|
||||
chartContainer.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
// Load saved position and size
|
||||
function loadState() {
|
||||
const saved = localStorage.getItem('metricsContainerState');
|
||||
@@ -343,6 +360,7 @@ console.error('Error is ' + err);
|
||||
if (state.bottom !== undefined) container.style.bottom = state.bottom + 'px';
|
||||
if (state.width) container.style.width = state.width + 'px';
|
||||
if (state.height) container.style.height = state.height + 'px';
|
||||
updateChartPosition();
|
||||
updateFontSize();
|
||||
} catch (e) {
|
||||
console.error('Error loading metrics state:', e);
|
||||
@@ -438,6 +456,18 @@ console.error('Error is ' + err);
|
||||
return { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
|
||||
// Check if touch/click is in resize handle area (bottom-right 50x50px)
|
||||
function isInResizeHandle(x, y) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const handleSize = 50;
|
||||
return (
|
||||
x >= rect.right - handleSize &&
|
||||
x <= rect.right &&
|
||||
y >= rect.bottom - handleSize &&
|
||||
y <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Start dragging
|
||||
function startDrag(e) {
|
||||
if (isResizing) return;
|
||||
@@ -449,6 +479,15 @@ console.error('Error is ' + err);
|
||||
startTop = container.offsetTop;
|
||||
container.style.right = 'auto';
|
||||
container.style.bottom = 'auto';
|
||||
|
||||
// Add global listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
document.addEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -460,12 +499,25 @@ console.error('Error is ' + err);
|
||||
startY = coords.y;
|
||||
startWidth = container.offsetWidth;
|
||||
startHeight = container.offsetHeight;
|
||||
|
||||
// Add global listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
document.addEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Handle move
|
||||
function handleMove(e) {
|
||||
// Only handle if we're actually dragging or resizing
|
||||
if (!isDragging && !isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = getCoordinates(e);
|
||||
|
||||
if (isDragging) {
|
||||
@@ -473,6 +525,8 @@ console.error('Error is ' + err);
|
||||
const deltaY = coords.y - startY;
|
||||
container.style.left = (startLeft + deltaX) + 'px';
|
||||
container.style.top = (startTop + deltaY) + 'px';
|
||||
updateChartPosition();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else if (isResizing) {
|
||||
const deltaX = coords.x - startX;
|
||||
@@ -481,17 +535,34 @@ console.error('Error is ' + err);
|
||||
const newHeight = Math.max(100, startHeight + deltaY);
|
||||
container.style.width = newWidth + 'px';
|
||||
container.style.height = newHeight + 'px';
|
||||
updateChartPosition();
|
||||
debouncedUpdateFontSize();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// End drag or resize
|
||||
function endDragOrResize(e) {
|
||||
if (isDragging || isResizing) {
|
||||
const wasDragging = isDragging;
|
||||
const wasResizing = isResizing;
|
||||
|
||||
// Always reset state first
|
||||
isDragging = false;
|
||||
isResizing = false;
|
||||
|
||||
// Always remove listeners to prevent leaks
|
||||
document.removeEventListener('mousemove', handleMove);
|
||||
document.removeEventListener('touchmove', handleMove);
|
||||
document.removeEventListener('mouseup', endDragOrResize);
|
||||
document.removeEventListener('touchend', endDragOrResize);
|
||||
document.removeEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
// Only save state if we were actually dragging/resizing
|
||||
if (wasDragging || wasResizing) {
|
||||
saveState();
|
||||
// If we were resizing, clear pending timeout and update immediately
|
||||
if (isResizing) {
|
||||
if (wasResizing) {
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = null;
|
||||
@@ -499,31 +570,49 @@ console.error('Error is ' + err);
|
||||
updateFontSize();
|
||||
}
|
||||
}
|
||||
isDragging = false;
|
||||
isResizing = false;
|
||||
}
|
||||
|
||||
// Add event listeners for dragging (on container, but not on resize handle)
|
||||
// Check if touch/click is inside container
|
||||
function isInsideContainer(x, y) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
return (
|
||||
x >= rect.left &&
|
||||
x <= rect.right &&
|
||||
y >= rect.top &&
|
||||
y <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Add event listeners for dragging/resizing on container
|
||||
container.addEventListener('mousedown', function(e) {
|
||||
if (e.target !== resizeHandle) startDrag(e);
|
||||
const coords = getCoordinates(e);
|
||||
// Double check we're actually inside the container
|
||||
if (!isInsideContainer(coords.x, coords.y)) {
|
||||
return;
|
||||
}
|
||||
if (isInResizeHandle(coords.x, coords.y)) {
|
||||
startResize(e);
|
||||
} else {
|
||||
startDrag(e);
|
||||
}
|
||||
});
|
||||
container.addEventListener('touchstart', function(e) {
|
||||
if (e.target !== resizeHandle) startDrag(e);
|
||||
});
|
||||
|
||||
// Add event listeners for resizing (on resize handle)
|
||||
resizeHandle.addEventListener('mousedown', startResize);
|
||||
resizeHandle.addEventListener('touchstart', startResize);
|
||||
|
||||
// Add global move and end listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
const coords = getCoordinates(e);
|
||||
// Double check we're actually inside the container
|
||||
if (!isInsideContainer(coords.x, coords.y)) {
|
||||
return;
|
||||
}
|
||||
if (isInResizeHandle(coords.x, coords.y)) {
|
||||
startResize(e);
|
||||
} else {
|
||||
startDrag(e);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Load saved state and set initial font size
|
||||
loadState();
|
||||
updateFontSize();
|
||||
updateChartPosition();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -818,7 +818,8 @@
|
||||
if (field.max !== undefined) input.max = field.max;
|
||||
input.value = value !== undefined ? value : '';
|
||||
}
|
||||
input.addEventListener(field.type === 'duration' || field.type === 'pace' ? 'change' : 'input', handleFieldChange);
|
||||
// Use 'change' event for duration, pace, and number fields to prevent keyboard from closing during typing
|
||||
input.addEventListener(field.type === 'duration' || field.type === 'pace' || field.type === 'number' ? 'change' : 'input', handleFieldChange);
|
||||
|
||||
// Add +/- buttons for duration, number, and pace fields
|
||||
if (field.type === 'duration' || field.type === 'number' || field.type === 'pace') {
|
||||
@@ -921,7 +922,7 @@
|
||||
state.intervals[index][field.syncWith] = speed;
|
||||
}
|
||||
}
|
||||
// Re-render to update both fields
|
||||
// Re-render to update speed field (pace uses 'change' event so keyboard is already closed)
|
||||
renderIntervals();
|
||||
updateChart();
|
||||
updateStatus();
|
||||
@@ -929,7 +930,7 @@
|
||||
} else if (type === 'number') {
|
||||
const raw = target.value;
|
||||
state.intervals[index][key] = raw === '' ? undefined : Number(raw);
|
||||
// If this is a speed field, re-render to update pace
|
||||
// If this is a speed field, re-render to update pace (uses 'change' event so keyboard is already closed)
|
||||
if (key === 'speed') {
|
||||
renderIntervals();
|
||||
}
|
||||
|
||||
@@ -657,7 +657,7 @@ int main(int argc, char *argv[]) {
|
||||
qInstallMessageHandler(myMessageOutput);
|
||||
qDebug() << QStringLiteral("version ") << app->applicationVersion();
|
||||
foreach (QString s, settings.allKeys()) {
|
||||
if (!s.contains(QStringLiteral("password")) && !s.contains("user_email") && !s.contains("username") && !s.contains("token")) {
|
||||
if (!s.contains(QStringLiteral("password")) && !s.contains("user_email") && !s.contains("username") && !s.contains("token") && !s.contains("garmin_device_serial") && !s.contains("garmin_email")) {
|
||||
|
||||
qDebug() << s << settings.value(s);
|
||||
}
|
||||
|
||||
@@ -935,7 +935,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
text: "version 2.20.23"
|
||||
text: "version 2.20.26"
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
|
||||
@@ -101,11 +101,12 @@ 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/sunnyfitstepper/sunnyfitstepper.cpp \
|
||||
$$PWD/devices/technogymbike/technogymbike.cpp \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.cpp \
|
||||
$$PWD/fitdatabaseprocessor.cpp \
|
||||
@@ -367,6 +368,7 @@ HEADERS += \
|
||||
$$PWD/devices/cycleopsphantombike/cycleopsphantombike.h \
|
||||
$$PWD/devices/deeruntreadmill/deerruntreadmill.h \
|
||||
$$PWD/devices/echelonstairclimber/echelonstairclimber.h \
|
||||
$$PWD/devices/sunnyfitstepper/sunnyfitstepper.h \
|
||||
$$PWD/devices/elitesquarecontroller/elitesquarecontroller.h \
|
||||
$$PWD/devices/focustreadmill/focustreadmill.h \
|
||||
$$PWD/devices/jumprope.h \
|
||||
@@ -379,9 +381,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 \
|
||||
@@ -1006,4 +1008,4 @@ INCLUDEPATH += purchasing/inapp
|
||||
|
||||
WINRT_MANIFEST = AppxManifest.xml
|
||||
|
||||
VERSION = 2.20.23
|
||||
VERSION = 2.20.26
|
||||
|
||||
@@ -389,6 +389,9 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
sessionMesg.SetTotalMovingTime(session.last().elapsedTime);
|
||||
sessionMesg.SetTotalAscent(session.last().elevationGain); // Total elevation gain (meters)
|
||||
sessionMesg.SetTotalDescent(session.last().negativeElevationGain); // Total elevation loss/descent (meters)
|
||||
if (speed_avg > 0) {
|
||||
sessionMesg.SetAvgSpeed(speed_avg / 3.6); // Convert from km/h to m/s
|
||||
}
|
||||
sessionMesg.SetMinAltitude(min_alt);
|
||||
sessionMesg.SetMaxAltitude(max_alt);
|
||||
sessionMesg.SetEvent(FIT_EVENT_SESSION);
|
||||
@@ -454,7 +457,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
sessionMesg.SetAvgStrokeDistance(session.last().avgStrokesLength);
|
||||
} else if (type == STAIRCLIMBER) {
|
||||
|
||||
sessionMesg.SetSport(FIT_SPORT_GENERIC);
|
||||
sessionMesg.SetSport(FIT_SPORT_FITNESS_EQUIPMENT);
|
||||
sessionMesg.SetSubSport(FIT_SUB_SPORT_STAIR_CLIMBING);
|
||||
} else if (type == JUMPROPE) {
|
||||
|
||||
@@ -699,7 +702,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
lapMesg.SetSport(FIT_SPORT_JUMP_ROPE);
|
||||
} else if (type == STAIRCLIMBER) {
|
||||
|
||||
lapMesg.SetSport(FIT_SPORT_GENERIC);
|
||||
lapMesg.SetSport(FIT_SPORT_FITNESS_EQUIPMENT);
|
||||
lapMesg.SetSubSport(FIT_SUB_SPORT_STAIR_CLIMBING);
|
||||
} else {
|
||||
|
||||
@@ -814,7 +817,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;
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ const QString QZSettings::default_user_email = QLatin1String("");
|
||||
const QString QZSettings::user_nickname = QStringLiteral("user_nickname");
|
||||
const QString QZSettings::default_user_nickname = QStringLiteral("");
|
||||
const QString QZSettings::miles_unit = QStringLiteral("miles_unit");
|
||||
const QString QZSettings::weight_kg_unit = QStringLiteral("weight_kg_unit");
|
||||
const QString QZSettings::pause_on_start = QStringLiteral("pause_on_start");
|
||||
const QString QZSettings::treadmill_force_speed = QStringLiteral("treadmill_force_speed");
|
||||
const QString QZSettings::pause_on_start_treadmill = QStringLiteral("pause_on_start_treadmill");
|
||||
@@ -776,6 +777,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");
|
||||
@@ -787,6 +789,7 @@ const QString QZSettings::tile_erg_mode_enabled = QStringLiteral("tile_erg_mode_
|
||||
const QString QZSettings::tile_erg_mode_order = QStringLiteral("tile_erg_mode_order");
|
||||
const QString QZSettings::toorx_srx_3500 = QStringLiteral("toorx_srx_3500");
|
||||
const QString QZSettings::stryd_speed_instead_treadmill = QStringLiteral("stryd_speed_instead_treadmill");
|
||||
const QString QZSettings::stryd_speed_correction_gain = QStringLiteral("stryd_speed_correction_gain");
|
||||
const QString QZSettings::inclination_delay_seconds = QStringLiteral("inclination_delay_seconds");
|
||||
const QString QZSettings::ergDataPoints = QStringLiteral("ergDataPoints");
|
||||
const QString QZSettings::default_ergDataPoints = QStringLiteral("");
|
||||
@@ -1050,7 +1053,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 = 859;
|
||||
|
||||
QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
|
||||
@@ -1110,6 +1113,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::user_email, QZSettings::default_user_email},
|
||||
{QZSettings::user_nickname, QZSettings::default_user_nickname},
|
||||
{QZSettings::miles_unit, QZSettings::default_miles_unit},
|
||||
{QZSettings::weight_kg_unit, QZSettings::default_weight_kg_unit},
|
||||
{QZSettings::pause_on_start, QZSettings::default_pause_on_start},
|
||||
{QZSettings::treadmill_force_speed, QZSettings::default_treadmill_force_speed},
|
||||
{QZSettings::pause_on_start_treadmill, QZSettings::default_pause_on_start_treadmill},
|
||||
@@ -1696,6 +1700,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},
|
||||
@@ -1707,6 +1712,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::tile_erg_mode_order, QZSettings::default_tile_erg_mode_order},
|
||||
{QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500},
|
||||
{QZSettings::stryd_speed_instead_treadmill, QZSettings::default_stryd_speed_instead_treadmill},
|
||||
{QZSettings::stryd_speed_correction_gain, QZSettings::default_stryd_speed_correction_gain},
|
||||
{QZSettings::inclination_delay_seconds, QZSettings::default_inclination_delay_seconds},
|
||||
{QZSettings::ergDataPoints, QZSettings::default_ergDataPoints},
|
||||
{QZSettings::proform_carbon_tl, QZSettings::default_proform_carbon_tl},
|
||||
|
||||
@@ -272,6 +272,12 @@ class QZSettings {
|
||||
static const QString miles_unit;
|
||||
static constexpr bool default_miles_unit = false;
|
||||
|
||||
/**
|
||||
*@brief Use kg for weight even when miles_unit is true (for UK users).
|
||||
*/
|
||||
static const QString weight_kg_unit;
|
||||
static constexpr bool default_weight_kg_unit = false;
|
||||
|
||||
static const QString pause_on_start;
|
||||
static constexpr bool default_pause_on_start = false;
|
||||
|
||||
@@ -2140,6 +2146,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;
|
||||
|
||||
@@ -2175,6 +2184,8 @@ class QZSettings {
|
||||
|
||||
static const QString stryd_speed_instead_treadmill;
|
||||
static constexpr bool default_stryd_speed_instead_treadmill = false;
|
||||
static const QString stryd_speed_correction_gain;
|
||||
static constexpr double default_stryd_speed_correction_gain = 1.0;
|
||||
static const QString inclination_delay_seconds;
|
||||
static constexpr float default_inclination_delay_seconds = 0.0;
|
||||
|
||||
|
||||
163
src/settings.qml
163
src/settings.qml
@@ -1273,6 +1273,9 @@ 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
|
||||
property bool weight_kg_unit: false
|
||||
property real stryd_speed_correction_gain: 1.0
|
||||
}
|
||||
|
||||
|
||||
@@ -1359,12 +1362,12 @@ import Qt.labs.platform 1.1
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelWeight
|
||||
text: qsTr("Player Weight") + "(" + (settings.miles_unit?"lbs":"kg") + ")"
|
||||
text: qsTr("Player Weight") + "(" + ((settings.miles_unit && !settings.weight_kg_unit)?"lbs":"kg") + ")"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
TextField {
|
||||
id: weightTextField
|
||||
text: (settings.miles_unit?settings.weight * 2.20462:settings.weight)
|
||||
text: ((settings.miles_unit && !settings.weight_kg_unit)?settings.weight * 2.20462:settings.weight)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
@@ -1376,11 +1379,11 @@ import Qt.labs.platform 1.1
|
||||
id: okWeightButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: { settings.weight = (settings.miles_unit?weightTextField.text / 2.20462:weightTextField.text); toast.show("Setting saved!"); }
|
||||
onClicked: { settings.weight = ((settings.miles_unit && !settings.weight_kg_unit)?weightTextField.text / 2.20462:weightTextField.text); toast.show("Setting saved!"); }
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: qsTr("Enter your weight in kilograms so QZ can more accurately calculate calories burned. NOTE: If you choose to use miles as the unit for distance traveled, you will be asked to enter your weight in pounds (lbs).")
|
||||
text: qsTr("Enter your weight in kilograms so QZ can more accurately calculate calories burned. NOTE: If you choose to use miles as the unit for distance traveled, you will be asked to enter your weight in pounds (lbs) unless you enable 'Use kg for weight'.")
|
||||
font.bold: true
|
||||
font.italic: true
|
||||
font.pixelSize: Qt.application.font.pixelSize - 2
|
||||
@@ -1706,6 +1709,36 @@ import Qt.labs.platform 1.1
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: weightKgUnitDelegate
|
||||
text: qsTr("Use kg for weight")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.weight_kg_unit
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: settings.weight_kg_unit = checked
|
||||
visible: settings.miles_unit
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Turn on if you want to use kilograms (kg) for weight instead of pounds (lbs). Useful for UK users who use miles for distance but kg for weight.")
|
||||
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)
|
||||
visible: settings.miles_unit
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: pauseOnStartDelegate
|
||||
text: qsTr("Pause when App Starts")
|
||||
@@ -2498,12 +2531,12 @@ import Qt.labs.platform 1.1
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelBikeWeight
|
||||
text: qsTr("Bike Weight") + "(" + (settings.miles_unit?"lbs":"kg") + ")"
|
||||
text: qsTr("Bike Weight") + "(" + ((settings.miles_unit && !settings.weight_kg_unit)?"lbs":"kg") + ")"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
TextField {
|
||||
id: bikeweightTextField
|
||||
text: (settings.miles_unit?settings.bike_weight * 2.20462:settings.bike_weight)
|
||||
text: ((settings.miles_unit && !settings.weight_kg_unit)?settings.bike_weight * 2.20462:settings.bike_weight)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
@@ -2515,12 +2548,12 @@ import Qt.labs.platform 1.1
|
||||
id: okBikeWeightButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: { settings.bike_weight = (settings.miles_unit?bikeweightTextField.text / 2.20462:bikeweightTextField.text); toast.show("Setting saved!"); }
|
||||
onClicked: { settings.bike_weight = ((settings.miles_unit && !settings.weight_kg_unit)?bikeweightTextField.text / 2.20462:bikeweightTextField.text); toast.show("Setting saved!"); }
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Enables QZ to include the weight of your bike when calculating speed. For example, if you are competing against yourself on VZfit, adding bike weight will “level the playing field” against your virtual self. If you have set QZ to calculate distance in miles, enter the bike weight in pounds (lbs). Default unit is kilograms (kgs).")
|
||||
text: qsTr("Enables QZ to include the weight of your bike when calculating speed. For example, if you are competing against yourself on VZfit, adding bike weight will 'level the playing field' against your virtual self. If you have set QZ to calculate distance in miles, enter the bike weight in pounds (lbs) unless you enable 'Use kg for weight'. Default unit is kilograms (kgs).")
|
||||
font.bold: true
|
||||
font.italic: true
|
||||
font.pixelSize: Qt.application.font.pixelSize - 2
|
||||
@@ -6726,6 +6759,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
|
||||
@@ -11833,6 +11889,60 @@ import Qt.labs.platform 1.1
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: settings.stryd_speed_instead_treadmill
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelStrydSpeedCorrectionGain
|
||||
text: qsTr("Speed Correction Gain:")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
TextField {
|
||||
id: strydSpeedCorrectionGainTextField
|
||||
text: settings.stryd_speed_correction_gain.toFixed(2)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
onActiveFocusChanged: if(this.focus) this.selectAll()
|
||||
onAccepted: {
|
||||
var value = parseFloat(text)
|
||||
if (value >= 0.0 && value <= 1.0) {
|
||||
settings.stryd_speed_correction_gain = value
|
||||
} else {
|
||||
text = settings.stryd_speed_correction_gain.toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: okStrydSpeedCorrectionGainButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
var value = parseFloat(strydSpeedCorrectionGainTextField.text)
|
||||
if (value >= 0.0 && value <= 1.0) {
|
||||
settings.stryd_speed_correction_gain = value
|
||||
} else {
|
||||
strydSpeedCorrectionGainTextField.text = settings.stryd_speed_correction_gain.toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: settings.stryd_speed_instead_treadmill
|
||||
text: qsTr("Controls how aggressively QZ corrects treadmill speed based on sensor readings. A value of 1.0 applies full correction immediately (may cause oscillations). Lower values (e.g., 0.3) apply gradual corrections to avoid speed fluctuations. Range: 0.0 - 1.0. Default: 1.0")
|
||||
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)
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
text: qsTr("Use inclination from the power sensor")
|
||||
spacing: 0
|
||||
@@ -12744,6 +12854,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)
|
||||
|
||||
@@ -1084,6 +1084,42 @@ void TemplateInfoSenderBuilder::onGearsMinus(const QJsonValue &msgContent, Templ
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onSpeedPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit speed_Plus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_speed_plus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onSpeedMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit speed_Minus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_speed_minus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onInclinationPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit inclination_Plus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_inclination_plus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onInclinationMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit inclination_Minus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_inclination_minus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onPelotonStartWorkout(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
@@ -1255,6 +1291,18 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) {
|
||||
} else if (msg == QStringLiteral("gears_minus")) {
|
||||
onGearsMinus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("speed_plus")) {
|
||||
onSpeedPlus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("speed_minus")) {
|
||||
onSpeedMinus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("inclination_plus")) {
|
||||
onInclinationPlus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("inclination_minus")) {
|
||||
onInclinationMinus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("peloton_start_workout")) {
|
||||
onPelotonStartWorkout(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
|
||||
@@ -34,6 +34,10 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
void pelotonOffset_Minus();
|
||||
void gears_Plus();
|
||||
void gears_Minus();
|
||||
void speed_Plus();
|
||||
void speed_Minus();
|
||||
void inclination_Plus();
|
||||
void inclination_Minus();
|
||||
int pelotonOffset();
|
||||
bool pelotonAskStart();
|
||||
void peloton_start_workout();
|
||||
@@ -79,6 +83,10 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
void onPelotonOffsetMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onGearsPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onGearsMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onSpeedPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onSpeedMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onInclinationPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onInclinationMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onPelotonStartWorkout(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onPelotonAbortWorkout(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onFloatingClose(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
|
||||
225
tst/Devices/TestSunnyfitStepper.h
Normal file
225
tst/Devices/TestSunnyfitStepper.h
Normal file
@@ -0,0 +1,225 @@
|
||||
#pragma once
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QByteArray>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief Sunnyfit Mini Stepper (SF-S) BLE Packet Test Data
|
||||
*
|
||||
* Extracted from btsnoop_hci.log capture of actual device communication.
|
||||
* These are the 20-byte data frames (0x5a 0x05) from the capture file.
|
||||
*/
|
||||
class SunnyfitStepperTestData {
|
||||
public:
|
||||
/**
|
||||
* @brief Raw 20-byte data frames captured from actual device
|
||||
* Format: 0x5a (start) + 0x05 (command) + 18 bytes of data
|
||||
*
|
||||
* Byte positions:
|
||||
* [0]: 0x5a (start marker)
|
||||
* [1]: 0x05 (command type - data frame)
|
||||
* [6]: Cadence (SPM) - single byte
|
||||
* [16]: Step Counter (increments 0, 1, 2, 3...)
|
||||
*/
|
||||
static const std::vector<QByteArray> getTestFrames() {
|
||||
return {
|
||||
// Frame 0: cadence=0, step=0
|
||||
QByteArray::fromHex("5a05001a032200000524000000000003260000052900"),
|
||||
|
||||
// Frame 1: cadence=0, step=1
|
||||
QByteArray::fromHex("5a05001a032200000524010000000003260100052900"),
|
||||
|
||||
// Frame 2: cadence=0, step=2
|
||||
QByteArray::fromHex("5a05001a032200000524020000000003260200052900"),
|
||||
|
||||
// Frame 3: cadence=32, step=3
|
||||
QByteArray::fromHex("5a05001a032220000524020000000003260300052900"),
|
||||
|
||||
// Frame 4: cadence=67, step=4
|
||||
QByteArray::fromHex("5a05001a032243000524040000000003260400052900"),
|
||||
|
||||
// Frame 5: cadence=67, step=5
|
||||
QByteArray::fromHex("5a05001a032243000524040000000003260500052900"),
|
||||
|
||||
// Frame 6: cadence=67, step=6
|
||||
QByteArray::fromHex("5a05001a032243000524040000000003260600052900"),
|
||||
|
||||
// Frame 7: cadence=20, step=7
|
||||
QByteArray::fromHex("5a05001a032214000524050000000003260700052900"),
|
||||
|
||||
// Frame 8: cadence=53, step=8
|
||||
QByteArray::fromHex("5a05001a032235000524070000000003260800052900"),
|
||||
|
||||
// Frame 9: cadence=63, step=9
|
||||
QByteArray::fromHex("5a05001a03223f000524080000000003260900052900"),
|
||||
|
||||
// Frame 10: cadence=63, step=10
|
||||
QByteArray::fromHex("5a05001a03223f000524080000000003260a00052900"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Expected extracted values from each test frame
|
||||
*/
|
||||
struct ExpectedMetrics {
|
||||
int frameIndex;
|
||||
double expectedCadence;
|
||||
int expectedStepCount;
|
||||
double expectedSpeed; // cadence / 3.2
|
||||
};
|
||||
|
||||
static const std::vector<ExpectedMetrics> getExpectedValues() {
|
||||
return {
|
||||
{0, 0.0, 0, 0.0}, // cadence=0, step=0
|
||||
{1, 0.0, 1, 0.0}, // cadence=0, step=1
|
||||
{2, 0.0, 2, 0.0}, // cadence=0, step=2
|
||||
{3, 32.0, 3, 10.0}, // cadence=32, step=3, speed=32/3.2=10
|
||||
{4, 67.0, 4, 20.9375}, // cadence=67, step=4, speed=67/3.2≈20.94
|
||||
{5, 67.0, 5, 20.9375}, // cadence=67, step=5
|
||||
{6, 67.0, 6, 20.9375}, // cadence=67, step=6
|
||||
{7, 20.0, 7, 6.25}, // cadence=20, step=7, speed=20/3.2=6.25
|
||||
{8, 53.0, 8, 16.5625}, // cadence=53, step=8, speed=53/3.2≈16.56
|
||||
{9, 63.0, 9, 19.6875}, // cadence=63, step=9, speed=63/3.2≈19.69
|
||||
{10, 63.0, 10, 19.6875}, // cadence=63, step=10
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a single 20-byte frame and extract metrics
|
||||
* @return pair<cadence, stepCount> or returns {-1, -1} on error
|
||||
*/
|
||||
static std::pair<double, int> parseFrame(const QByteArray& frame) {
|
||||
if (frame.length() != 20) {
|
||||
return {-1, -1};
|
||||
}
|
||||
|
||||
if ((uint8_t)frame[0] != 0x5a) {
|
||||
return {-1, -1};
|
||||
}
|
||||
|
||||
// Extract cadence from byte 6 (single byte)
|
||||
double cadence = (double)(uint8_t)frame[6];
|
||||
|
||||
// Extract step counter from byte 16 (single byte, little-endian)
|
||||
int stepCount = (uint8_t)frame[16];
|
||||
|
||||
return {cadence, stepCount};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test suite for Sunnyfit Stepper frame parsing
|
||||
*/
|
||||
class SunnyfitStepperParsingTest : public testing::Test {
|
||||
protected:
|
||||
SunnyfitStepperTestData testData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test parsing of individual frames
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, ParseFrames) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
auto expectedValues = SunnyfitStepperTestData::getExpectedValues();
|
||||
|
||||
ASSERT_EQ(frames.size(), expectedValues.size())
|
||||
<< "Test data mismatch: frames and expected values should have same size";
|
||||
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frames[i]);
|
||||
|
||||
EXPECT_EQ(cadence, expectedValues[i].expectedCadence)
|
||||
<< "Frame " << i << ": Cadence mismatch";
|
||||
|
||||
EXPECT_EQ(stepCount, expectedValues[i].expectedStepCount)
|
||||
<< "Frame " << i << ": Step count mismatch";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test speed calculation from cadence
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, CalculateSpeed) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
auto expectedValues = SunnyfitStepperTestData::getExpectedValues();
|
||||
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frames[i]);
|
||||
|
||||
double calculatedSpeed = cadence / 3.2;
|
||||
|
||||
EXPECT_DOUBLE_EQ(calculatedSpeed, expectedValues[i].expectedSpeed)
|
||||
<< "Frame " << i << ": Speed calculation mismatch (cadence=" << cadence << ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test step counter increments
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, StepCounterIncrement) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
auto expectedValues = SunnyfitStepperTestData::getExpectedValues();
|
||||
|
||||
int previousSteps = -1;
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frames[i]);
|
||||
|
||||
if (previousSteps >= 0) {
|
||||
int increment = stepCount - previousSteps;
|
||||
EXPECT_EQ(increment, 1)
|
||||
<< "Frame " << i << ": Step counter should increment by 1 (was "
|
||||
<< previousSteps << ", now " << stepCount << ")";
|
||||
}
|
||||
previousSteps = stepCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test cadence variation detection
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, CadenceVariation) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
|
||||
std::vector<double> cadences;
|
||||
for (const auto& frame : frames) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frame);
|
||||
cadences.push_back(cadence);
|
||||
}
|
||||
|
||||
// Verify we have cadence variation in the test data
|
||||
double minCadence = *std::min_element(cadences.begin(), cadences.end());
|
||||
double maxCadence = *std::max_element(cadences.begin(), cadences.end());
|
||||
|
||||
EXPECT_LT(minCadence, maxCadence)
|
||||
<< "Test data should have cadence variation";
|
||||
|
||||
EXPECT_EQ(minCadence, 0.0)
|
||||
<< "Minimum cadence should be 0";
|
||||
|
||||
EXPECT_EQ(maxCadence, 67.0)
|
||||
<< "Maximum cadence should be 67";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test frame validation
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, FrameValidation) {
|
||||
// Invalid length
|
||||
QByteArray shortFrame = QByteArray::fromHex("5a05001a0322");
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(shortFrame);
|
||||
EXPECT_EQ(cadence, -1) << "Should reject short frames";
|
||||
EXPECT_EQ(stepCount, -1) << "Should reject short frames";
|
||||
|
||||
// Invalid start marker
|
||||
QByteArray invalidStart = QByteArray::fromHex("0105001a032200000524000000000003260000052900");
|
||||
std::tie(cadence, stepCount) = SunnyfitStepperTestData::parseFrame(invalidStart);
|
||||
EXPECT_EQ(cadence, -1) << "Should reject frames with invalid start marker";
|
||||
EXPECT_EQ(stepCount, -1) << "Should reject frames with invalid start marker";
|
||||
|
||||
// Valid frame
|
||||
QByteArray validFrame = SunnyfitStepperTestData::getTestFrames()[3];
|
||||
std::tie(cadence, stepCount) = SunnyfitStepperTestData::parseFrame(validFrame);
|
||||
EXPECT_EQ(cadence, 32.0) << "Should parse valid frame";
|
||||
EXPECT_EQ(stepCount, 3) << "Should parse valid frame";
|
||||
}
|
||||
@@ -54,6 +54,7 @@ HEADERS += \
|
||||
Devices/deviceindex.h \
|
||||
Devices/devicenamepatterngroup.h \
|
||||
Devices/devicetestdataindex.h \
|
||||
Devices/TestSunnyfitStepper.h \
|
||||
Erg/ergtabletestsuite.h \
|
||||
GarminConnect/garminconnecttestsuite.h \
|
||||
ToolTests/qfittestsuite.h \
|
||||
|
||||
Reference in New Issue
Block a user