Compare commits

..

33 Commits

Author SHA1 Message Date
Roberto Viola
4a732edbd5 a lot of improvement on the charts! 2021-04-01 16:20:30 +02:00
Roberto Viola
1c20a2c77d power, heart and cadence chart working! 2021-04-01 12:21:53 +02:00
Roberto Viola
a6b7f4ff94 iOS version 2.5.16 2021-04-01 08:00:47 +02:00
Roberto Viola
a42d218eda Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2021-03-31 16:16:41 +02:00
Roberto Viola
48d985cf67 added peloton activity name to strava 2021-03-31 16:15:04 +02:00
Roberto Viola
43bb830c23 workout name on peloton and password hidden on peloton setting 2021-03-31 15:40:41 +02:00
Roberto Viola
82888440cf first version working on MacOS 2021-03-31 13:54:15 +02:00
Roberto Viola
f45de06bcf first commit with peloton following workout feature (NOT TESTED) 2021-03-31 11:33:40 +02:00
Roberto Viola
6f081cc6b4 all the API implemented! 2021-03-30 22:54:30 +02:00
Roberto Viola
11c137e0e3 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2021-03-30 18:39:43 +02:00
Roberto Viola
dea69cc74a QT iOS patches in order to restore the correct functionality on the Echelon bikes 2021-03-30 18:38:29 +02:00
Roberto Viola
e7eb0822e7 Merge pull request #228 from ben75020/docs/usage
Documentation about usage
2021-03-30 17:38:55 +02:00
ben75020
1943a08632 Merge branch 'master' into docs/usage 2021-03-30 17:37:36 +02:00
Benjamin.Riou
167dc93a55 Documentation about usage 2021-03-30 17:33:49 +02:00
Roberto Viola
9159af36f7 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2021-03-30 13:37:17 +02:00
Roberto Viola
2763ce6e8a added binary patched from iOS 2021-03-30 13:37:05 +02:00
Roberto Viola
f4c6dfaeb6 m3ibike kcal method setting 2021-03-30 11:58:52 +02:00
Roberto Viola
bda4f5cf6b Merge pull request #222 from ben75020/docs/update1
Docs Update on CLI usage and installation
2021-03-28 18:34:43 +02:00
Benjamin.Riou
5b33d479e0 Docs Update on CLI usage and installation 2021-03-28 18:28:11 +02:00
Roberto Viola
907d494803 esanglinker compatibility added 2021-03-28 14:38:35 +02:00
Roberto Viola
1c4a3e6185 Merge pull request #220 from ben75020/docs/supported
Docs/supported devices
2021-03-28 12:21:36 +02:00
ben75020
b11a0cca1c Update 20_supported_devices_and_applications.md 2021-03-28 10:03:40 +02:00
ben75020
dde586ecdd Update 20_supported_devices_and_applications.md 2021-03-28 10:02:45 +02:00
ben75020
390cf9bfef Update 21_applications_detail.md 2021-03-28 10:01:17 +02:00
Benjamin.Riou
758349b80f Add documentation on devices and software 2021-03-28 09:59:01 +02:00
Benjamin.Riou
896c641851 Documentation 2021-03-28 08:24:53 +02:00
Benjamin.Riou
5cb44c17e8 Documentation 2021-03-28 08:24:53 +02:00
Roberto Viola
2c124f4365 Merge pull request #219 from p3g4asus/QMLTests
Qml tests
2021-03-27 20:25:07 +01:00
Roberto Viola
6d4c030754 heartratebelt on iOS using UUID as for schwinnbike 2021-03-27 18:57:49 +01:00
Matteo Federico Zazzetta
0fad920c39 New design for peloton options 2021-03-27 18:42:53 +01:00
Matteo Federico Zazzetta
6bd2327165 Fix heart rate settings 2021-03-27 18:14:50 +01:00
Matteo Federico Zazzetta
497865f2e6 Some minor changes 2021-03-27 18:02:20 +01:00
Matteo Federico Zazzetta
b279344dbc Setting redesign 2021-03-27 16:59:45 +01:00
53 changed files with 3432 additions and 2499 deletions

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@ src/ui_charts.h
src/ui_mainwindow.h
src/debug-*
*.swo
*.swp

View File

@@ -39,35 +39,9 @@ Read the [installation procedure](docs/10_Installation.md)
### Tested on
- Raspberry PI 0W and Domyos Intense Run
You can run the app on [Macintosh or Linux devices](docs/10_Installation.md). IOS and Android are also supported.
- MacBook Air 2011 and Domyos Intense Run
- Raspberry 3b+ and Domyos T900C
- Raspberry 3b+ and Toorx TRX Route Key
- Android Pixel 2 and Echelon Connect Sport
- Raspberry Pi 0W and SportsTech ESX500
### Your machine is not compatible?
Open an issue and follow these steps!
1. first of all you need an android device (phone or tablet)
2. you need to become developer on your phone https://wccftech.com/how-to/how-to-enable-developer-options-on-android-10-tutorial/
3. Go to Settings
4. Go into developer options
5. Enable the option Enable Bluetooth HCI snoop log
6. restart your phone
7. open your machine app and play with it collecting inclination and speed
8. Disable the option Enable Bluetooth HCI snoop log
9. in Developer Options: Bug report->Full report
10. wait a random amount of time (10-20 seconds)
11. A notification will appear at the top of the device. Click on it, share, email it to yourself
12. You'll get a zip file with the entire report. In the FS/Data/Log/bt directory of the zipfile is the file you want.
13. attach the log file in a new issue with a short description of the steps you did in the app when you used it
QDomyos-Zwift works on every [FTMS-compatible application](docs/20_supported_devices_and_applications.md), and virtually any [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
### No gui version
@@ -81,6 +55,8 @@ https://github.com/ProH4Ck/treadmill-bridge
https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/
Icons used in this documentation comes from [flaticon.com](https://www.flaticon.com)
### Blog
https://robertoviola.cloud

View File

@@ -149,6 +149,9 @@
876EE3CDDF69DA139329ADD8 /* qquicklayoutsplugin in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 307B2F8E9A717B514EA118E0 /* qquicklayoutsplugin */; };
8772A0E625E43ADB0080718C /* trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */; };
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */; };
8781907E2615089D0085E656 /* peloton.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8781907C2615089D0085E656 /* peloton.cpp */; };
87819080261508B10085E656 /* moc_peloton.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8781907F261508B10085E656 /* moc_peloton.cpp */; };
8781908526150C8E0085E656 /* libqtlabsplatformplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 8781908126150B490085E656 /* libqtlabsplatformplugin.a */; };
8783153B25E8D81E0007817C /* moc_sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */; };
8783153C25E8DAFD0007817C /* sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3EBBA25D2CFED0040EB4C /* sportstechbike.cpp */; };
878A331A25AB4FF800BD13E1 /* yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331725AB4FF800BD13E1 /* yesoulbike.cpp */; };
@@ -556,6 +559,11 @@
8772A0E425E43AD90080718C /* trxappgateusbbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = trxappgateusbbike.h; path = ../src/trxappgateusbbike.h; sourceTree = "<group>"; };
8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = trxappgateusbbike.cpp; path = ../src/trxappgateusbbike.cpp; sourceTree = "<group>"; };
8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_trxappgateusbbike.cpp; sourceTree = "<group>"; };
8781907C2615089D0085E656 /* peloton.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = peloton.cpp; path = ../src/peloton.cpp; sourceTree = "<group>"; };
8781907D2615089D0085E656 /* peloton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = peloton.h; path = ../src/peloton.h; sourceTree = "<group>"; };
8781907F261508B10085E656 /* moc_peloton.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_peloton.cpp; sourceTree = "<group>"; };
8781908126150B490085E656 /* libqtlabsplatformplugin.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtlabsplatformplugin.a; path = ../../Qt/5.15.2/ios/qml/Qt/labs/platform/libqtlabsplatformplugin.a; sourceTree = "<group>"; };
8781908326150C5B0085E656 /* libqtlabsplatformplugin_debug.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtlabsplatformplugin_debug.a; path = ../../Qt/5.15.2/ios/qml/Qt/labs/platform/libqtlabsplatformplugin_debug.a; sourceTree = "<group>"; };
878225C234983ACB863D2D29 /* fit_nmea_sentence_mesg.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_nmea_sentence_mesg.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_nmea_sentence_mesg.hpp"; sourceTree = "<absolute>"; };
8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechbike.cpp; sourceTree = "<group>"; };
87842E7E25AF88FB00321E69 /* secret.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = secret.h; path = ../src/secret.h; sourceTree = "<group>"; };
@@ -816,6 +824,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8781908526150C8E0085E656 /* libqtlabsplatformplugin.a in Link Binary With Libraries */,
38B66B7A90741F136A02EDEB /* qios in Link Binary With Libraries */,
B460F624007324313696BE86 /* QuartzCore.framework in Link Binary With Libraries */,
4EB68D0C8182BCE33529C421 /* AudioToolbox.framework in Link Binary With Libraries */,
@@ -928,6 +937,7 @@
25B08E2869634E9BCBA333A2 /* Generated Sources */ = {
isa = PBXGroup;
children = (
8781907F261508B10085E656 /* moc_peloton.cpp */,
87D269A225F535300076AA48 /* moc_m3ibike.cpp */,
87D269A125F535300076AA48 /* moc_skandikawiribike.cpp */,
87B617EF25F260140094A1CB /* moc_fitshowtreadmill.cpp */,
@@ -975,6 +985,8 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
8781907C2615089D0085E656 /* peloton.cpp */,
8781907D2615089D0085E656 /* peloton.h */,
8762D5112601F89500F6F049 /* scanrecordresult.cpp */,
8762D5122601F89500F6F049 /* scanrecordresult.h */,
8762D50E2601F7EA00F6F049 /* M3iIOS-Interface.h */,
@@ -1145,6 +1157,8 @@
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
isa = PBXGroup;
children = (
8781908326150C5B0085E656 /* libqtlabsplatformplugin_debug.a */,
8781908126150B490085E656 /* libqtlabsplatformplugin.a */,
2D4A13931169E5681CE465F0 /* Qt5NetworkAuth */,
BC3A8C3E433A8FA00BB15F07 /* qios */,
7EC00404ACD5AB0E97726B0E /* QuartzCore.framework */,
@@ -1749,6 +1763,7 @@
DD2E0091F3318F053D2995AA /* fit_mesg_broadcaster.cpp in Compile Sources */,
FE77C778768741F1A161682E /* fit_mesg_definition.cpp in Compile Sources */,
87DF68B825E2673B00FCDA46 /* eslinkertreadmill.cpp in Compile Sources */,
8781907E2615089D0085E656 /* peloton.cpp in Compile Sources */,
2B800DC34C91D8B080DEFBE8 /* fit_mesg_with_event_broadcaster.cpp in Compile Sources */,
6DC5D7C695B8763F9E2E029F /* fit_profile.cpp in Compile Sources */,
23191C28CB29474279752FD3 /* fit_protocol_validator.cpp in Compile Sources */,
@@ -1767,6 +1782,7 @@
47E45EE0BB22C1E4332F1D1D /* trxappgateusbtreadmill.cpp in Compile Sources */,
6943DA124B60175E1F9EBD1B /* virtualbike.cpp in Compile Sources */,
0317752B0C295CAB82D37E45 /* virtualtreadmill.cpp in Compile Sources */,
87819080261508B10085E656 /* moc_peloton.cpp in Compile Sources */,
7EC1321DD83EAAFAA2B7109C /* domyosbike.cpp in Compile Sources */,
614192CB787D12C3E98ADE55 /* lockscreen.mm in Compile Sources */,
0F974CB18B3E792B42270F19 /* FitDecode.mm in Compile Sources */,
@@ -2164,7 +2180,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2.5.11;
CURRENT_PROJECT_VERSION = 2.5.16;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
HEADER_SEARCH_PATHS = (
@@ -2187,6 +2203,37 @@
"../../Qt/5.15.2/ios/mkspecs/macx-ios-clang",
../../Qt/5.15.2/ios/include/QtNetworkAuth,
);
LIBRARY_SEARCH_PATHS = (
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
/System/Library/Frameworks/,
/Users/cagnulein/Qt/5.15.2/ios/lib,
/Users/cagnulein/Qt/5.15.2/ios/plugins/imageformats,
/Users/cagnulein/Qt/5.15.2/ios/plugins/position,
/Users/cagnulein/Qt/5.15.2/ios/lib,
/Users/cagnulein/Qt/5.15.2/ios/plugins/qmltooling,
/Users/cagnulein/Qt/5.15.2/ios/plugins/bearer,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtGraphicalEffects/private,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQml/Models.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQml/WorkerScript.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Dialogs,
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel,
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/platform,
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/settings,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Dialogs/Private,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/PrivateWidgets,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQml,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Layouts,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Window.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Templates.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Fusion,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Universal,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtGraphicalEffects,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Imagine,
);
MARKETING_VERSION = 2.5;
PRODUCT_BUNDLE_IDENTIFIER = "org.cagnulein.${PRODUCT_NAME:rfc1034identifier}";
PRODUCT_NAME = qdomyoszwift;
@@ -2216,7 +2263,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2.5.11;
CURRENT_PROJECT_VERSION = 2.5.16;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -2241,6 +2288,37 @@
"../../Qt/5.15.2/ios/mkspecs/macx-ios-clang",
../../Qt/5.15.2/ios/include/QtNetworkAuth,
);
LIBRARY_SEARCH_PATHS = (
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
/System/Library/Frameworks/,
/Users/cagnulein/Qt/5.15.2/ios/lib,
/Users/cagnulein/Qt/5.15.2/ios/plugins/imageformats,
/Users/cagnulein/Qt/5.15.2/ios/plugins/position,
/Users/cagnulein/Qt/5.15.2/ios/lib,
/Users/cagnulein/Qt/5.15.2/ios/plugins/qmltooling,
/Users/cagnulein/Qt/5.15.2/ios/plugins/bearer,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtGraphicalEffects/private,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQml/Models.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQml/WorkerScript.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Dialogs,
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel,
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/platform,
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/settings,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Dialogs/Private,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/PrivateWidgets,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQml,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Layouts,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Window.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Templates.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Fusion,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Universal,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtGraphicalEffects,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtQuick/Controls.2/Imagine,
);
MARKETING_VERSION = 2.5;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "org.cagnulein.${PRODUCT_NAME:rfc1034identifier}";
@@ -2304,7 +2382,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2.5.11;
CURRENT_PROJECT_VERSION = 2.5.16;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -2394,7 +2472,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2.5.11;
CURRENT_PROJECT_VERSION = 2.5.16;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_NS_ASSERTIONS = NO;
@@ -2478,7 +2556,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2.5.11;
CURRENT_PROJECT_VERSION = 2.5.16;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_PREVIEWS = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -2565,7 +2643,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2.5.11;
CURRENT_PROJECT_VERSION = 2.5.16;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_NS_ASSERTIONS = NO;

View File

@@ -1,17 +1,23 @@
# Installation
QDomyos-Zwift can be installed from source on MacOs, Linux, Android and IOS.
Once you've installed QDomyos-Zwift, you can access the [operation guide](30_usage.md) for more information.
## On a Linux System (from source)
```buildoutcfg
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default libqt5networkauth5-dev
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd qdomyos-zwift
$ cd src
$ qmake
$ make -j4
$ sudo ./qdomyos-zwift
```
## MacOs installation
You will need to (at a minimum) to install the xcode Command Line Tools (CLI) thanks to @richardwait
@@ -97,6 +103,7 @@ This operation takes a moment to complete.
`sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default libqt5networkauth5-dev`
`git clone https://github.com/cagnulein/qdomyos-zwift.git`
`cd qdomyos-zwift`
`cd src`
`qmake`
`make`

View File

@@ -0,0 +1,63 @@
# Supported applications
QDomyos-Zwift should support every application able to read a [FTMS](specs/FTMS_v1.0.pdf) bluetooth signal.
This list is not exhaustive. Please report any application known to be working with QDomyos-Zwift !
|Application|Sport|Platform|Speed|RPM|Power|HRM |Resistance Control|
|-----------|-----|--------|------------|---|-----|-----|----------------|
|[Zwift](21_applications_detail.md#zwift)|![bike](img/20_bike.png) ![run](img/20_treadmill.png)|![IOS](img/20_apple.png) ![Android](img/20_android.png) ![PC](img/20_windows.png)| Yes|Yes|Yes| Yes, no FTMS support | Yes |
|[BKool](21_applications_detail.md#bkool) |![bike](img/20_bike.png)|![IOS](img/20_apple.png) ![Android](img/20_android.png) ![PC](img/20_windows.png)|Yes|Yes|Yes|Yes| Yes |
|[Fulgaz](21_applications_detail.md#fulgaz)|![bike](img/20_bike.png)|![IOS](img/20_apple.png) ![Android](img/20_android.png) ![PC](img/20_windows.png)|Yes|Yes|Yes|Yes, no FTMS support (see note)|Yes (see note) |
# Supported devices
This list is not exhaustive.
Try the qdomyos app with your fitness appliance and report how it is going.
If it's not working, you can [ask for your device to be supported](#ask-for-device-support)
## Supported bikes
|Manufacturer|Model|Speed|RPM|Power|HRM|Resistence Control|
|------------|-----|------------|---|-----|---|------------------|
|[Echelon](22_devices_detail.md#echelon)|Connect Sport|Yes|Yes|Yes|Yes|N/A|
|[Sportstech](22_devices_detail.md#sportstech)|ESX500|Yes|Yes|Yes|Yes|Yes|
## Supported treadmills
|Manufacturer|Model|Speed|HRM|Inclinaison Control| Speed control|
|------------|-----|------------|---|-------------------|--------------|
|Domyos|Intense Run|Yes|Yes|Yes|Yes|
|Domyos|T900c|Yes|Yes|Yes|Yes|
|Toorx|TRX Route Key|Yes|Yes|Yes|Yes|
# Ask for device support
You can ask for supporting a device by opening an issue and following these steps.
We need to "spy" the bluetooth activity from your fitness device and it's application, in order to guess how they communicate.
An android device is required for this operation.
## Android device
1. first of all you need an android device (phone or tablet)
2. you need to become developer on your phone https://wccftech.com/how-to/how-to-enable-developer-options-on-android-10-tutorial/
3. Go to Settings
4. Go into developer options
5. Enable the option Enable Bluetooth HCI snoop log
6. restart your phone
7. open your machine app and play with it collecting inclination and speed
8. Disable the option Enable Bluetooth HCI snoop log
9. in Developer Options: Bug report->Full report
10. wait a random amount of time (10-20 seconds)
11. A notification will appear at the top of the device. Click on it, share, email it to yourself
12. You'll get a zip file with the entire report. In the FS/Data/Log/bt directory of the zipfile is the file you want.
13. attach the log file in a new issue with a short description of the steps you did in the app when you used it
## Android Device (oppo based OS : oppo, ColorOS, RealMe, ...)
1. Dial *#800# on the phone app. A special menu should appear.
2. Go to Bluetooth and press "Start capture" (green button)
3. open your machine app and play with it collecting inclination and speed
4. Go back to the special menu by dialing *#800# on the phone app. Stop the bluetooth capture.
5. A new debug directory is stored onto `/oppo_log/` with a time stamp.
6. Fetch the CFA file stored in the `btsnoop_hci` folder.

View File

@@ -0,0 +1,30 @@
# Applications supported
This list is not exhaustive !
Open a discussion to tell us if a missing application is supported.
## BKool
Everything is working out of the box (read FTMS data).
## Fulgaz
### HRM Support
The application do not read the FTMS value. It is required to start the application with `-heart-service` or `bike_heartrate_service: true` in settings.
The HRM captor will not be shown by default, you need to go in the `Application Settings > Advanced > Disable Bluetooth Filter`.
### Resistance management
Fulgaz is known to be very severe by default in resistance adjustment. It is advised to adjust [application settings](https://tempocyclist.com/2020/04/29/fulgaz-resistance-too-hard/).
The resistance is automatically adjusted with the slope. However, you can override it using
### Additional notes
You can have ae true 4k video stream while you ride ("extreme quality" setting) however it requires about 10 gb per hour.
## Zwift
### HRM support
The application do not read the FTMS value. It is required to start the application with `-heart-service` or `bike_heartrate_service: true` in settings.
### Resistance management
You can adjust resistence using arrows [up and down](img/21_zwift-resistance-buttons.jpg) rom the riding screen.

13
docs/22_devices_detail.md Normal file
View File

@@ -0,0 +1,13 @@
# Devices detail
## Echelon
## Sportstech
### ESX 500
#### HRM
The [cardio belt provided](https://www.sports-tech.uk.com/chest-strap-sportstech-uncoded) with the bike is also supported, the bike reads the value and forwards it in its bluetooth signal.
The cardio captors are reported to not be accurate.
#### Resistance
The resistance is adjusted after a few seconds delay (time for the engine to adapt magnetic resistance).

52
docs/30_usage.md Normal file
View File

@@ -0,0 +1,52 @@
# QDomyos-Zwift operation guide
# Usage
The QDomyos-Zwift can be started in two modes : QML or NativeQT.
The main difference is the configuration management : you can change settings within the application with QML, where you need to specify settings at startup in NativeQT.
**Note:** Android and IOS are always running in QML mode.
On MacOS and Linux, you start QDomyos-Zwift in NativeQT mode (where settings are defined in commandline switches).
You can start the application in QML mode with the command-line switch -qml.
## Configuration in QML mode
Please refer to this article for more information under [QML Operations](https://robertoviola.cloud/qdomyos-zwift-guide/) with several useful information.
## Configuration in NativeQT mode
This is the list of settings available in the application. These settings needs to be appended to the binary command line.
*Example :* `sudo ./qdomyos-zwift -no-gui` for disabling any graphical interface.
| **Option** | **Type** | **Default** | **Function** |
|:------------------------|:---------|:------------|:-----------------------------------------------------------------------------|
| -no-gui | Boolean | False | Disable GUI |
| -qml | Boolean | False | Enables the QML interface |
| -miles | Boolean | False | Swithes to Imperial Units System |
| -no-console | Boolean | False | Not in use |
| -test-resistance | Boolean | False | |
| -no-log | Boolean | False | Disable Logging |
| -no-write-resistance | Boolean | False | Disable resistance instructions from QZ to your fitness equipment |
| -no-heart-service | Boolean | False | Do not simulate external HR monitor, use only FTMS |
| -heart-service | Boolean | True | Simulate HR service (required for applications not reading FTMS) |
| -only-virtualbike | Boolean | False | |
| -only-virtualtreadmill | Boolean | False | |
| -no-reconnection | Boolean | False | QZ will not try to reconnect your fitness equipement if enabled |
| -bluetooth-relaxed | Boolean | False | In case of deconnections from QZ to your fitness equipement |
| -bike-cadence-sensor | Boolean | False | |
| -bike-power-sensor | Boolean | False | |
| -battery-service | Boolean | False | |
| -service-changed | Boolean | False | |
| -bike-wheel-revs | Boolean | False | |
| -run-cadence-sensor | Boolean | False | |
| -train | String | | Force training program |
| -name | String | | Force bluetooth device name (if QZ struggles finding your fitness equipment) |
| -poll-device-time | Int | 200 (ms) | Frequency to refresh informations from QZ to Fitness equipment |
| -bike-resistance-gain | Int | | Adjust resistance from the fitness application |
| -bike-resistance-offset | Int | | Set another resistance point than default |

BIN
docs/img/20_android.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

BIN
docs/img/20_apple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

BIN
docs/img/20_bike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

BIN
docs/img/20_macintosh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

BIN
docs/img/20_pc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

BIN
docs/img/20_treadmill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

BIN
docs/img/20_windows.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

View File

@@ -0,0 +1,28 @@
# libQt5Bluetooth.la - a libtool library file
# Generated by qmake/libtool (3.1) (Qt 5.15.2)
# The name that we can dlopen(3).
dlname=''
# Names of this library.
library_names=' '
# The name of the static archive.
old_library='libQt5Bluetooth.a'
# Libraries that this one depends upon.
dependency_libs='-L=/usr/local/Qt-5.15.2/lib -lQt5Core -framework MobileCoreServices -lm -framework UIKit -framework CoreFoundation -framework Foundation -lz -lqtpcre2'
# Version information for libQt5Bluetooth.la
current=65
age=0
revision=2
# Is this an already installed library.
installed=yes
# Files to dlopen/dlpreopen.
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='=/usr/local/Qt-5.15.2/lib'

View File

@@ -0,0 +1,7 @@
QMAKE_PRL_BUILD_DIR = /Users/cagnulein/Downloads/qt-everywhere-src-5.15.2/qtconnectivity/src/bluetooth
QMAKE_PRO_INPUT = bluetooth.pro
QMAKE_PRL_TARGET = libQt5Bluetooth.a
QMAKE_PRL_CONFIG = lex yacc depend_includepath testcase_targets import_qpa_plugin asset_catalogs rez prepare_docs qt_docs_targets qt_build_extra file_copies qmake_use qt warn_on release link_prl bitcode reduce_exports shallow_bundle no_qt_rpath app_bundle incremental global_init_link_order lib_version_first sdk clang_pch_style cross_compile static ios uikit mac darwin unix posix gcc clang llvm cross_compile compile_examples largefile neon precompile_header prefix_build force_independent force_bootstrap builtin_testdata utf8_source create_prl link_prl no_private_qt_headers_warning QTDIR_build qt_example_installs exceptions_off testcase_exceptions explicitlib release ReleaseBuild Release build_pass device iphoneos simulator iphonesimulator sse sse2 generated_privates relative_qt_rpath app_extension_api_only target_qt c++11 strict_c++ c++14 c++1z c99 c11 hide_symbols qt_install_headers need_fwd_pri qt_install_module debug_and_release build_all create_cmake absolute_library_soname compiler_supports_fpmath create_pc create_libtool release ReleaseBuild Release build_pass have_target staticlib exclusive_builds objective_c c++11 no_autoqmake thread moc resources gc_binaries
QMAKE_PRL_VERSION = 5.15.2
QMAKE_PRL_LIBS = $$[QT_INSTALL_LIBS]/libQt5Core.a -framework MobileCoreServices -lm -framework UIKit -framework CoreFoundation -framework Foundation -lz $$[QT_INSTALL_LIBS]/libqtpcre2.a -framework Foundation -framework CoreBluetooth
QMAKE_PRL_LIBS_FOR_CMAKE = $$[QT_INSTALL_LIBS]/libQt5Core.a;-framework;MobileCoreServices;-lm;-framework;UIKit;-framework;CoreFoundation;-framework;Foundation;-lz;$$[QT_INSTALL_LIBS]/libqtpcre2.a;-framework;Foundation;-framework;CoreBluetooth;;

Binary file not shown.

View File

@@ -0,0 +1,28 @@
# libQt5Bluetooth_debug.la - a libtool library file
# Generated by qmake/libtool (3.1) (Qt 5.15.2)
# The name that we can dlopen(3).
dlname=''
# Names of this library.
library_names=' '
# The name of the static archive.
old_library='libQt5Bluetooth_debug.a'
# Libraries that this one depends upon.
dependency_libs='-L=/usr/local/Qt-5.15.2/lib -lQt5Core_debug -framework MobileCoreServices -lm -framework UIKit -framework CoreFoundation -framework Foundation -lz -lqtpcre2_debug'
# Version information for libQt5Bluetooth_debug.la
current=65
age=0
revision=2
# Is this an already installed library.
installed=yes
# Files to dlopen/dlpreopen.
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='=/usr/local/Qt-5.15.2/lib'

View File

@@ -0,0 +1,7 @@
QMAKE_PRL_BUILD_DIR = /Users/cagnulein/Downloads/qt-everywhere-src-5.15.2/qtconnectivity/src/bluetooth
QMAKE_PRO_INPUT = bluetooth.pro
QMAKE_PRL_TARGET = libQt5Bluetooth_debug.a
QMAKE_PRL_CONFIG = lex yacc debug depend_includepath testcase_targets import_qpa_plugin asset_catalogs rez prepare_docs qt_docs_targets qt_build_extra file_copies qmake_use qt warn_on link_prl bitcode reduce_exports shallow_bundle no_qt_rpath app_bundle incremental global_init_link_order lib_version_first sdk clang_pch_style cross_compile debug static ios uikit mac darwin unix posix gcc clang llvm cross_compile compile_examples largefile neon precompile_header prefix_build force_independent force_bootstrap builtin_testdata utf8_source create_prl link_prl no_private_qt_headers_warning QTDIR_build qt_example_installs exceptions_off testcase_exceptions explicitlib debug DebugBuild Debug build_pass device iphoneos simulator iphonesimulator sse sse2 generated_privates relative_qt_rpath app_extension_api_only target_qt c++11 strict_c++ c++14 c++1z c99 c11 hide_symbols qt_install_headers need_fwd_pri qt_install_module debug_and_release build_all create_cmake absolute_library_soname compiler_supports_fpmath create_libtool debug DebugBuild Debug build_pass have_target staticlib no_plist exclusive_builds debug_info objective_c c++11 no_autoqmake thread moc resources gc_binaries
QMAKE_PRL_VERSION = 5.15.2
QMAKE_PRL_LIBS = $$[QT_INSTALL_LIBS]/libQt5Core_debug.a -framework MobileCoreServices -lm -framework UIKit -framework CoreFoundation -framework Foundation -lz $$[QT_INSTALL_LIBS]/libqtpcre2_debug.a -framework Foundation -framework CoreBluetooth
QMAKE_PRL_LIBS_FOR_CMAKE = $$[QT_INSTALL_LIBS]/libQt5Core_debug.a;-framework;MobileCoreServices;-lm;-framework;UIKit;-framework;CoreFoundation;-framework;Foundation;-lz;$$[QT_INSTALL_LIBS]/libqtpcre2_debug.a;-framework;Foundation;-framework;CoreBluetooth;;

View File

@@ -50,6 +50,8 @@
#include <vector>
#include <limits>
quint8 QZ_EnableDiscoveryCharsAndDescripttors = false;
Q_DECLARE_METATYPE(QLowEnergyHandle)
QT_BEGIN_NAMESPACE
@@ -555,15 +557,23 @@ QT_USE_NAMESPACE
if (!service.characteristics || !service.characteristics.count)
return [self serviceDetailsDiscoveryFinished:service];
/* rviola
NSArray *const cs = service.characteristics;
for (CBCharacteristic *c in cs) {
if (c.properties & CBCharacteristicPropertyRead) {
[self watchAfter:c timeout:OperationTimeout::characteristicRead];
return [peripheral readValueForCharacteristic:c];
//rviola
if(QZ_EnableDiscoveryCharsAndDescripttors == true)
{
qCWarning(QT_BT_OSX) << "QZ_EnableDiscoveryCharsAndDescripttors turned on";
NSArray *const cs = service.characteristics;
for (CBCharacteristic *c in cs) {
if (c.properties & CBCharacteristicPropertyRead) {
[self watchAfter:c timeout:OperationTimeout::characteristicRead];
return [peripheral readValueForCharacteristic:c];
}
}
}
*/
else
{
qCWarning(QT_BT_OSX) << "QZ_EnableDiscoveryCharsAndDescripttors turned off";
}
// No readable properties? Discover descriptors then:
[self discoverDescriptors:service];
}
@@ -602,18 +612,26 @@ QT_USE_NAMESPACE
Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
QT_BT_MAC_AUTORELEASEPOOL;
/* rviola
NSArray *const cs = service.characteristics;
// We can never be here if we have no characteristics.
Q_ASSERT_X(cs && cs.count, Q_FUNC_INFO, "invalid service");
for (CBCharacteristic *c in cs) {
if (c.descriptors && c.descriptors.count) {
CBDescriptor *desc = [c.descriptors objectAtIndex:0];
[self watchAfter:desc timeout:OperationTimeout::descriptorRead];
return [peripheral readValueForDescriptor:desc];
//rviola
if(QZ_EnableDiscoveryCharsAndDescripttors == true)
{
qCWarning(QT_BT_OSX) << "QZ_EnableDiscoveryCharsAndDescripttors turned on";
NSArray *const cs = service.characteristics;
// We can never be here if we have no characteristics.
Q_ASSERT_X(cs && cs.count, Q_FUNC_INFO, "invalid service");
for (CBCharacteristic *c in cs) {
if (c.descriptors && c.descriptors.count) {
CBDescriptor *desc = [c.descriptors objectAtIndex:0];
[self watchAfter:desc timeout:OperationTimeout::descriptorRead];
return [peripheral readValueForDescriptor:desc];
}
}
}
*/
else
{
qCWarning(QT_BT_OSX) << "QZ_EnableDiscoveryCharsAndDescripttors turned off";
}
// No descriptors to read, done.
[self serviceDetailsDiscoveryFinished:service];
}

View File

@@ -0,0 +1,64 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15
import Qt.labs.settings 1.0
ColumnLayout {
property Settings settings: null
id: rootElement
property bool invert: false
property bool linkedBoolSettingDefault: false
property string linkedBoolSetting: "example_setting"
property bool isOpen: invert ? !settings[linkedBoolSetting]:settings[linkedBoolSetting]
property string title: ""
default property alias accordionContent: contentPlaceholder.data
spacing: 0
Layout.fillWidth: true;
/*RowLayout {
id: accordionHeader
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true;
Rectangle{
id:indicatRect
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
width: 8; height: 8
radius: 8
color: "white"
}
Text {
id: accordionText
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
color: "#FFFFFF"
text: rootElement.title
}
}*/
SwitchDelegate {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
id: indicatCbx
text: rootElement.title
checked: rootElement.isOpen
onClicked: {
rootElement.isOpen = checked
settings[rootElement.linkedBoolSetting] = rootElement.invert? !checked:checked
}
}
// This will get filled with the content
ColumnLayout {
id: contentPlaceholder
visible: rootElement.isOpen
Layout.fillWidth: true;
}
}

68
src/AccordionElement.qml Normal file
View File

@@ -0,0 +1,68 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
ColumnLayout {
id: rootElement
property bool isOpen: false
property string title: ""
property alias color: accordionHeader.color
property alias textColor: accordionText.color
property alias textFont: accordionText.font.family
property alias textFontSize: accordionText.font.pixelSize
property alias indicatRectColor: indicatRect.color
default property alias accordionContent: contentPlaceholder.data
spacing: 0
Layout.fillWidth: true;
Rectangle {
id: accordionHeader
color: "red"
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true;
height: 48
Rectangle{
id:indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
}
Text {
id: accordionText
x:34;y:13
color: "#FFFFFF"
text: rootElement.title
}
Image {
y:13
anchors.right: parent.right
anchors.rightMargin: 20
width: 30; height: 30
id: indicatImg
source: "qrc:/icons/arrow-collapse-vertical.png"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
rootElement.isOpen = !rootElement.isOpen
if(rootElement.isOpen)
{
indicatImg.source = "qrc:/icons/arrow-expand-vertical.png"
}else{
indicatImg.source = "qrc:/icons/arrow-collapse-vertical.png"
}
}
}
}
// This will get filled with the content
ColumnLayout {
id: contentPlaceholder
visible: rootElement.isOpen
Layout.fillWidth: true;
}
}

22
src/ChartsEndWorkout.qml Normal file
View File

@@ -0,0 +1,22 @@
import QtQuick 2.4
ChartsEndWorkoutForm {
Component.onCompleted: {
//console.log("ChartsEndWorkoutForm completed " + rootItem.workout_sample_points)
for(var i=0;i<rootItem.workout_sample_points;i+=10)
{
//console.log("ChartsEndWorkoutForm completed " + i + " " + rootItem.workout_watt_points[i])
powerSeries.append(i, rootItem.workout_watt_points[i]);
heartSeries.append(i, rootItem.workout_heart_points[i]);
cadenceSeries.append(i, rootItem.workout_cadence_points[i]);
resistanceSeries.append(i, rootItem.workout_resistance_points[i]);
pelotonResistanceSeries.append(i, rootItem.workout_peloton_resistance_points[i]);
}
rootItem.update_chart_power(powerChart);
//rootItem.update_axes(valueAxisX, valueAxisY);
rootItem.update_chart_heart(heartChart);
//rootItem.update_axes(valueAxisXHR, valueAxisYHR);
//rootItem.update_chart(cadenceChart);
//rootItem.update_axes(valueAxisXCadence, valueAxisYCadence);
}
}

View File

@@ -0,0 +1,274 @@
import QtQuick 2.4
import QtCharts 2.2
import Qt.labs.settings 1.0
import QtQuick.Controls 2.15
Item {
anchors.fill: parent
property alias powerSeries: powerSeries
property alias powerChart: powerChart
property alias heartSeries: heartSeries
property alias heartChart: heartChart
property alias cadenceSeries: cadenceSeries
property alias resistanceSeries: resistanceSeries
property alias pelotonResistanceSeries: pelotonResistanceSeries
property alias cadenceChart: cadenceChart
Settings {
id: settings
property real ftp: 200.0
}
Row {
id: row
anchors.fill: parent
Text {
id: date
text: rootItem.workoutStartDate
font.pixelSize: 16
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
anchors.top: date.bottom
id: title
text: rootItem.workoutName
font.pixelSize: 24
color: "yellow"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.horizontalCenter: parent.horizontalCenter
}
/*
ChartView {
id: caloriesChart
title: "KCal"
anchors.left: parent.left
anchors.top: parent.top
legend.visible: false
antialiasing: true
width: 64
height: 64
margins.bottom: 1
margins.top: 1
margins.left: 1
margins.right: 1
Text {
anchors.topMargin: 10
anchors.fill: caloriesChart
text: rootItem.Pcalories
font.pixelSize: 16
color: "black"
z: 1
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.horizontalCenter: cadenceChart.horizontalCenter
}
PieSeries {
size: 1.0
holeSize: 0.7
PieSlice { value: 1.0; color: "red" }
}
}
ChartView {
id: outputChart
title: "KJoul"
anchors.left: caloriesChart.right
anchors.top: parent.top
legend.visible: false
antialiasing: true
width: 64
height: 64
margins.bottom: 1
margins.top: 1
margins.left: 1
margins.right: 1
Text {
anchors.topMargin: 10
anchors.fill: outputChart
text: rootItem.Pkjoules
font.pixelSize: 16
color: "black"
z: 1
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.horizontalCenter: cadenceChart.horizontalCenter
}
PieSeries {
size: 1.0
holeSize: 0.7
PieSlice { value: 1.0; color: "yellow" }
}
}*/
ScrollView {
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: title.bottom
anchors.bottom: parent.bottom
contentHeight: powerChart.height+heartChart.height+cadenceChart.height
ChartView {
id: powerChart
antialiasing: true
legend.visible: false
height: 400
width: parent.width
title: "Power"
titleFont.pixelSize: 20
ValueAxis {
id: valueAxisX
min: 0
max: rootItem.workout_sample_points
//tickCount: 60
labelFormat: "%.0f"
//labelsVisible: false
gridVisible: false
//lineVisible: false
}
ValueAxis {
id: valueAxisY
min: 0
max: rootItem.wattMaxChart
//tickCount: 60
tickCount: 6
labelFormat: "%.0f"
//labelsVisible: false
//gridVisible: false
//lineVisible: false
}
LineSeries {
//name: "Power"
id: powerSeries
visible: true
axisX: valueAxisX
axisY: valueAxisY
color: "black"
width: 3
}
}
ChartView {
id: heartChart
height: 400
width: parent.width
antialiasing: true
legend.visible: false
anchors.top: powerChart.bottom
title: "Heart Rate"
titleFont.pixelSize: 20
ValueAxis {
id: valueAxisXHR
min: 0
max: rootItem.workout_sample_points
//tickCount: 60
labelFormat: "%.0f"
//labelsVisible: false
gridVisible: false
//lineVisible: false
}
ValueAxis {
id: valueAxisYHR
min: 0
max: 220
tickCount: 5
labelFormat: "%.0f"
//labelsVisible: false
//gridVisible: false
//lineVisible: false
}
LineSeries {
//name: "Power"
id: heartSeries
visible: true
axisX: valueAxisXHR
axisY: valueAxisYHR
color: "black"
width: 3
}
}
ChartView {
id: cadenceChart
height: 400
width: parent.width
antialiasing: true
legend.visible: true
anchors.top: heartChart.bottom
title: "Cadence"
titleFont.pixelSize: 20
ValueAxis {
id: valueAxisXCadence
min: 0
max: rootItem.workout_sample_points
//tickCount: 60
labelFormat: "%.0f"
//labelsVisible: false
gridVisible: false
//lineVisible: false
}
ValueAxis {
id: valueAxisYCadence
min: 0
max: 200
//tickCount: 60
labelFormat: "%.0f"
//labelsVisible: false
gridVisible: false
//lineVisible: false
}
LineSeries {
name: "Cadence"
id: cadenceSeries
visible: true
axisX: valueAxisXCadence
axisY: valueAxisYCadence
color: "black"
width: 3
}
LineSeries {
name: "Resistance"
id: resistanceSeries
visible: true
axisX: valueAxisXCadence
axisY: valueAxisYCadence
color: "blue"
width: 3
}
LineSeries {
name: "Peloton Resistance"
id: pelotonResistanceSeries
visible: true
axisX: valueAxisXCadence
axisY: valueAxisYCadence
color: "red"
width: 3
}
}
}
}
}

View File

@@ -4,19 +4,30 @@ import QtQuick.Controls.Material 2.12
import QtGraphicalEffects 1.12
import QtQuick.Window 2.12
import Qt.labs.settings 1.0
import Qt.labs.platform 1.1
HomeForm{
objectName: "home"
signal start_clicked;
signal stop_clicked;
signal lap_clicked;
signal peloton_start_workout;
signal plus_clicked(string name)
signal minus_clicked(string name)
Settings {
id: settings
property real ui_zoom: 100.0
property bool chart_footer: true
}
MessageDialog {
id: messagePelotonAskStart
text: "Peloton Workout in progress"
informativeText: "Do you want to follow the resistance?"
buttons: (MessageDialog.Yes | MessageDialog.No)
onYesClicked: {rootItem.pelotonAskStart = false; peloton_start_workout();}
onNoClicked: rootItem.pelotonAskStart = false;
visible: rootItem.pelotonAskStart
}
Popup {
@@ -54,56 +65,17 @@ HomeForm{
onTriggered: popupLap.close();
}
Timer {
id: refreshChartTimer
interval: 500
running: true
repeat: true
property int timeline: 0
onTriggered: {
if(settings.chart_footer) chartView.height = 130 * settings.ui_zoom / 100; else chartView.height = 0;
if(rootItem.labelHelp || rootItem.wattMax === 0 || !settings.chart_footer) return;
series1.upperSeries.append(timeline, rootItem.wattZ1);
series2.upperSeries.append(timeline, rootItem.wattZ2);
series3.upperSeries.append(timeline, rootItem.wattZ3);
series4.upperSeries.append(timeline, rootItem.wattZ4);
series5.upperSeries.append(timeline, rootItem.wattZ5);
series6.upperSeries.append(timeline, rootItem.wattZ6);
series7.upperSeries.append(timeline, rootItem.wattZ7);
series1.axisX.min = (timeline - 60 > 0) ? timeline - 60 : 0;
series2.axisX.min = (timeline - 60 > 0) ? timeline - 60 : 0;
series3.axisX.min = (timeline - 60 > 0) ? timeline - 60 : 0;
series4.axisX.min = (timeline - 60 > 0) ? timeline - 60 : 0;
series5.axisX.min = (timeline - 60 > 0) ? timeline - 60 : 0;
series6.axisX.min = (timeline - 60 > 0) ? timeline - 60 : 0;
series7.axisX.min = (timeline - 60 > 0) ? timeline - 60 : 0;
series1.axisX.max = (timeline - 60 > 0) ? timeline : 60;
series2.axisX.max = (timeline - 60 > 0) ? timeline : 60;
series3.axisX.max = (timeline - 60 > 0) ? timeline : 60;
series4.axisX.max = (timeline - 60 > 0) ? timeline : 60;
series5.axisX.max = (timeline - 60 > 0) ? timeline : 60;
series6.axisX.max = (timeline - 60 > 0) ? timeline : 60;
series7.axisX.max = (timeline - 60 > 0) ? timeline : 60;
/*series1.append(1, 5);
series1.append(2, 50);
series1.append(3, 500);*/
//rootItem.dataSource.update(chartView.series(0));
//console.log("refreshChartTimer" + timeline);
timeline++;
}
}
start.onClicked: { start_clicked(); }
stop.onClicked: { stop_clicked(); }
stop.onClicked: { stop_clicked(); stackView.push("ChartsEndWorkout.qml") }
lap.onClicked: { lap_clicked(); popupLap.open(); popupLapAutoClose.running = true; }
Component.onCompleted: { console.log("completed"); }
GridView {
anchors.horizontalCenter: parent.horizontalCenter
anchors.fill: parent
cellWidth: 175 * settings.ui_zoom / 100
cellHeight: 130 * settings.ui_zoom / 100
anchors.fill: parent
focus: true
model: appModel
leftMargin: { (parent.width % cellWidth) / 2 }
@@ -155,7 +127,7 @@ HomeForm{
id: myIcon
x: 5
anchors {
bottom: id1.bottom
bottom: id1.bottom
}
width: 48 * settings.ui_zoom / 100
height: 48 * settings.ui_zoom / 100
@@ -194,7 +166,7 @@ HomeForm{
top: myIcon.top
}
font.bold: true
font.pointSize: labelFontSize
font.pointSize: labelFontSize
color: "white"
text: name
anchors.left: parent.left

View File

@@ -2,7 +2,6 @@ import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtGraphicalEffects 1.12
import QtCharts 2.15
Page {
@@ -14,15 +13,6 @@ Page {
property alias lap: lap
property alias row: row
property alias chartView: chartView
property alias series1: series1
property alias series2: series2
property alias series3: series3
property alias series4: series4
property alias series5: series5
property alias series6: series6
property alias series7: series7
Item {
width: parent.width
height: rootItem.topBarHeight
@@ -171,112 +161,6 @@ Page {
visible: rootItem.labelHelp
}
}
footer:
ChartView {
id: chartView
backgroundColor: Material.backgroundColor
margins.bottom: 0
margins.left: 0
margins.right: 0
margins.top: 0
plotAreaColor: Material.backgroundColor
//title: "Spline"
//anchors.top: gridView.bottom
antialiasing: true
legend.visible: false
width: stackView.width
//Label.visible: false
//visible: settings.chart_footer
ValueAxis {
id: valueAxisX
min: 0
max: 60
tickCount: 60
labelFormat: "%.0f"
labelsVisible: false
gridVisible: false
lineVisible: false
}
ValueAxis {
id: valueAxisY
min: 0
max: rootItem.wattMax
//tickCount: 60
labelFormat: "%.0f"
labelsVisible: false
gridVisible: false
lineVisible: false
titleVisible: false
}
AreaSeries {
id: series1
useOpenGL: true
axisX: valueAxisX
axisY: valueAxisY
name: "Z1"
color: "white"
borderColor: color
upperSeries: LineSeries {}
}
AreaSeries {
id: series2
useOpenGL: true
axisX: valueAxisX
axisY: valueAxisY
color: "limegreen"
borderColor: color
upperSeries: LineSeries {}
}
AreaSeries {
id: series3
useOpenGL: true
axisX: valueAxisX
axisY: valueAxisY
color: "gold"
borderColor: color
upperSeries: LineSeries {}
}
AreaSeries {
id: series4
useOpenGL: true
axisX: valueAxisX
axisY: valueAxisY
color: "orange"
borderColor: color
upperSeries: LineSeries {}
}
AreaSeries {
id: series5
useOpenGL: true
axisX: valueAxisX
axisY: valueAxisY
color: "darkorange"
borderColor: color
upperSeries: LineSeries {}
}
AreaSeries {
id: series6
useOpenGL: true
axisX: valueAxisX
axisY: valueAxisY
color: "orangered"
borderColor: color
upperSeries: LineSeries {}
}
AreaSeries {
id: series7
useOpenGL: true
axisX: valueAxisX
axisY: valueAxisY
color: "red"
borderColor: color
upperSeries: LineSeries {}
}
}
}
/*##^##

View File

@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="2.5.10" android:versionCode="111" android:installLocation="auto">
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="2.5.16" android:versionCode="113" 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 -->

View File

@@ -16,6 +16,7 @@ uint8_t bike::fanSpeed() { return FanSpeed; }
bool bike::connected() { return false; }
uint16_t bike::watts() { return 0; }
metric bike::pelotonResistance() { return m_pelotonResistance; }
int bike::pelotonToBikeResistance(int pelotonResistance) {return pelotonResistance;}
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }

View File

@@ -18,6 +18,7 @@ public:
virtual uint16_t lastCrankEventTime();
virtual bool connected();
virtual uint16_t watts();
virtual int pelotonToBikeResistance(int pelotonResistance);
bluetoothdevice::BLUETOOTH_TYPE deviceType();
metric pelotonResistance();
void clearStats();

View File

@@ -149,13 +149,11 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
heartRateBeltFound = heartRateBeltAvaiable();
}
if(!device.name().length()) return;
bool found = false;
QMutableListIterator<QBluetoothDeviceInfo> i(devices);
while (i.hasNext()) {
QBluetoothDeviceInfo b = i.next();
if(SAME_BLUETOOTH_DEVICE(b,device))
if(SAME_BLUETOOTH_DEVICE(b,device) && b.name().length())
{
found = true;
break;
@@ -166,6 +164,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
emit deviceFound(device.name());
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')' + " " + device.majorDeviceClass() + ":" + device.minorDeviceClass());
#if defined(Q_OS_DARWIN) || defined(Q_OS_IOS)
qDebug() << device.deviceUuid();
#endif
if(onlyDiscover) return;
@@ -244,7 +245,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
if(!discoveryAgent->isActive())
emit searchingStop();
}
else if((b.name().toUpper().startsWith("HORIZON") || b.name().toUpper().startsWith("F80")) && !horizonTreadmill && filter)
else if((b.name().toUpper().startsWith("HORIZON") || b.name().toUpper().startsWith("F80") || b.name().toUpper().startsWith("ESANGLINKER")) && !horizonTreadmill && filter)
{
discoveryAgent->stop();
horizonTreadmill = new horizontreadmill(noWriteResistance, noHeartService);
@@ -263,7 +264,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
emit searchingStop();
}
else if(b.name().startsWith("ECH") && !echelonConnectSport && filter)
{
{
discoveryAgent->stop();
echelonConnectSport = new echelonconnectsport(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
//stateFileRead();
@@ -471,10 +472,33 @@ void bluetooth::connectedAndDiscovered()
if(this->device() != nullptr)
{
#ifdef Q_OS_IOS
QString heartRateBeltName = settings.value("heart_rate_belt_name", "Disabled").toString();
QString b = settings.value("hrm_lastdevice_name", "").toString();
qDebug() << "last hrm name" << b;
if(!b.compare(heartRateBeltName))
{
heartRateBelt = new heartratebelt();
//connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
connect(heartRateBelt, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
connect(heartRateBelt, SIGNAL(heartRate(uint8_t)), this->device(), SLOT(heartRate(uint8_t)));
QBluetoothDeviceInfo bt;
bt.setDeviceUuid(QBluetoothUuid(settings.value("hrm_lastdevice_address", "").toString()));
qDebug() << "UUID" << bt.deviceUuid();
heartRateBelt->deviceDiscovered(bt);
}
#endif
foreach(QBluetoothDeviceInfo b, devices)
{
if(((b.name().startsWith(heartRateBeltName))) && !heartRateBelt && !heartRateBeltName.startsWith("Disabled"))
{
settings.setValue("hrm_lastdevice_name", b.name());
#ifndef Q_OS_IOS
settings.setValue("hrm_lastdevice_address", b.address().toString());
#else
settings.setValue("hrm_lastdevice_address", b.deviceUuid().toString());
#endif
heartRateBelt = new heartratebelt();
//connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));

View File

@@ -9,8 +9,15 @@
#include <math.h>
#include "ios/lockscreen.h"
#ifdef Q_OS_IOS
extern quint8 QZ_EnableDiscoveryCharsAndDescripttors;
#endif
echelonconnectsport::echelonconnectsport(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain)
{
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -136,7 +143,7 @@ void echelonconnectsport::update()
if(requestResistance != -1)
{
if(requestResistance > 32) requestResistance = 32;
if(requestResistance > max_resistance) requestResistance = max_resistance;
else if(requestResistance <= 0) requestResistance = 1;
if(requestResistance != currentResistance().value())
@@ -171,6 +178,25 @@ void echelonconnectsport::serviceDiscovered(const QBluetoothUuid &gatt)
qDebug() << "serviceDiscovered " + gatt.toString();
}
int echelonconnectsport::pelotonToBikeResistance(int pelotonResistance)
{
for(int i = 1; i<max_resistance-1; i++)
{
if(bikeResistanceToPeloton(i) <= pelotonResistance && bikeResistanceToPeloton(i+1) >= pelotonResistance)
return i;
}
return 1;
}
double echelonconnectsport::bikeResistanceToPeloton(double resistance)
{
//0,0097x3 - 0,4972x2 + 10,126x - 37,08
double p = ((pow(resistance,3) * 0.0097) - (0.4972 * pow(resistance, 2)) + (10.126 * resistance) - 37.08);
if(p < 0)
p = 0;
return p;
}
void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
@@ -186,12 +212,7 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
if(newValue.length() == 5 && ((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd2)
{
Resistance = newValue.at(3);
//0,0097x3 - 0,4972x2 + 10,126x - 37,08
double p = ((pow(Resistance.value(),3) * 0.0097) - (0.4972 * pow(Resistance.value(), 2)) + (10.126 * Resistance.value()) - 37.08);
if(p < 0)
p = 0;
m_pelotonResistance = p;
m_pelotonResistance = bikeResistanceToPeloton(Resistance.value());
qDebug() << "Current resistance: " + QString::number(Resistance.value());
return;

View File

@@ -39,12 +39,15 @@ class echelonconnectsport : public bike
Q_OBJECT
public:
echelonconnectsport(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain);
int pelotonToBikeResistance(int pelotonResistance);
bool connected();
void* VirtualBike();
void* VirtualDevice();
private:
const int max_resistance = 32;
double bikeResistanceToPeloton(double resistance);
double GetDistanceFromPacket(QByteArray packet);
QTime GetElapsedFromPacket(QByteArray packet);
void btinit();

View File

@@ -127,7 +127,7 @@ void heartratebelt::deviceDiscovered(const QBluetoothDeviceInfo &device)
QSettings settings;
QString heartRateBeltName = settings.value("heart_rate_belt_name", "Disabled").toString();
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith(heartRateBeltName))
//if(device.name().startsWith(heartRateBeltName))
{
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);

View File

@@ -133,6 +133,9 @@ homeform::homeform(QQmlApplicationEngine* engine, bluetooth* bl)
this, SLOT(refresh_bluetooth_devices_clicked()));
QObject::connect(home, SIGNAL(lap_clicked()),
this, SLOT(Lap()));
QObject::connect(home, SIGNAL(peloton_start_workout()),
this, SLOT(peloton_start_workout()));
if(settings.value("top_bar_enabled", true).toBool())
{
@@ -146,6 +149,9 @@ homeform::homeform(QQmlApplicationEngine* engine, bluetooth* bl)
emit tile_orderChanged(tile_order());
pelotonHandler = new peloton(bl);
connect(pelotonHandler, SIGNAL(workoutStarted(QString)), this, SLOT(pelotonWorkoutStarted(QString)));
//populate the UI
#if 0
#warning("disable me!")
@@ -202,6 +208,34 @@ homeform::homeform(QQmlApplicationEngine* engine, bluetooth* bl)
#endif
}
void homeform::peloton_start_workout()
{
qDebug() << "peloton_start_workout!";
if(pelotonHandler && pelotonHandler->trainrows.length())
{
if(trainProgram)
{
trainProgram->stop();
delete trainProgram;
}
trainProgram = new trainprogram(pelotonHandler->trainrows, bluetoothManager);
trainProgramSignals();
trainProgram->restart();
}
}
void homeform::pelotonWorkoutStarted(QString name)
{
QSettings settings;
if(!settings.value("top_bar_enabled", true).toBool()) return;
m_info = name;
emit infoChanged(m_info);
stravaPelotonActivityName = name;
m_pelotonAskStart = true;
emit(changePelotonAskStart(pelotonAskStart()));
}
void homeform::backup()
{
static uint8_t index = 0;
@@ -822,6 +856,7 @@ void homeform::update()
double resistance = 0;
double watts = 0;
double pace = 0;
double peloton_resistance = 0;
uint8_t cadence = 0;
bool miles = settings.value("miles_unit", false).toBool();
@@ -867,7 +902,8 @@ void homeform::update()
{
cadence = ((bike*)bluetoothManager->device())->currentCadence().value();
resistance = ((bike*)bluetoothManager->device())->currentResistance().value();
this->peloton_resistance->setValue(QString::number(((bike*)bluetoothManager->device())->pelotonResistance().value(), 'f', 0));
peloton_resistance = ((bike*)bluetoothManager->device())->pelotonResistance().value();
this->peloton_resistance->setValue(QString::number(peloton_resistance, 'f', 0));
this->target_resistance->setValue(QString::number(((bike*)bluetoothManager->device())->lastRequestedResistance().value(), 'f', 0));
this->resistance->setValue(QString::number(resistance, 'f', 0));
this->cadence->setValue(QString::number(cadence));
@@ -896,23 +932,13 @@ void homeform::update()
QString ftpMinW = "0";
QString ftpMaxW = "0";
if(ftpSetting > 0)
{
ftpPerc = (watts / ftpSetting) * 100.0;
m_wattZ1Max = ftpSetting * 0.54;
m_wattZ2Max = ftpSetting * 0.75;
m_wattZ3Max = ftpSetting * 0.90;
m_wattZ4Max = ftpSetting * 1.05;
m_wattZ5Max = ftpSetting * 1.20;
m_wattZ6Max = ftpSetting * 1.50;
}
if(ftpPerc < 55)
{
ftpMinW = QString::number(0, 'f', 0);
ftpMaxW = QString::number(ftpSetting * 0.54, 'f', 0);
ftpZone = 1;
ftp->setValueFontColor("white");
m_wattZ2 = m_wattZ3 = m_wattZ4 = m_wattZ5 = m_wattZ6 = m_wattZ7 = 0;
m_wattZ1 = watts;
}
else if(ftpPerc < 76)
{
@@ -920,8 +946,6 @@ void homeform::update()
ftpMaxW = QString::number(ftpSetting * 0.75, 'f', 0);
ftpZone = 2;
ftp->setValueFontColor("limegreen");
m_wattZ3 = m_wattZ4 = m_wattZ5 = m_wattZ6 = m_wattZ7 = 0;
m_wattZ2 = watts;
}
else if(ftpPerc < 91)
{
@@ -929,8 +953,6 @@ void homeform::update()
ftpMaxW = QString::number(ftpSetting * 0.90, 'f', 0);
ftpZone = 3;
ftp->setValueFontColor("gold");
m_wattZ4 = m_wattZ5 = m_wattZ6 = m_wattZ7 = 0;
m_wattZ3 = watts;
}
else if(ftpPerc < 106)
{
@@ -938,8 +960,6 @@ void homeform::update()
ftpMaxW = QString::number(ftpSetting * 1.05, 'f', 0);
ftpZone = 4;
ftp->setValueFontColor("orange");
m_wattZ5 = m_wattZ6 = m_wattZ7 = 0;
m_wattZ4 = watts;
}
else if(ftpPerc < 121)
{
@@ -947,8 +967,6 @@ void homeform::update()
ftpMaxW = QString::number(ftpSetting * 1.20, 'f', 0);
ftpZone = 5;
ftp->setValueFontColor("darkorange");
m_wattZ6 = m_wattZ7 = 0;
m_wattZ5 = watts;
}
else if(ftpPerc < 151)
{
@@ -956,8 +974,6 @@ void homeform::update()
ftpMaxW = QString::number(ftpSetting * 1.50, 'f', 0);
ftpZone = 6;
ftp->setValueFontColor("orangered");
m_wattZ7 = 0;
m_wattZ6 = watts;
}
else
{
@@ -965,13 +981,10 @@ void homeform::update()
ftpMaxW = "";
ftpZone = 7;
ftp->setValueFontColor("red");
m_wattZ7 = watts;
}
ftp->setValue("Z" + QString::number(ftpZone, 'f', 0));
ftp->setSecondLine(ftpMinW + "-" + ftpMaxW + "W " + QString::number(ftpPerc, 'f', 0) + "%");
emit wattMaxChanged(bluetoothManager->device()->wattsMetric().max());
QString Z;
double maxHeartRate = 220.0 - settings.value("age", 35).toDouble();
if(maxHeartRate == 0) maxHeartRate = 190.0;
@@ -1087,6 +1100,7 @@ void homeform::update()
bluetoothManager->device()->odometer(),
watts,
resistance,
peloton_resistance,
(uint8_t)bluetoothManager->device()->currentHeart().value(),
pace, cadence, bluetoothManager->device()->calories(),
bluetoothManager->device()->elevationGain(),
@@ -1101,7 +1115,7 @@ void homeform::update()
}
emit changeOfdevice();
emit changeOflap();
emit changeOflap();
}
bool homeform::getDevice()
@@ -1372,13 +1386,20 @@ bool homeform::strava_upload_file(QByteArray &data, QString remotename)
// use metadata config if the user selected it
QString activityName = " " + settings.value("strava_suffix", "#qdomyos-zwift").toString() ;
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
if(stravaPelotonActivityName.length())
{
activityName = "Run" + activityName;
activityName = stravaPelotonActivityName + activityName;
}
else
{
activityName = "Ride" + activityName;
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
activityName = "Run" + activityName;
}
else
{
activityName = "Ride" + activityName;
}
}
activityNamePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain;charset=utf-8"));
activityNamePart.setBody(activityName.toUtf8());

View File

@@ -5,9 +5,13 @@
#include <QQmlApplicationEngine>
#include <QOAuth2AuthorizationCodeFlow>
#include <QNetworkReply>
#include <QGraphicsScene>
#include <QChart>
#include <QColor>
#include "bluetooth.h"
#include "sessionline.h"
#include "trainprogram.h"
#include "peloton.h"
class DataObject : public QObject
{
@@ -76,6 +80,7 @@ class homeform: public QObject
Q_PROPERTY(bool labelHelp READ labelHelp NOTIFY changeLabelHelp)
Q_PROPERTY( bool device READ getDevice NOTIFY changeOfdevice)
Q_PROPERTY( bool lap READ getLap NOTIFY changeOflap)
Q_PROPERTY( bool pelotonAskStart READ pelotonAskStart NOTIFY changePelotonAskStart WRITE setPelotonAskStart)
Q_PROPERTY(int topBarHeight READ topBarHeight NOTIFY topBarHeightChanged)
Q_PROPERTY(QString info READ info NOTIFY infoChanged)
Q_PROPERTY(QString signal READ signal NOTIFY signalChanged)
@@ -88,39 +93,108 @@ class homeform: public QObject
Q_PROPERTY(QStringList bluetoothDevices READ bluetoothDevices NOTIFY bluetoothDevicesChanged)
Q_PROPERTY(QStringList tile_order READ tile_order NOTIFY tile_orderChanged)
Q_PROPERTY(bool generalPopupVisible READ generalPopupVisible NOTIFY generalPopupVisibleChanged WRITE setGeneralPopupVisible)
Q_PROPERTY(double wattZ1 READ wattZ1)
Q_PROPERTY(double wattZ2 READ wattZ2)
Q_PROPERTY(double wattZ3 READ wattZ3)
Q_PROPERTY(double wattZ4 READ wattZ4)
Q_PROPERTY(double wattZ5 READ wattZ5)
Q_PROPERTY(double wattZ6 READ wattZ6)
Q_PROPERTY(double wattZ7 READ wattZ7)
Q_PROPERTY(double wattZ1Max READ wattZ1Max)
Q_PROPERTY(double wattZ2Max READ wattZ2Max)
Q_PROPERTY(double wattZ3Max READ wattZ3Max)
Q_PROPERTY(double wattZ4Max READ wattZ4Max)
Q_PROPERTY(double wattZ5Max READ wattZ5Max)
Q_PROPERTY(double wattZ6Max READ wattZ6Max)
Q_PROPERTY(double wattMax READ wattMax NOTIFY wattMaxChanged)
Q_PROPERTY(QString workoutStartDate READ workoutStartDate)
Q_PROPERTY(QString workoutName READ workoutName)
Q_PROPERTY(int workout_sample_points READ workout_sample_points)
Q_PROPERTY(QList<double> workout_watt_points READ workout_watt_points)
Q_PROPERTY(QList<double> workout_heart_points READ workout_heart_points)
Q_PROPERTY(QList<double> workout_cadence_points READ workout_cadence_points)
Q_PROPERTY(QList<double> workout_peloton_resistance_points READ workout_peloton_resistance_points)
Q_PROPERTY(QList<double> workout_resistance_points READ workout_resistance_points)
Q_PROPERTY(double wattMaxChart READ wattMaxChart)
public:
Q_INVOKABLE void update_chart_power(QQuickItem *item){
if(QGraphicsScene *scene = item->findChild<QGraphicsScene *>()){
for(QGraphicsItem *it : scene->items()){
if(QtCharts::QChart *chart = dynamic_cast<QtCharts::QChart *>(it)){
// Customize chart background
QLinearGradient backgroundGradient;
double maxWatt = wattMaxChart();
QSettings settings;
double ftpSetting = settings.value("ftp", 200.0).toDouble();
/*backgroundGradient.setStart(QPointF(0, 0));
backgroundGradient.setFinalStop(QPointF(0, 1));
backgroundGradient.setColorAt((maxWatt - (ftpSetting * 0.55)) / maxWatt, QColor("white"));
backgroundGradient.setColorAt((maxWatt - (ftpSetting * 0.75)) / maxWatt, QColor("limegreen"));
backgroundGradient.setColorAt((maxWatt - (ftpSetting * 0.90)) / maxWatt, QColor("gold"));
backgroundGradient.setColorAt((maxWatt - (ftpSetting * 1.05)) / maxWatt, QColor("orange"));
backgroundGradient.setColorAt((maxWatt - (ftpSetting * 1.20)) / maxWatt, QColor("darkorange"));
backgroundGradient.setColorAt((maxWatt - (ftpSetting * 1.5)) / maxWatt, QColor("orangered"));
backgroundGradient.setColorAt(0.0, QColor("red"));*/
//backgroundGradient.setCoordinateMode(QGradient::ObjectBoundingMode);
//chart->setBackgroundBrush(backgroundGradient);
// Customize plot area background
QLinearGradient plotAreaGradient;
plotAreaGradient.setStart(QPointF(0, 0));
plotAreaGradient.setFinalStop(QPointF(0, 1));
plotAreaGradient.setColorAt((maxWatt - (ftpSetting * 0.55)) / maxWatt, QColor("white"));
plotAreaGradient.setColorAt((maxWatt - (ftpSetting * 0.75)) / maxWatt, QColor("limegreen"));
plotAreaGradient.setColorAt((maxWatt - (ftpSetting * 0.90)) / maxWatt, QColor("gold"));
plotAreaGradient.setColorAt((maxWatt - (ftpSetting * 1.05)) / maxWatt, QColor("orange"));
plotAreaGradient.setColorAt((maxWatt - (ftpSetting * 1.20)) / maxWatt, QColor("darkorange"));
plotAreaGradient.setColorAt((maxWatt - (ftpSetting * 1.5)) / maxWatt, QColor("orangered"));
plotAreaGradient.setColorAt(0.0, QColor("red"));
plotAreaGradient.setCoordinateMode(QGradient::ObjectBoundingMode);
chart->setPlotAreaBackgroundBrush(plotAreaGradient);
chart->setPlotAreaBackgroundVisible(true);
}
}
}
}
Q_INVOKABLE void update_chart_heart(QQuickItem *item){
if(QGraphicsScene *scene = item->findChild<QGraphicsScene *>()){
for(QGraphicsItem *it : scene->items()){
if(QtCharts::QChart *chart = dynamic_cast<QtCharts::QChart *>(it)){
// Customize chart background
QLinearGradient backgroundGradient;
QSettings settings;
double maxHeartRate = 220.0 - settings.value("age", 35).toDouble();
if(maxHeartRate == 0) maxHeartRate = 190.0;
/*backgroundGradient.setStart(QPointF(0, 0));
backgroundGradient.setFinalStop(QPointF(0, 1));
backgroundGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone1", 70.0).toDouble() / 100)) / 220, QColor("lightsteelblue"));
backgroundGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone2", 80.0).toDouble() / 100)) / 220, QColor("green"));
backgroundGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone3", 90.0).toDouble() / 100)) / 220, QColor("yellow"));
backgroundGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone4", 100.0).toDouble() / 100)) / 220, QColor("orange"));
backgroundGradient.setColorAt(0.0, QColor("red"));*/
//backgroundGradient.setCoordinateMode(QGradient::ObjectBoundingMode);
//chart->setBackgroundBrush(backgroundGradient);
// Customize plot area background
QLinearGradient plotAreaGradient;
plotAreaGradient.setStart(QPointF(0, 0));
plotAreaGradient.setFinalStop(QPointF(0, 1));
plotAreaGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone1", 70.0).toDouble() / 100)) / 220, QColor("lightsteelblue"));
plotAreaGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone2", 80.0).toDouble() / 100)) / 220, QColor("green"));
plotAreaGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone3", 90.0).toDouble() / 100)) / 220, QColor("yellow"));
plotAreaGradient.setColorAt((220 - (maxHeartRate * settings.value("heart_rate_zone4", 100.0).toDouble() / 100)) / 220, QColor("orange"));
plotAreaGradient.setColorAt(0.0, QColor("red"));
plotAreaGradient.setCoordinateMode(QGradient::ObjectBoundingMode);
chart->setPlotAreaBackgroundBrush(plotAreaGradient);
chart->setPlotAreaBackgroundVisible(true);
}
}
}
}
Q_INVOKABLE void update_axes(QtCharts::QAbstractAxis *axisX, QtCharts::QAbstractAxis *axisY){
if(axisX && axisY){
// Customize axis colors
QPen axisPen(QRgb(0xd18952));
axisPen.setWidth(2);
axisX->setLinePen(axisPen);
axisY->setLinePen(axisPen);
// Customize grid lines and shades
axisY->setShadesPen(Qt::NoPen);
axisY->setShadesBrush(QBrush(QColor(0x99, 0xcc, 0xcc, 0x55)));
}
}
homeform(QQmlApplicationEngine* engine, bluetooth* bl);
~homeform();
int topBarHeight() {return m_topBarHeight;}
double wattZ1() {return m_wattZ1;}
double wattZ2() {return m_wattZ2;}
double wattZ3() {return m_wattZ3;}
double wattZ4() {return m_wattZ4;}
double wattZ5() {return m_wattZ5;}
double wattZ6() {return m_wattZ6;}
double wattZ7() {return m_wattZ7;}
double wattZ1Max() {return m_wattZ1Max;}
double wattZ2Max() {return m_wattZ2Max;}
double wattZ3Max() {return m_wattZ3Max;}
double wattZ4Max() {return m_wattZ4Max;}
double wattZ5Max() {return m_wattZ5Max;}
double wattZ6Max() {return m_wattZ6Max;}
double wattMax() {if(bluetoothManager && bluetoothManager->device()) return bluetoothManager->device()->wattsMetric().max(); else return 0;}
QString info() {return m_info;}
QString signal();
QString startText();
@@ -129,11 +203,24 @@ public:
QString stopText();
QString stopIcon();
QString stopColor();
QString workoutStartDate() {if(Session.length()) return Session.first().time.toString(); else return "";}
QString workoutName() {if(stravaPelotonActivityName.length()) return stravaPelotonActivityName; else {if(bluetoothManager->device() && bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) return "Ride"; else return "Run";}}
bool pelotonAskStart() {return m_pelotonAskStart;}
void setPelotonAskStart(bool value) {m_pelotonAskStart = value;}
bool generalPopupVisible();
bool labelHelp();
QStringList bluetoothDevices();
QStringList tile_order();
void setGeneralPopupVisible(bool value);
int workout_sample_points() { return Session.count();}
double wattMaxChart() {if(bluetoothManager && bluetoothManager->device() && bluetoothManager->device()->wattsMetric().max()) return bluetoothManager->device()->wattsMetric().max(); else { QSettings settings; return settings.value("ftp", 200.0).toDouble() * 2;} }
QList<double> workout_watt_points() { QList<double> l; foreach(SessionLine s, Session) {l.append(s.watt);} return l; }
QList<double> workout_heart_points() { QList<double> l; foreach(SessionLine s, Session) {l.append(s.heart);} return l; }
QList<double> workout_cadence_points() { QList<double> l; foreach(SessionLine s, Session) {l.append(s.cadence);} return l; }
QList<double> workout_resistance_points() { QList<double> l; foreach(SessionLine s, Session) {l.append(s.resistance);} return l; }
QList<double> workout_peloton_resistance_points() { QList<double> l; foreach(SessionLine s, Session) {l.append(s.peloton_resistance);} return l; }
private:
QList<QObject *> dataList;
@@ -150,25 +237,14 @@ private:
QOAuth2AuthorizationCodeFlow* strava;
QNetworkAccessManager* manager = 0;
//charts
double m_wattZ1 = 0;
double m_wattZ2 = 0;
double m_wattZ3 = 0;
double m_wattZ4 = 0;
double m_wattZ5 = 0;
double m_wattZ6 = 0;
double m_wattZ7 = 0;
double m_wattZ1Max = 0;
double m_wattZ2Max = 0;
double m_wattZ3Max = 0;
double m_wattZ4Max = 0;
double m_wattZ5Max = 0;
double m_wattZ6Max = 0;
bool paused = false;
bool stopped = false;
bool lapTrigger = false;
peloton* pelotonHandler = 0;
bool m_pelotonAskStart = false;
QString stravaPelotonActivityName = "";
DataObject* speed;
DataObject* inclination;
DataObject* cadence;
@@ -230,6 +306,8 @@ private slots:
void callbackReceived(const QVariantMap &values);
void writeFileCompleted();
void errorOccurredUploadStrava(QNetworkReply::NetworkError code);
void pelotonWorkoutStarted(QString name);
void peloton_start_workout();
signals:
@@ -247,8 +325,8 @@ signals:
void bluetoothDevicesChanged(QStringList value);
void tile_orderChanged(QStringList value);
void changeLabelHelp(bool value);
void changePelotonAskStart(bool value);
void generalPopupVisibleChanged(bool value);
void wattMaxChanged(double value);
};
#endif // HOMEFORM_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -579,6 +579,7 @@ void m3ibike::processAdvertising(const QByteArray& data) {
return;
debug(" << " + data.toHex(' '));
if (parse_data(data, &k3)) {
QSettings settings;
detectDisc->start(M3i_DISCONNECT_THRESHOLD);
if (!initDone) {
initDone = true;
@@ -587,7 +588,6 @@ void m3ibike::processAdvertising(const QByteArray& data) {
&& !h
#endif
) {
QSettings settings;
bool virtual_device_enabled = settings.value("virtual_device_enabled", true).toBool();
#if defined(Q_OS_IOS) && !defined(IO_UNDER_QT)
h = new lockscreen();
@@ -637,8 +637,11 @@ void m3ibike::processAdvertising(const QByteArray& data) {
Cadence = k3.rpm;
m_watt = k3.watt;
watts(); // to update avg and max
Speed = k3.speed;
KCal = k3.calorie;
Speed = k3.speed;
if(settings.value("m3i_bike_kcal", true).toBool())
KCal = k3.calorie;
else
KCal += ((( (0.048 * ((double)watts()) + 1.19) * settings.value("weight", 75.0).toFloat() * 3.5) / 200.0 ) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))));
Distance = k3.distance;
if (!not_in_pause || k3.time_orig <= 10) {
lastTimerRestart = -1;
@@ -657,6 +660,8 @@ void m3ibike::processAdvertising(const QByteArray& data) {
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
#ifdef Q_OS_ANDROID
if (antHeart)
Heart = (uint8_t)KeepAwakeHelper::heart();
@@ -678,7 +683,6 @@ void m3ibike::processAdvertising(const QByteArray& data) {
}
#if defined(Q_OS_IOS) && !defined(IO_UNDER_QT)
QSettings settings;
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
bool ios_peloton_workaround = settings.value("ios_peloton_workaround", false).toBool();
if (ios_peloton_workaround && cadence && h) {

View File

@@ -167,6 +167,7 @@ private:
keiser_m3i_out_t k3;
qint64 lastTimerRestart = -1;
int lastTimerRestartOffset = 0;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
virtualbike* virtualBike = 0;

View File

@@ -152,6 +152,14 @@ ApplicationWindow {
drawer.close()
}
}
ItemDelegate {
text: qsTr("Charts")
width: parent.width
onClicked: {
stackView.push("ChartsEndWorkout.qml")
drawer.close()
}
}
ItemDelegate {
id: gpx_open
text: qsTr("Open GPX")

View File

@@ -55,6 +55,7 @@ void MainWindow::update()
uint8_t cadence = 0;
double inclination = 0;
double resistance = 0;
double peloton_resistance = 0;
double watts = 0;
double pace = 0;
@@ -151,6 +152,7 @@ void MainWindow::update()
bluetoothManager->device()->odometer(),
watts,
resistance,
peloton_resistance,
(uint8_t)bluetoothManager->device()->currentHeart().value(),
pace, cadence, bluetoothManager->device()->calories(),
bluetoothManager->device()->elevationGain(),

229
src/peloton.cpp Normal file
View File

@@ -0,0 +1,229 @@
#include "peloton.h"
const bool log_request = true;
peloton::peloton(bluetooth* bl, QObject *parent) : QObject(parent)
{
QSettings settings;
bluetoothManager = bl;
mgr = new QNetworkAccessManager(this);
timer = new QTimer(this);
if(!settings.value("peloton_username", "username").toString().compare("username"))
{
qDebug() << "invalid peloton credentials";
return;
}
connect(timer,SIGNAL(timeout()), this, SLOT(startEngine()));
startEngine();
}
void peloton::startEngine()
{
QSettings settings;
timer->stop();
connect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(login_onfinish(QNetworkReply*)));
QUrl url("https://api.onepeloton.com/auth/login");
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::UserAgentHeader, "qdomyos-zwift");
QJsonObject obj;
obj["username_or_email"] = settings.value("peloton_username", "username").toString();
obj["password"] = settings.value("peloton_password", "password").toString();
QJsonDocument doc(obj);
QByteArray data = doc.toJson();
mgr->post(request, data);
}
void peloton::login_onfinish(QNetworkReply* reply)
{
disconnect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(login_onfinish(QNetworkReply*)));
QByteArray payload = reply->readAll(); // JSON
QJsonParseError parseError;
QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
if(log_request)
qDebug() << "login_onfinish" << document;
else
qDebug() << "login_onfinish";
user_id = document["user_id"].toString();
total_workout = document["user_data"]["total_workouts"].toInt();
getWorkoutList(1);
}
void peloton::workoutlist_onfinish(QNetworkReply* reply)
{
disconnect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(workoutlist_onfinish(QNetworkReply*)));
QByteArray payload = reply->readAll(); // JSON
QJsonParseError parseError;
current_workout = QJsonDocument::fromJson(payload, &parseError);
QJsonObject json = current_workout.object();
QJsonArray data = json["data"].toArray();
qDebug() << "data" << data;
QString id = data.at(0)["id"].toString();
QString status = data.at(0)["status"].toString();
current_workout_id = id;
if(status.toUpper().contains("IN_PROGRESS") && !current_workout_status.contains("IN_PROGRESS"))
{
// starting a workout
qDebug() << "workoutlist_onfinish IN PROGRESS!";
// peloton bike only
if(bluetoothManager && bluetoothManager->device() && bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
getSummary(id);
timer->start(60000); // timeout request
}
else
{
timer->start(10000); // check for a status changed
}
}
else
{
//getSummary(current_workout_id); // debug
timer->start(10000); // check for a status changed
}
current_workout_status = status;
if(log_request)
qDebug() << "workoutlist_onfinish" << current_workout;
qDebug() << "current workout id" << current_workout_id;
}
void peloton::summary_onfinish(QNetworkReply* reply)
{
disconnect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(summary_onfinish(QNetworkReply*)));
QByteArray payload = reply->readAll(); // JSON
QJsonParseError parseError;
current_workout_summary = QJsonDocument::fromJson(payload, &parseError);
if(log_request)
qDebug() << "summary_onfinish" << current_workout_summary;
else
qDebug() << "summary_onfinish";
getWorkout(current_workout_id);
}
void peloton::workout_onfinish(QNetworkReply* reply)
{
disconnect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(workout_onfinish(QNetworkReply*)));
QByteArray payload = reply->readAll(); // JSON
QJsonParseError parseError;
workout = QJsonDocument::fromJson(payload, &parseError);
QJsonObject ride = workout.object()["ride"].toObject();
current_workout_name = ride["title"].toString();
if(log_request)
qDebug() << "workout_onfinish" << workout;
else
qDebug() << "workout_onfinish";
getPerformance(current_workout_id);
}
void peloton::performance_onfinish(QNetworkReply* reply)
{
disconnect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(performance_onfinish(QNetworkReply*)));
QByteArray payload = reply->readAll(); // JSON
QJsonParseError parseError;
performance = QJsonDocument::fromJson(payload, &parseError);
QJsonObject json = performance.object();
QJsonObject target_performance_metrics = json["target_performance_metrics"].toObject();
QJsonArray target_graph_metrics = target_performance_metrics["target_graph_metrics"].toArray();
QJsonObject resistances = target_graph_metrics[1].toObject();
QJsonObject graph_data = resistances["graph_data"].toObject();
QJsonArray averages = graph_data["average"].toArray();
trainrows.clear();
foreach(QJsonValue a, averages)
{
trainrow r;
r.duration = QTime(0,0,peloton_workout_second_resolution,0);
r.resistance = ((bike*)bluetoothManager->device())->pelotonToBikeResistance(a.toInt());
trainrows.append(r);
}
if(log_request)
qDebug() << "performance_onfinish" << workout;
else
qDebug() << "performance_onfinish" << trainrows.length();
if(trainrows.length())
{
emit workoutStarted(current_workout_name);
}
timer->start(30000); // check for a status changed
}
void peloton::getPerformance(QString workout)
{
connect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(performance_onfinish(QNetworkReply*)));
QUrl url("https://api.onepeloton.com/api/workout/" + workout + "/performance_graph?every_n=" + QString::number(peloton_workout_second_resolution));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::UserAgentHeader, "qdomyos-zwift");
mgr->get(request);
}
void peloton::getWorkout(QString workout)
{
connect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(workout_onfinish(QNetworkReply*)));
QUrl url("https://api.onepeloton.com/api/workout/" + workout);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::UserAgentHeader, "qdomyos-zwift");
mgr->get(request);
}
void peloton::getSummary(QString workout)
{
connect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(summary_onfinish(QNetworkReply*)));
QUrl url("https://api.onepeloton.com/api/workout/" + workout + "/summary");
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::UserAgentHeader, "qdomyos-zwift");
mgr->get(request);
}
void peloton::getWorkoutList(int num)
{
if(num == 0)
num = this->total_workout;
int limit = 100;
int pages = num / limit;
int rem = num % limit;
connect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(workoutlist_onfinish(QNetworkReply*)));
int current_page = 0;
QUrl url("https://api.onepeloton.com/api/user/" + user_id + "/workouts?sort_by=-created&page=" + QString::number(current_page) + "&limit=" + QString::number(limit));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::UserAgentHeader, "qdomyos-zwift");
mgr->get(request);
}

64
src/peloton.h Normal file
View File

@@ -0,0 +1,64 @@
#ifndef PELOTON_H
#define PELOTON_H
#include <QObject>
#include <QAbstractOAuth2>
#include <QNetworkAccessManager>
#include <QOAuth2AuthorizationCodeFlow>
#include <QOAuthHttpServerReplyHandler>
#include <QDesktopServices>
#include <QJsonDocument>
#include <QUrlQuery>
#include <QHttpMultiPart>
#include <QSettings>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonArray>
#include <QTimer>
#include "trainprogram.h"
#include "bluetooth.h"
class peloton : public QObject
{
Q_OBJECT
public:
explicit peloton(bluetooth* bl, QObject *parent = nullptr);
QList<trainrow> trainrows;
private:
const int peloton_workout_second_resolution = 10;
QNetworkAccessManager * mgr = 0;
QString user_id;
QString current_workout_id = "";
QString current_workout_name = "";
QString current_workout_status = "";
QJsonDocument current_workout;
QJsonDocument current_workout_summary;
QJsonDocument workout;
QJsonDocument performance;
QTimer* timer;
bluetooth* bluetoothManager = 0;
int total_workout;
void getWorkoutList(int num);
void getSummary(QString workout);
void getWorkout(QString workout);
void getPerformance(QString workout);
private slots:
void login_onfinish(QNetworkReply* reply);
void workoutlist_onfinish(QNetworkReply* reply);
void summary_onfinish(QNetworkReply* reply);
void workout_onfinish(QNetworkReply* reply);
void performance_onfinish(QNetworkReply* reply);
void startEngine();
signals:
void workoutStarted(QString name);
};
#endif // PELOTON_H

View File

@@ -1,6 +1,6 @@
QT += bluetooth widgets xml positioning quick networkauth
QT+= charts
!ios: QT+= charts
unix:android: QT += androidextras gui-private
CONFIG += c++11 console debug app_bundle
@@ -63,6 +63,7 @@ SOURCES += \
keepawakehelper.cpp \
main.cpp \
metric.cpp \
peloton.cpp \
proformbike.cpp \
proformtreadmill.cpp \
qfit.cpp \
@@ -334,6 +335,7 @@ HEADERS += \
ios/M3iIOS-Interface.h \
material.h \
metric.h \
peloton.h \
proformbike.h \
proformtreadmill.h \
qfit.h \
@@ -420,4 +422,4 @@ ios {
QMAKE_TARGET_BUNDLE_PREFIX = org.cagnulein
}
VERSION = 2.5.12
VERSION = 2.5.16

View File

@@ -93,7 +93,7 @@ void qfit::save(QString filename, QList<SessionLine> session, bluetoothdevice::B
lapMesg.SetStartTime(session.first().time.toSecsSinceEpoch() - 631065600L);
lapMesg.SetTimestamp(session.first().time.toSecsSinceEpoch() - 631065600L);
lapMesg.SetEvent(FIT_EVENT_WORKOUT);
lapMesg.SetEventType(FIT_EVENT_TYPE_START);
lapMesg.SetEventType(FIT_EVENT_TYPE_STOP);
lapMesg.SetLapTrigger(FIT_LAP_TRIGGER_TIME);
lapMesg.SetTotalElapsedTime(0);
lapMesg.SetTotalTimerTime(0);

View File

@@ -7,5 +7,11 @@
<file>qtquickcontrols2.conf</file>
<file>Home.qml</file>
<file>settings.qml</file>
<file>AccordionElement.qml</file>
<file>AccordionCheckElement.qml</file>
<file>icons/arrow-collapse-vertical.png</file>
<file>icons/arrow-expand-vertical.png</file>
<file>ChartsEndWorkout.qml</file>
<file>ChartsEndWorkoutForm.ui.qml</file>
</qresource>
</RCC>

View File

@@ -1,12 +1,13 @@
#include "sessionline.h"
SessionLine::SessionLine(double speed, int8_t inclination, double distance, uint16_t watt, int8_t resistance, uint8_t heart, double pace, uint8_t cadence, double calories, double elevationGain, uint32_t elapsed, bool lap, QDateTime time)
SessionLine::SessionLine(double speed, int8_t inclination, double distance, uint16_t watt, int8_t resistance, int8_t peloton_resistance, uint8_t heart, double pace, uint8_t cadence, double calories, double elevationGain, uint32_t elapsed, bool lap, QDateTime time)
{
this->speed = speed;
this->inclination = inclination;
this->distance = distance;
this->watt = watt;
this->resistance = resistance;
this->peloton_resistance = peloton_resistance;
this->heart = heart;
this->pace = pace;
this->time = time;

View File

@@ -11,6 +11,7 @@ public:
double distance;
uint16_t watt;
int8_t resistance;
int8_t peloton_resistance;
uint8_t heart;
double pace;
uint8_t cadence;
@@ -21,7 +22,7 @@ public:
bool lapTrigger = false;
SessionLine();
SessionLine(double speed, int8_t inclination, double distance, uint16_t watt, int8_t resistance, uint8_t heart, double pace, uint8_t cadence, double calories, double elevationGain, uint32_t elapsed, bool lap, QDateTime time = QDateTime::currentDateTime());
SessionLine(double speed, int8_t inclination, double distance, uint16_t watt, int8_t resistance, int8_t peloton_resistance, uint8_t heart, double pace, uint8_t cadence, double calories, double elevationGain, uint32_t elapsed, bool lap, QDateTime time = QDateTime::currentDateTime());
};
#endif // SESSIONLINE_H

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,12 @@
class trainrow
{
public:
QTime duration;
double speed;
QTime duration = QTime(0,0,0,0);
double speed = -1;
double fanspeed = -1;
double inclination;
int8_t resistance;
bool forcespeed;
double inclination = -1;
int8_t resistance = -1;
bool forcespeed = false;
};
class trainprogram: public QObject