Compare commits

...

79 Commits

Author SHA1 Message Date
Roberto Viola
51106cb293 heart rate always read on cscbike 2021-04-08 17:09:56 +02:00
Roberto Viola
ed0b1be6c4 csc fixes 2021-04-08 16:34:09 +02:00
Roberto Viola
8a7f3193aa first commit of cscbike 2021-04-08 16:07:07 +02:00
Roberto Viola
054fe59ddb added icon for peloton credentials if they are ok or not 2021-04-07 15:24:36 +02:00
Roberto Viola
e94bdde451 connection timeout on inspirebike 2021-04-07 14:17:01 +02:00
Roberto Viola
78dfb415bb clearing the heartRate lastname if it set to disabled 2021-04-07 12:12:28 +02:00
Roberto Viola
1c81291d5d added the ability to save settings to the debug file #240 2021-04-07 11:22:13 +02:00
Roberto Viola
c56b9ea3d3 fixed screen rotation issue #105 2021-04-07 11:08:24 +02:00
Roberto Viola
617ee0a32c fixed crash when user changes the settings while connected to the bike 2021-04-07 09:50:49 +02:00
Roberto Viola
9b1f5dfd6d fixed inclination on random training program 2021-04-07 07:42:50 +02:00
Roberto Viola
97b446ec7f distance on echelon on virtualbike 2021-04-06 08:20:24 +02:00
Roberto Viola
4b230e856c Update issue templates 2021-04-06 08:09:27 +02:00
Roberto Viola
97e6f8449d fixed a bug that occurs when you have a peloton class running and you
start QZ after
2021-04-05 11:52:04 +02:00
Roberto Viola
5b4ff32501 fixed missing training program line on peloton #233 2021-04-05 11:28:31 +02:00
Roberto Viola
7441c50dfa added setting for peloton difficulty #242 2021-04-05 10:48:03 +02:00
Roberto Viola
03a0690e39 requested peloton resistance in the trainprogram #233 2021-04-05 10:31:26 +02:00
Roberto Viola
4fd51423c0 add primary service for virtual echelon on virtualbike #198 2021-04-05 10:12:45 +02:00
Roberto Viola
277febac16 iOS build fixed 2021-04-04 19:59:51 +02:00
Roberto Viola
f0956f47b4 added an erg filter for zwift #230 2021-04-04 19:35:03 +02:00
Roberto Viola
5df686c1a5 fixed advertisment packet on echelon sport virtualbike 2021-04-04 19:18:17 +02:00
Roberto Viola
7f13505f54 apple watch on domyosbike #249 2021-04-04 19:07:05 +02:00
Roberto Viola
a897514fbb small fix on chart with peloton instructor 2021-04-03 15:46:37 +02:00
Roberto Viola
4b36a51861 version 2.5.20 2021-04-03 15:21:21 +02:00
Roberto Viola
07a7966879 reduced charts width to 1 2021-04-03 15:18:36 +02:00
Roberto Viola
14190a771e added target peloton resistance to echelonconnectsport #233 2021-04-03 15:13:27 +02:00
Roberto Viola
d9d92ff955 JLL IC400 bike managed #243 2021-04-03 14:57:50 +02:00
Roberto Viola
eb3d5983be pause handled on trainprogram #235 2021-04-03 14:21:40 +02:00
Roberto Viola
87a4c9511c peloton instructorName #237 2021-04-03 14:15:52 +02:00
Roberto Viola
a393e8767a limit peloton workout to 1 #236 2021-04-03 13:32:41 +02:00
Roberto Viola
2417b85f64 fixing domyosbike splitted packages #234 2021-04-03 13:30:02 +02:00
Roberto Viola
a18e2b771b fix fonts on chart on ios 2021-04-02 15:50:52 +02:00
Roberto Viola
ba268cf97c ios build fixed 2021-04-02 15:29:39 +02:00
Roberto Viola
e0a695be01 settings fixed and moved the tiles groups on the root of the settings 2021-04-02 14:51:39 +02:00
Roberto Viola
b1ceae9136 erg mode fixed 2021-04-02 14:22:24 +02:00
Roberto Viola
ff8a3dc340 JLL_IC400_bike managed 2021-04-02 14:06:44 +02:00
Roberto Viola
686c3eb6f0 autoresistance button added 2021-04-02 13:57:05 +02:00
Roberto Viola
d3cebaa79e S77 treadmill compatibility added 2021-04-02 13:22:12 +02:00
Roberto Viola
a6585e8b58 zwift erg mode for domyosbike 2021-04-02 09:46:18 +02:00
Roberto Viola
ff5143894c fix on watt function on echelonconnectsport 2021-04-02 09:08:02 +02:00
Roberto Viola
b717818616 zwift erg mode managed for echelonconnectsport 2021-04-02 09:01:47 +02:00
Roberto Viola
3a099f89f4 added target power tile and target cadence tile; managed cadence from
peloton
2021-04-02 08:35:38 +02:00
Roberto Viola
aafec8f292 screenshot has been called by QML now in order to be synced with the
chart UI
2021-04-02 08:04:56 +02:00
Roberto Viola
ba611c908e min heart rate on chart set to 60 2021-04-02 07:54:49 +02:00
Roberto Viola
05b34fd935 dateaxis on charts 2021-04-02 07:52:38 +02:00
Roberto Viola
961c070011 toolbar must be always available when entering in the chart form 2021-04-02 07:29:54 +02:00
Roberto Viola
0d4f038a26 iOS fixing Charts build issues 2021-04-02 07:18:42 +02:00
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
71 changed files with 5122 additions and 2360 deletions

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[REQ]"
labels: enhancement
assignees: cagnulein
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

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

@@ -120,6 +120,8 @@
87062647259480B400D06586 /* WatchKitConnection.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876E4E53259479EE00BD5714 /* WatchKitConnection.swift */; };
87062648259480B700D06586 /* WorkoutTracking.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876E4E50259479EE00BD5714 /* WorkoutTracking.swift */; };
871E4CD125A6FB5A00E18D6D /* BLEPeripheralManager.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 871E4CD025A6FB5A00E18D6D /* BLEPeripheralManager.swift */; };
872BAB4E261750EE006A59AB /* libQt5Charts.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 872BAB4D261750EE006A59AB /* libQt5Charts.a */; };
872BAB50261751FB006A59AB /* libqtchartsqml2.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 872BAB4F261751FB006A59AB /* libqtchartsqml2.a */; };
873063BE259DF20000DA0F44 /* heartratebelt.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873063BC259DF20000DA0F44 /* heartratebelt.cpp */; };
873063C0259DF2C500DA0F44 /* moc_heartratebelt.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873063BF259DF2C500DA0F44 /* moc_heartratebelt.cpp */; };
87368825259C602800C71C7E /* watchAppStart.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87368824259C602800C71C7E /* watchAppStart.swift */; };
@@ -149,6 +151,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 */; };
@@ -507,6 +512,8 @@
86DD72842A64993F31E31719 /* fit_ant_rx_mesg_listener.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_ant_rx_mesg_listener.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_ant_rx_mesg_listener.hpp"; sourceTree = "<absolute>"; };
86F10E1AE2D47520E65C0543 /* fit_dive_summary_mesg_listener.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_dive_summary_mesg_listener.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_dive_summary_mesg_listener.hpp"; sourceTree = "<absolute>"; };
871E4CD025A6FB5A00E18D6D /* BLEPeripheralManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BLEPeripheralManager.swift; path = ../src/ios/BLEPeripheralManager.swift; sourceTree = "<group>"; };
872BAB4D261750EE006A59AB /* libQt5Charts.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5Charts.a; path = ../../Qt/5.15.2/ios/lib/libQt5Charts.a; sourceTree = "<group>"; };
872BAB4F261751FB006A59AB /* libqtchartsqml2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtchartsqml2.a; path = ../../Qt/5.15.2/ios/qml/QtCharts/libqtchartsqml2.a; sourceTree = "<group>"; };
873063BC259DF20000DA0F44 /* heartratebelt.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = heartratebelt.cpp; path = ../src/heartratebelt.cpp; sourceTree = "<group>"; };
873063BD259DF20000DA0F44 /* heartratebelt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = heartratebelt.h; path = ../src/heartratebelt.h; sourceTree = "<group>"; };
873063BF259DF2C500DA0F44 /* moc_heartratebelt.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_heartratebelt.cpp; sourceTree = "<group>"; };
@@ -556,6 +563,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 +828,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
872BAB50261751FB006A59AB /* libqtchartsqml2.a in Link Binary With Libraries */,
872BAB4E261750EE006A59AB /* libQt5Charts.a in Link Binary With Libraries */,
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 +943,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 +991,8 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
8781907C2615089D0085E656 /* peloton.cpp */,
8781907D2615089D0085E656 /* peloton.h */,
8762D5112601F89500F6F049 /* scanrecordresult.cpp */,
8762D5122601F89500F6F049 /* scanrecordresult.h */,
8762D50E2601F7EA00F6F049 /* M3iIOS-Interface.h */,
@@ -1145,6 +1163,10 @@
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
isa = PBXGroup;
children = (
872BAB4F261751FB006A59AB /* libqtchartsqml2.a */,
872BAB4D261750EE006A59AB /* libQt5Charts.a */,
8781908326150C5B0085E656 /* libqtlabsplatformplugin_debug.a */,
8781908126150B490085E656 /* libqtlabsplatformplugin.a */,
2D4A13931169E5681CE465F0 /* Qt5NetworkAuth */,
BC3A8C3E433A8FA00BB15F07 /* qios */,
7EC00404ACD5AB0E97726B0E /* QuartzCore.framework */,
@@ -1749,6 +1771,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 +1790,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 +2188,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.21;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
HEADER_SEARCH_PATHS = (
@@ -2181,12 +2205,45 @@
../../Qt/5.15.2/ios/include/QtPositioning,
../../Qt/5.15.2/ios/include/QtQmlModels,
../../Qt/5.15.2/ios/include/QtQml,
../../Qt/5.15.2/ios/include/QtCharts,
../../Qt/5.15.2/ios/include/QtNetwork,
../../Qt/5.15.2/ios/include/QtCore,
.,
"../../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,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtCharts,
);
MARKETING_VERSION = 2.5;
PRODUCT_BUNDLE_IDENTIFIER = "org.cagnulein.${PRODUCT_NAME:rfc1034identifier}";
PRODUCT_NAME = qdomyoszwift;
@@ -2216,7 +2273,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.21;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -2235,12 +2292,45 @@
../../Qt/5.15.2/ios/include/QtPositioning,
../../Qt/5.15.2/ios/include/QtQmlModels,
../../Qt/5.15.2/ios/include/QtQml,
../../Qt/5.15.2/ios/include/QtCharts,
../../Qt/5.15.2/ios/include/QtNetwork,
../../Qt/5.15.2/ios/include/QtCore,
.,
"../../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,
/Users/cagnulein/Qt/5.15.2/ios/qml/QtCharts,
);
MARKETING_VERSION = 2.5;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "org.cagnulein.${PRODUCT_NAME:rfc1034identifier}";
@@ -2304,7 +2394,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.21;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -2394,7 +2484,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.21;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_NS_ASSERTIONS = NO;
@@ -2478,7 +2568,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.21;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_PREVIEWS = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -2565,7 +2655,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.21;
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;
}
}

24
src/ChartsEndWorkout.qml Normal file
View File

@@ -0,0 +1,24 @@
import QtQuick 2.4
ChartsEndWorkoutForm {
Component.onCompleted: {
headerToolbar.visible = true;
//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 * 1000, rootItem.workout_watt_points[i]);
heartSeries.append(i * 1000, rootItem.workout_heart_points[i]);
cadenceSeries.append(i * 1000, rootItem.workout_cadence_points[i]);
resistanceSeries.append(i * 1000, rootItem.workout_resistance_points[i]);
pelotonResistanceSeries.append(i * 1000, 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,290 @@
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
}
Text {
anchors.top: title.bottom
id: instructor
text: rootItem.instructorName
font.pixelSize: 18
color: "white"
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: instructor.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
DateTimeAxis {
id: valueAxisX
tickCount: 7
min: new Date(0)
max: new Date(rootItem.workout_sample_points * 1000)
format: "mm:ss"
//labelsVisible: false
gridVisible: false
//lineVisible: false
labelsFont.pixelSize: 10
}
ValueAxis {
id: valueAxisY
min: 0
max: rootItem.wattMaxChart
//tickCount: 60
tickCount: 6
labelFormat: "%.0f"
//labelsVisible: false
//gridVisible: false
//lineVisible: false
labelsFont.pixelSize: 10
}
LineSeries {
//name: "Power"
id: powerSeries
visible: true
axisX: valueAxisX
axisY: valueAxisY
color: "black"
width: 1
}
}
ChartView {
id: heartChart
height: 400
width: parent.width
antialiasing: true
legend.visible: false
anchors.top: powerChart.bottom
title: "Heart Rate"
titleFont.pixelSize: 20
DateTimeAxis {
id: valueAxisXHR
tickCount: 7
min: new Date(0)
max: new Date(rootItem.workout_sample_points * 1000)
format: "mm:ss"
//labelsVisible: false
gridVisible: false
//lineVisible: false
labelsFont.pixelSize: 10
}
ValueAxis {
id: valueAxisYHR
min: 60
max: 220
tickCount: 5
labelFormat: "%.0f"
//labelsVisible: false
//gridVisible: false
//lineVisible: false
labelsFont.pixelSize: 10
}
LineSeries {
//name: "Power"
id: heartSeries
visible: true
axisX: valueAxisXHR
axisY: valueAxisYHR
color: "black"
width: 1
}
}
ChartView {
id: cadenceChart
height: 400
width: parent.width
antialiasing: true
legend.visible: true
anchors.top: heartChart.bottom
title: "Cadence"
titleFont.pixelSize: 20
DateTimeAxis {
id: valueAxisXCadence
min: new Date(0)
max: new Date(rootItem.workout_sample_points * 1000)
format: "mm:ss"
tickCount: 7
//labelsVisible: false
gridVisible: false
//lineVisible: false
labelsFont.pixelSize: 10
}
ValueAxis {
id: valueAxisYCadence
min: 0
max: 200
//tickCount: 60
labelFormat: "%.0f"
//labelsVisible: false
gridVisible: false
//lineVisible: false
labelsFont.pixelSize: 10
}
LineSeries {
name: "Cadence"
id: cadenceSeries
visible: true
axisX: valueAxisXCadence
axisY: valueAxisYCadence
color: "black"
width: 1
}
LineSeries {
name: "Resistance"
id: resistanceSeries
visible: true
axisX: valueAxisXCadence
axisY: valueAxisYCadence
color: "blue"
width: 1
}
LineSeries {
name: "Peloton Resistance"
id: pelotonResistanceSeries
visible: true
axisX: valueAxisXCadence
axisY: valueAxisYCadence
color: "red"
width: 1
}
}
}
}
}

View File

@@ -4,12 +4,14 @@ 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)
@@ -18,6 +20,16 @@ HomeForm{
property real ui_zoom: 100.0
}
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 {
id: popupLap
parent: Overlay.overlay
@@ -54,7 +66,7 @@ HomeForm{
}
start.onClicked: { start_clicked(); }
stop.onClicked: { stop_clicked(); }
stop.onClicked: { stop_clicked(); rootItem.save_screenshot(); stackView.push("ChartsEndWorkout.qml") }
lap.onClicked: { lap_clicked(); popupLap.open(); popupLapAutoClose.running = true; }
Component.onCompleted: { console.log("completed"); }
@@ -66,15 +78,14 @@ HomeForm{
cellHeight: 130 * settings.ui_zoom / 100
focus: true
model: appModel
leftMargin: { (parent.width % cellWidth) / 2 }
leftMargin: { (Screen.width % cellWidth) / 2 }
anchors.topMargin: rootItem.topBarHeight + 30
id: gridView
objectName: "gridview"
onMovementEnded: { headerToolbar.visible = (contentY == 0); }
onWidthChanged: gridView.leftMargin = (parent.width % cellWidth) / 2;
Screen.orientationUpdateMask: Qt.LandscapeOrientation | Qt.PortraitOrientation
Screen.onPrimaryOrientationChanged:{
gridView.leftMargin = (parent.width % cellWidth) / 2;
gridView.leftMargin = (Screen.width % cellWidth) / 2;
}
// highlight: Rectangle {

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.22" android:versionCode="117" 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

@@ -6,16 +6,24 @@ bike::bike()
}
void bike::changeResistance(int8_t resistance) { requestResistance = resistance * m_difficult; RequestedResistance = resistance * m_difficult; }
void bike::changeResistance(int8_t resistance) { if(autoResistanceEnable) {requestResistance = resistance * m_difficult;} RequestedResistance = resistance * m_difficult; }
void bike::changeRequestedPelotonResistance(int8_t resistance) { RequestedPelotonResistance = resistance; }
void bike::changeCadence(int16_t cadence) { RequestedCadence = cadence; }
void bike::changePower(int32_t power) { RequestedPower = power; }
double bike::currentCrankRevolutions() { return CrankRevs;}
uint16_t bike::lastCrankEventTime() { return LastCrankEventTime;}
metric bike::lastRequestedResistance() { return RequestedResistance; }
metric bike::lastRequestedPelotonResistance() { return RequestedPelotonResistance; }
metric bike::lastRequestedCadence() { return RequestedCadence; }
metric bike::lastRequestedPower() { return RequestedPower; }
metric bike::currentResistance() { return Resistance;}
metric bike::currentCadence() { return Cadence;}
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;}
uint8_t bike::resistanceFromPowerRequest(uint16_t power) {return power / 10;} // in order to have something
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }
@@ -30,7 +38,10 @@ void bike::clearStats()
elevationAcc = 0;
m_watt.clear(false);
RequestedPelotonResistance.clear(false);
RequestedResistance.clear(false);
RequestedCadence.clear(false);
RequestedPower.clear(false);
m_pelotonResistance.clear(false);
Cadence.clear(false);
Resistance.clear(false);
@@ -49,7 +60,10 @@ void bike::setPaused(bool p)
m_pelotonResistance.setPaused(p);
Cadence.setPaused(p);
Resistance.setPaused(p);
RequestedPelotonResistance.setPaused(p);
RequestedResistance.setPaused(p);
RequestedCadence.setPaused(p);
RequestedPower.setPaused(p);
}
void bike::setLap()
@@ -62,7 +76,10 @@ void bike::setLap()
m_jouls.setLap(true);
m_watt.setLap(false);
RequestedPelotonResistance.setLap(false);
RequestedResistance.setLap(false);
RequestedCadence.setLap(false);
RequestedPower.setLap(false);
m_pelotonResistance.setLap(false);
Cadence.setLap(false);
Resistance.setLap(false);

View File

@@ -11,6 +11,9 @@ class bike:public bluetoothdevice
public:
bike();
metric lastRequestedResistance();
metric lastRequestedPelotonResistance();
metric lastRequestedCadence();
metric lastRequestedPower();
virtual metric currentResistance();
virtual metric currentCadence();
virtual uint8_t fanSpeed();
@@ -18,6 +21,8 @@ public:
virtual uint16_t lastCrankEventTime();
virtual bool connected();
virtual uint16_t watts();
virtual int pelotonToBikeResistance(int pelotonResistance);
virtual uint8_t resistanceFromPowerRequest(uint16_t power);
bluetoothdevice::BLUETOOTH_TYPE deviceType();
metric pelotonResistance();
void clearStats();
@@ -25,7 +30,10 @@ public:
void setPaused(bool p);
public slots:
virtual void changeResistance(int8_t res);
virtual void changeResistance(int8_t res);
virtual void changeCadence(int16_t cad);
virtual void changePower(int32_t power);
virtual void changeRequestedPelotonResistance(int8_t resistance);
signals:
void bikeStarted();
@@ -34,6 +42,9 @@ protected:
metric Cadence;
metric Resistance;
metric RequestedResistance;
metric RequestedPelotonResistance;
metric RequestedCadence;
metric RequestedPower;
uint16_t LastCrankEventTime = 0;
int8_t requestResistance = -1;
double CrankRevs = 0;

View File

@@ -143,6 +143,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
bool heartRateBeltFound = heartRateBeltName.startsWith("Disabled");
bool toorx_bike = settings.value("toorx_bike", false).toBool();
bool snode_bike = settings.value("snode_bike", false).toBool();
bool JLL_IC400_bike = settings.value("jll_IC400_bike", false).toBool();
bool csc_as_bike = settings.value("cadence_sensor_as_bike", false).toBool();
QString cscName = settings.value("cadence_sensor_name", "Disabled").toString();
if(!heartRateBeltFound)
{
@@ -153,7 +156,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
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;
@@ -164,6 +167,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;
@@ -191,6 +197,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
emit searchingStop();
}
}
else if(csc_as_bike && b.name().startsWith(cscName) && !cscBike && filter)
{
discoveryAgent->stop();
cscBike = new cscbike(noWriteResistance, noHeartService);
emit(deviceConnected());
connect(cscBike, SIGNAL(connectedAndDiscovered()), this, SLOT(connectedAndDiscovered()));
//connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart()));
connect(cscBike, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
cscBike->deviceDiscovered(b);
connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop()));
if(!discoveryAgent->isActive())
emit searchingStop();
}
else if(b.name().startsWith("Domyos-Bike") && !b.name().startsWith("DomyosBridge") && !domyosBike && filter)
{
discoveryAgent->stop();
@@ -242,7 +261,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("S77") || b.name().toUpper().startsWith("ESANGLINKER")) && !horizonTreadmill && filter)
{
discoveryAgent->stop();
horizonTreadmill = new horizontreadmill(noWriteResistance, noHeartService);
@@ -380,7 +399,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
connect(toorx, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
toorx->deviceDiscovered(b);
}
else if(((b.name().startsWith("TOORX")) || b.name().toUpper().startsWith("XT485") || (b.name().startsWith("V-RUN")) || (b.name().startsWith("i-Console+")) || (b.name().startsWith("i-Running")) || (device.name().startsWith("F63"))) && !trxappgateusb && !trxappgateusbBike && !toorx_bike && filter)
else if(((b.name().startsWith("TOORX")) || b.name().toUpper().startsWith("XT485") || (b.name().startsWith("V-RUN")) || (b.name().startsWith("i-Console+")) || (b.name().startsWith("i-Running")) || (device.name().startsWith("F63"))) && !trxappgateusb && !trxappgateusbBike && !toorx_bike && !JLL_IC400_bike && filter)
{
discoveryAgent->stop();
trxappgateusb = new trxappgateusbtreadmill();
@@ -390,7 +409,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
connect(trxappgateusb, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
trxappgateusb->deviceDiscovered(b);
}
else if((((b.name().startsWith("TOORX") || b.name().toUpper().startsWith("I-CONSOLE+") || b.name().toUpper().startsWith("ICONSOLE+")) && toorx_bike)) && !trxappgateusb && !trxappgateusbBike && filter)
else if((((b.name().startsWith("TOORX") || b.name().toUpper().startsWith("I-CONSOLE+") || b.name().toUpper().startsWith("ICONSOLE+")) && (toorx_bike || JLL_IC400_bike))) && !trxappgateusb && !trxappgateusbBike && filter)
{
discoveryAgent->stop();
trxappgateusbBike = new trxappgateusbbike(noWriteResistance, noHeartService);
@@ -467,12 +486,41 @@ void bluetooth::connectedAndDiscovered()
((bike*)device())->changeResistance(settings.value("bike_resistance_start", 1).toUInt());
}
if(heartRateBeltName.startsWith("Disabled"))
{
settings.setValue("hrm_lastdevice_name", "");
settings.setValue("hrm_lastdevice_address", "");
}
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) && b.length())
{
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()));
@@ -565,6 +613,11 @@ void bluetooth::restart()
delete domyosElliptical;
domyosElliptical = 0;
}
if(cscBike)
{
delete cscBike;
cscBike = 0;
}
if(toorx)
{
delete toorx;
@@ -654,6 +707,8 @@ bluetoothdevice* bluetooth::device()
return fitshowTreadmill;
else if(domyosElliptical)
return domyosElliptical;
else if(cscBike)
return cscBike;
else if(toorx)
return toorx;
else if(trxappgateusb)

View File

@@ -37,6 +37,7 @@
#include "sportstechbike.h"
#include "skandikawiribike.h"
#include "heartratebelt.h"
#include "cscbike.h"
#include "bluetoothdevice.h"
#include "signalhandler.h"
@@ -73,6 +74,7 @@ private:
eslinkertreadmill* eslinkerTreadmill = 0;
m3ibike* m3iBike = 0;
skandikawiribike* skandikaWiriBike = 0;
cscbike* cscBike = 0;
heartratebelt* heartRateBelt = 0;
QString filterDevice = "";
bool testResistance = false;

View File

@@ -45,7 +45,10 @@ public:
QBluetoothDeviceInfo bluetoothDevice;
void disconnectBluetooth();
virtual void setPaused(bool p);
bool isPaused() {return paused;}
virtual void setLap();
void setAutoResistance(bool value) {autoResistanceEnable = value;}
bool autoResistance() {return autoResistanceEnable;}
void setDifficult(double d);
double difficult();
@@ -86,6 +89,7 @@ protected:
metric m_watt;
bool paused = false;
bool autoResistanceEnable = true;
};
#endif // BLUETOOTHDEVICE_H

473
src/cscbike.cpp Normal file
View File

@@ -0,0 +1,473 @@
#include "cscbike.h"
#include "virtualbike.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QSettings>
#include <QBluetoothLocalDevice>
#include <math.h>
#include <QThread>
#include "ios/lockscreen.h"
#ifdef Q_OS_ANDROID
#include <QLowEnergyConnectionParameters>
#endif
#include "keepawakehelper.h"
cscbike::cscbike(bool noWriteResistance, bool noHeartService)
{
m_watt.setType(metric::METRIC_WATT);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
initDone = false;
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
refresh->start(200);
}
/*
void cscbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool wait_for_response)
{
QEventLoop loop;
QTimer timeout;
if(wait_for_response)
{
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
timeout.singleShot(300, &loop, SLOT(quit()));
}
else
{
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
timeout.singleShot(300, &loop, SLOT(quit()));
}
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray((const char*)data, data_len));
if(!disable_log)
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
loop.exec();
}*/
void cscbike::update()
{
QSettings settings;
QString heartRateBeltName = settings.value("heart_rate_belt_name", "Disabled").toString();
#ifdef Q_OS_ANDROID
if(settings.value("ant_heart", false).toBool())
{
Heart = (uint8_t)KeepAwakeHelper::heart();
debug("Current Heart: " + QString::number(Heart.value()));
}
#endif
if(heartRateBeltName.startsWith("Disabled"))
{
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
lockscreen h;
long appleWatchHeartRate = h.heartRate();
Heart = appleWatchHeartRate;
debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate));
#endif
#endif
}
int avgP = ((settings.value("power_hr_pwr1", 200).toDouble() * settings.value("power_hr_hr2",170).toDouble()) - (settings.value("power_hr_pwr2",230).toDouble() * settings.value("power_hr_hr1",150).toDouble())) / (settings.value("power_hr_hr2",170).toDouble() - settings.value("power_hr_hr1",150).toDouble()) + (Heart.value() * ((settings.value("power_hr_pwr1",200).toDouble() - settings.value("power_hr_pwr2",230).toDouble()) / (settings.value("power_hr_hr1",150).toDouble() - settings.value("power_hr_hr2",170).toDouble())));
if (avgP < 50)
avgP = 50;
m_watt = avgP;
debug("Current Watt: " + QString::number(m_watt.value()));
if(m_control->state() == QLowEnergyController::UnconnectedState)
{
emit disconnected();
return;
}
if(initRequest)
{
initRequest = false;
}
else if(bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState //&&
//gattCommunicationChannelService &&
//gattWriteCharacteristic.isValid() &&
//gattNotify1Characteristic.isValid() &&
/*initDone*/)
{
QDateTime current = QDateTime::currentDateTime();
double deltaTime = (((double)lastTimeUpdate.msecsTo(current)) / ((double)1000.0));
if(currentSpeed().value() > 0.0 && !firstUpdate && !paused)
{
elapsed += deltaTime;
m_watt = (double)watts();
m_jouls += (m_watt.value() * deltaTime);
}
lastTimeUpdate = current;
// updating the treadmill console every second
if(sec1Update++ == (500 / refresh->interval()))
{
sec1Update = 0;
//updateDisplay(elapsed);
}
if(requestResistance != -1)
{
if(requestResistance > 15) requestResistance = 15;
else if(requestResistance == 0) requestResistance = 1;
if(requestResistance != currentResistance().value())
{
debug("writing resistance " + QString::number(requestResistance));
//forceResistance(requestResistance);
}
requestResistance = -1;
}
if(requestStart != -1)
{
debug("starting...");
//btinit();
requestStart = -1;
emit bikeStarted();
}
if(requestStop != -1)
{
debug("stopping...");
//writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
}
firstUpdate = false;
}
void cscbike::serviceDiscovered(const QBluetoothUuid &gatt)
{
debug("serviceDiscovered " + gatt.toString());
}
void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
Q_UNUSED(characteristic);
QSettings settings;
QString heartRateBeltName = settings.value("heart_rate_belt_name", "Disabled").toString();
debug(" << " + newValue.toHex(' '));
if(characteristic.uuid() != QBluetoothUuid((quint16)0x2A5B))
return;
lastPacket = newValue;
uint8_t index = 1;
if(newValue.at(0) == 0x02)
CrankRevs = (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
else
CrankRevs = (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) | ((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | ((uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index)));
if(newValue.at(0) == 0x01)
index += 4;
else
index += 2;
LastCrankEventTime = (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
int16_t deltaT = LastCrankEventTime - oldLastCrankEventTime;
if(deltaT < 0)
{
if(newValue.at(0) == 0x01)
deltaT = LastCrankEventTime + 1024 - oldLastCrankEventTime;
else
deltaT = LastCrankEventTime + 65535 - oldLastCrankEventTime;
}
if(CrankRevs != oldCrankRevs)
{
Cadence = ((CrankRevs - oldCrankRevs) / deltaT) * 1024 * 60;
debug("Current Cadence: " + QString::number(Cadence.value()));
}
oldLastCrankEventTime = LastCrankEventTime;
oldCrankRevs = CrankRevs;
//Distance += ((Speed.value() / 3600000.0) * ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())) );
//debug("Current Distance: " + QString::number(Distance.value()));
//Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))));
//debug("Current Resistance: " + QString::number(Resistance.value()));
KCal += ((( (0.048 * ((double)watts()) + 1.19) * settings.value("weight", 75.0).toFloat() * 3.5) / 200.0 ) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg * 3.5) / 200 ) / 60
debug("Current KCal: " + QString::number(KCal.value()));
if(Cadence.value() > 0)
{
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
bool ios_peloton_workaround = settings.value("ios_peloton_workaround", true).toBool();
if(ios_peloton_workaround && cadence && h && firstStateChanged)
{
h->virtualbike_setCadence(currentCrankRevolutions(),lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)currentHeart().value());
}
#endif
#endif
debug("Current CrankRevs: " + QString::number(CrankRevs));
debug("Last CrankEventTime: " + QString::number(LastCrankEventTime));
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
}
void cscbike::stateChanged(QLowEnergyService::ServiceState state)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
debug("BTLE stateChanged " + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
foreach(QLowEnergyService* s, gattCommunicationChannelService)
{
qDebug() << "stateChanged" << s->serviceUuid() << s->state();
if(s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService)
{
qDebug() << "not all services discovered";
return;
}
}
qDebug() << "all services discovered!";
foreach(QLowEnergyService* s, gattCommunicationChannelService)
{
if(s->state() == QLowEnergyService::ServiceDiscovered)
{
// establish hook into notifications
connect(s, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
this, SLOT(characteristicChanged(QLowEnergyCharacteristic,QByteArray)));
connect(s, SIGNAL(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)),
this, SLOT(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)));
connect(s, SIGNAL(characteristicRead(const QLowEnergyCharacteristic, const QByteArray)),
this, SLOT(characteristicRead(const QLowEnergyCharacteristic, const QByteArray)));
connect(s, SIGNAL(error(QLowEnergyService::ServiceError)),
this, SLOT(errorService(QLowEnergyService::ServiceError)));
connect(s, SIGNAL(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)), this,
SLOT(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)));
connect(s, SIGNAL(descriptorRead(const QLowEnergyDescriptor, const QByteArray)), this,
SLOT(descriptorRead(const QLowEnergyDescriptor, const QByteArray)));
qDebug() << s->serviceUuid() << "connected!";
foreach(QLowEnergyCharacteristic c, s->characteristics())
{
qDebug() << "char uuid" << c.uuid() << "handle" << c.handle();
foreach(QLowEnergyDescriptor d, c.descriptors())
qDebug() << "descriptor uuid" << d.uuid() << "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() << "ClientCharacteristicConfiguration" << c.uuid() << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() << " is not valid";
qDebug() << s->serviceUuid() << c.uuid() << "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() << "ClientCharacteristicConfiguration" << c.uuid() << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() << " is not valid";
qDebug() << s->serviceUuid() << c.uuid() << "indication subscribed!";
}
else if((c.properties() & QLowEnergyCharacteristic::Read) == QLowEnergyCharacteristic::Read)
{
//s->readCharacteristic(c);
//qDebug() << s->serviceUuid() << c.uuid() << "reading!";
}
}
}
}
// ******************************************* virtual bike init *************************************
if(!firstStateChanged && !virtualBike
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
#endif
#endif
)
{
QSettings settings;
bool virtual_device_enabled = settings.value("virtual_device_enabled", true).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
bool ios_peloton_workaround = settings.value("ios_peloton_workaround", true).toBool();
if(ios_peloton_workaround && cadence)
{
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
}
else
#endif
#endif
if(virtual_device_enabled)
{
debug("creating virtual bike interface...");
virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
//connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
}
}
firstStateChanged = 1;
// ********************************************************************************************************
}
void cscbike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
{
debug("descriptorWritten " + descriptor.name() + " " + newValue.toHex(' '));
initRequest = true;
emit connectedAndDiscovered();
}
void cscbike::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
{
qDebug() << "descriptorRead " << descriptor.name() << descriptor.uuid() << newValue.toHex(' ');
}
void cscbike::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
Q_UNUSED(characteristic);
debug("characteristicWritten " + newValue.toHex(' '));
}
void cscbike::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
qDebug() << "characteristicRead " << characteristic.uuid() << newValue.toHex(' ');
}
void cscbike::serviceScanDone(void)
{
debug("serviceScanDone");
#ifdef Q_OS_ANDROID
QLowEnergyConnectionParameters c;
c.setIntervalRange(24,40);
c.setLatency(0);
c.setSupervisionTimeout(420);
m_control->requestConnectionUpdate(c);
#endif
foreach(QBluetoothUuid s, m_control->services())
{
gattCommunicationChannelService.append(m_control->createServiceObject(s));
connect(gattCommunicationChannelService.last(), SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(stateChanged(QLowEnergyService::ServiceState)));
gattCommunicationChannelService.last()->discoverDetails();
}
}
void cscbike::errorService(QLowEnergyService::ServiceError err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
debug("cscbike::errorService" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
}
void cscbike::error(QLowEnergyController::Error err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
debug("cscbike::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
}
void cscbike::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
{
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, SIGNAL(serviceDiscovered(const QBluetoothUuid &)),
this, SLOT(serviceDiscovered(const QBluetoothUuid &)));
connect(m_control, SIGNAL(discoveryFinished()),
this, SLOT(serviceScanDone()));
connect(m_control, SIGNAL(error(QLowEnergyController::Error)),
this, SLOT(error(QLowEnergyController::Error)));
connect(m_control, SIGNAL(stateChanged(QLowEnergyController::ControllerState)), this, SLOT(controllerStateChanged(QLowEnergyController::ControllerState)));
connect(m_control, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, [this](QLowEnergyController::Error error) {
Q_UNUSED(error);
Q_UNUSED(this);
debug("Cannot connect to remote device.");
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
debug("Controller connected. Search services...");
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
debug("LowEnergy controller disconnected");
emit disconnected();
});
// Connect
m_control->connectToDevice();
return;
}
}
bool cscbike::connected()
{
if(!m_control)
return false;
return m_control->state() == QLowEnergyController::DiscoveredState;
}
void* cscbike::VirtualBike()
{
return virtualBike;
}
void* cscbike::VirtualDevice()
{
return VirtualBike();
}
uint16_t cscbike::watts()
{
if(currentCadence().value() == 0) return 0;
return m_watt.value();
}
void cscbike::controllerStateChanged(QLowEnergyController::ControllerState state)
{
qDebug() << "controllerStateChanged" << state;
if(state == QLowEnergyController::UnconnectedState && m_control)
{
qDebug() << "trying to connect back again...";
initDone = false;
m_control->connectToDevice();
}
}

101
src/cscbike.h Normal file
View File

@@ -0,0 +1,101 @@
#ifndef CSCBIKE_H
#define CSCBIKE_H
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QtCore/qmutex.h>
#include <QObject>
#include <QString>
#include <QDateTime>
#include "virtualbike.h"
#include "bike.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class cscbike : public bike
{
Q_OBJECT
public:
cscbike(bool noWriteResistance, bool noHeartService);
bool connected();
void* VirtualBike();
void* VirtualDevice();
private:
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false, bool wait_for_response = false);
void startDiscover();
uint16_t watts();
QTimer* refresh;
virtualbike* virtualBike = 0;
QList<QLowEnergyService*> gattCommunicationChannelService;
//QLowEnergyCharacteristic gattNotify1Characteristic;
QDateTime lastTimeUpdate;
bool firstUpdate = true;
uint8_t sec1Update = 0;
QByteArray lastPacket;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
uint8_t firstStateChanged = 0;
bool initDone = false;
bool initRequest = false;
bool noWriteResistance = false;
bool noHeartService = false;
uint16_t oldLastCrankEventTime = 0;
uint16_t oldCrankRevs = 0;
#ifdef Q_OS_IOS
lockscreen* h = 0;
#endif
signals:
void disconnected();
void debug(QString string);
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void stateChanged(QLowEnergyService::ServiceState state);
void controllerStateChanged(QLowEnergyController::ControllerState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};
#endif // CSCBIKE_H

View File

@@ -7,6 +7,7 @@
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
#include <QSettings>
#include "ios/lockscreen.h"
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testResistance, uint8_t bikeResistanceOffset, double bikeResistanceGain)
{
@@ -38,7 +39,7 @@ void domyosbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString in
if(wait_for_response)
{
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
connect(this, SIGNAL(packetReceived()),
&loop, SLOT(quit()));
timeout.singleShot(300, &loop, SLOT(quit()));
}
@@ -49,6 +50,13 @@ void domyosbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString in
timeout.singleShot(300, &loop, SLOT(quit()));
}
if(gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
m_control->state() == QLowEnergyController::UnconnectedState)
{
qDebug() << "writeCharacteristic error because the connection is closed";
return;
}
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray((const char*)data, data_len));
if(!disable_log)
@@ -164,7 +172,7 @@ void domyosbike::update()
//else
// btinit_telink(false);
}
else if(bluetoothDevice.isValid() &&
else if(/*bluetoothDevice.isValid() &&*/
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
@@ -182,71 +190,95 @@ void domyosbike::update()
lastTimeUpdate = current;
// ******************************************* virtual bike init *************************************
if(!firstVirtual && searchStopped && !virtualBike)
if(!firstStateChanged && !virtualBike
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
#endif
#endif
)
{
QSettings settings;
bool virtual_device_enabled = settings.value("virtual_device_enabled", true).toBool();
if(virtual_device_enabled)
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
bool ios_peloton_workaround = settings.value("ios_peloton_workaround", true).toBool();
if(ios_peloton_workaround && cadence)
{
qDebug() << "creating virtual bike interface...";
virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
//connect(virtualBike,&virtualbike::debug ,this,&domyosbike::debug);
firstVirtual = 1;
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
}
else
#endif
#endif
if(virtual_device_enabled)
{
qDebug() << "creating virtual bike interface...";
virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
//connect(virtualBike,&virtualbike::debug ,this,&schwinnic4bike::debug);
}
}
firstStateChanged = 1;
// ********************************************************************************************************
// updating the treadmill console every second
if(sec1Update++ == (1000 / refresh->interval()))
{
sec1Update = 0;
updateDisplay(elapsed.value());
if(incompletePackets == false)
updateDisplay(elapsed.value());
}
else
{
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true, true);
if(incompletePackets == false)
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true, true);
}
if(testResistance)
{
if((((int)elapsed.value()) % 5) == 0)
if(incompletePackets == false)
{
if(testResistance)
{
uint8_t new_res = currentResistance().value() + 1;
if(new_res > 15)
new_res = 1;
forceResistance(new_res);
if((((int)elapsed.value()) % 5) == 0)
{
uint8_t new_res = currentResistance().value() + 1;
if(new_res > 15)
new_res = 1;
forceResistance(new_res);
}
}
}
if(requestResistance != -1)
{
if(requestResistance > 15) requestResistance = 15;
else if(requestResistance < 1) requestResistance = 1;
if(requestResistance != -1)
{
if(requestResistance > max_resistance) requestResistance = max_resistance;
else if(requestResistance < 1) requestResistance = 1;
if(requestResistance != currentResistance().value())
{
qDebug() << "writing resistance " + QString::number(requestResistance);
forceResistance(requestResistance);
}
requestResistance = -1;
}
if(requestStart != -1)
{
qDebug() << "starting...";
if(requestResistance != currentResistance().value())
{
qDebug() << "writing resistance " + QString::number(requestResistance);
forceResistance(requestResistance);
}
requestResistance = -1;
}
if(requestStart != -1)
{
qDebug() << "starting...";
//if(bike_type == CHANG_YOW)
btinit_changyow(true);
//else
// btinit_telink(true);
//if(bike_type == CHANG_YOW)
btinit_changyow(true);
//else
// btinit_telink(true);
requestStart = -1;
emit bikeStarted();
}
if(requestStop != -1)
{
qDebug() << "stopping...";
writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
requestStart = -1;
emit bikeStarted();
}
if(requestStop != -1)
{
qDebug() << "stopping...";
writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
}
}
@@ -264,33 +296,80 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
Q_UNUSED(characteristic);
QSettings settings;
QString heartRateBeltName = settings.value("heart_rate_belt_name", "Disabled").toString();
QByteArray value = newValue;
qDebug() << " << " + newValue.toHex(' ');
qDebug() << " << " + QString::number(value.length()) + " " + value.toHex(' ');
lastPacket = newValue;
if (newValue.length() != 26)
// for the init packets, the lenght is always less than 20
// for the display and status packets, the lenght is always grater then 20 and there are 2 cases:
// - intense run: it always send more than 20 bytes in one packets, so the lenght will be always != 20
// - t900: it splits packets with lenght grater than 20 in two distinct packets, so the first one it has lenght of 20,
// and the second one with the remained byte
// so this simply condition will match all the cases, excluding the 20byte packet of the T900.
if(newValue.length() != 20)
{
qDebug() << "packetReceived!";
emit packetReceived();
}
QByteArray startBytes;
startBytes.append(0xf0);
startBytes.append(0xbc);
QByteArray startBytes2;
startBytes2.append(0xf0);
startBytes2.append(0xdb);
QByteArray startBytes3;
startBytes3.append(0xf0);
startBytes3.append(0xdd);
// on some treadmills, the 26bytes has splitted in 2 packets
if((lastPacket.length() == 20 && lastPacket.startsWith(startBytes) && value.length() == 6) ||
(lastPacket.length() == 20 && lastPacket.startsWith(startBytes2) && value.length() == 7) ||
(lastPacket.length() == 20 && lastPacket.startsWith(startBytes3) && value.length() == 7))
{
incompletePackets = false;
qDebug() << "...final bytes received";
lastPacket.append(value);
value = lastPacket;
}
lastPacket = value;
if (value.length() != 26)
{
// semaphore for any writing packets (for example, update display)
if(value.length() == 20 && (value.startsWith(startBytes) || value.startsWith(startBytes2) || value.startsWith(startBytes3)))
{
qDebug() << "waiting for other bytes...";
incompletePackets = true;
}
qDebug() << "packet ignored";
return;
}
if (newValue.at(22) == 0x06)
if (value.at(22) == 0x06)
{
qDebug() << "start button pressed!";
requestStart = 1;
}
else if (newValue.at(22) == 0x07)
else if (value.at(22) == 0x07)
{
qDebug() << "stop button pressed!";
requestStop = 1;
}
/*if ((uint8_t)newValue.at(1) != 0xbc && newValue.at(2) != 0x04) // intense run, these are the bytes for the inclination and speed status
/*if ((uint8_t)value.at(1) != 0xbc && value.at(2) != 0x04) // intense run, these are the bytes for the inclination and speed status
return;*/
double speed = GetSpeedFromPacket(newValue);
double kcal = GetKcalFromPacket(newValue);
double distance = GetDistanceFromPacket(newValue);
double speed = GetSpeedFromPacket(value);
double kcal = GetKcalFromPacket(value);
double distance = GetDistanceFromPacket(value);
Cadence = ((uint8_t)newValue.at(9));
Resistance = newValue.at(14);
Cadence = ((uint8_t)value.at(9));
Resistance = value.at(14);
if(Resistance.value() < 1)
{
qDebug() << "invalid resistance value " + QString::number(Resistance.value()) + " putting to default";
@@ -304,7 +383,22 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
#endif
{
if(heartRateBeltName.startsWith("Disabled"))
Heart = ((uint8_t)newValue.at(18));
{
uint8_t heart = ((uint8_t)value.at(18));
if(heart == 0)
{
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
lockscreen h;
long appleWatchHeartRate = h.heartRate();
Heart = appleWatchHeartRate;
qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate);
#endif
#endif
}
else
Heart = heart;
}
}
if(Cadence.value() > 0)
@@ -314,6 +408,18 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
}
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
bool ios_peloton_workaround = settings.value("ios_peloton_workaround", true).toBool();
if(ios_peloton_workaround && cadence && h && firstStateChanged)
{
h->virtualbike_setCadence(currentCrankRevolutions(),lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)currentHeart().value());
}
#endif
#endif
qDebug() << "Current speed: " + QString::number(speed);
qDebug() << "Current cadence: " + QString::number(Cadence.value());
qDebug() << "Current resistance: " + QString::number(Resistance.value());
@@ -579,16 +685,31 @@ void* domyosbike::VirtualDevice()
return VirtualBike();
}
uint8_t domyosbike::resistanceFromPowerRequest(uint16_t power)
{
qDebug() << "resistanceFromPowerRequest" << currentCadence().value();
for(int i = 1; i<max_resistance-1; i++)
{
if(wattsFromResistance(i) <= power && wattsFromResistance(i+1) >= power)
return i;
}
return Resistance.value();
}
uint16_t domyosbike::wattsFromResistance(double resistance)
{
return ((10.39 + 1.45 * (resistance - 1.0)) * (exp(0.028 * (currentCadence().value()))));
}
uint16_t domyosbike::watts()
{
QSettings settings;
double v = 0;
//const uint8_t max_resistance = 15;
// ref https://translate.google.com/translate?hl=it&sl=en&u=https://support.wattbike.com/hc/en-us/articles/115001881825-Power-Resistance-and-Cadence-Tables&prev=search&pto=aue
if(currentSpeed().value() <= 0) return 0;
v = ((10.39 + 1.45 * (currentResistance().value() - 1.0)) * (exp(0.028 * (currentCadence().value()))));
v = wattsFromResistance(currentResistance().value());
return v;
}

View File

@@ -29,11 +29,16 @@
#include "virtualbike.h"
#include "bike.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class domyosbike : public bike
{
Q_OBJECT
public:
domyosbike(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0);
domyosbike(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0);
uint8_t resistanceFromPowerRequest(uint16_t power);
~domyosbike();
bool connected();
@@ -45,6 +50,7 @@ private:
double GetInclinationFromPacket(QByteArray packet);
double GetKcalFromPacket(QByteArray packet);
double GetDistanceFromPacket(QByteArray packet);
uint16_t wattsFromResistance(double resistance);
void forceResistance(int8_t requestResistance);
void updateDisplay(uint16_t elapsed);
void btinit_changyow(bool startTape);
@@ -53,14 +59,17 @@ private:
void startDiscover();
uint16_t watts();
const int max_resistance = 15;
QTimer* refresh;
virtualbike* virtualBike = 0;
uint8_t firstVirtual = 0;
uint8_t firstStateChanged = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotifyCharacteristic;
volatile bool incompletePackets = false;
bool initDone = false;
bool initRequest = false;
bool noWriteResistance = false;
@@ -80,9 +89,14 @@ private:
TELINK,
};
_BIKE_TYPE bike_type = CHANG_YOW;
#ifdef Q_OS_IOS
lockscreen* h = 0;
#endif
signals:
void disconnected();
void packetReceived();
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);

View File

@@ -6,6 +6,7 @@
#include <QMetaEnum>
#include <QSettings>
#include <QBluetoothLocalDevice>
#include "ios/lockscreen.h"
// set speed and incline to 0
uint8_t initData1[] = { 0xf0, 0xc8, 0x01, 0xb9 };
@@ -529,7 +530,22 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
#endif
{
if(heartRateBeltName.startsWith("Disabled"))
Heart = ((uint8_t)value.at(18));
{
uint8_t heart = ((uint8_t)value.at(18));
if(heart == 0)
{
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
lockscreen h;
long appleWatchHeartRate = h.heartRate();
Heart = appleWatchHeartRate;
debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate));
#endif
#endif
}
else
Heart = heart;
}
}
FanSpeed = value.at(23);

View File

@@ -28,6 +28,10 @@
#include "virtualtreadmill.h"
#include "treadmill.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class domyostreadmill : public treadmill
{
Q_OBJECT
@@ -79,6 +83,10 @@ private:
bool initDone = false;
bool initRequest = false;
#ifdef Q_OS_IOS
lockscreen* h = 0;
#endif
signals:
void disconnected();

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,37 @@ 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 Resistance.value();
}
uint8_t echelonconnectsport::resistanceFromPowerRequest(uint16_t power)
{
qDebug() << "resistanceFromPowerRequest" << Cadence.value();
for(int i = 1; i<max_resistance-1; i++)
{
if(wattsFromResistance(i) <= power && wattsFromResistance(i+1) >= power)
return i;
}
return Resistance.value();
}
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 +224,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;
@@ -475,7 +508,11 @@ void* echelonconnectsport::VirtualDevice()
uint16_t echelonconnectsport::watts()
{
if(currentCadence().value() == 0) return 0;
return wattsFromResistance(Resistance.value());
}
uint16_t echelonconnectsport::wattsFromResistance(double resistance)
{
// https://github.com/cagnulein/qdomyos-zwift/issues/62#issuecomment-736913564
/*if(currentCadence().value() < 90)
return (uint16_t)((3.59 * exp(0.0217 * (double)(currentCadence().value()))) * exp(0.095 * (double)(currentResistance().value())) );
@@ -520,7 +557,7 @@ uint16_t echelonconnectsport::watts()
{Epsilon, 12.5, 48.0, 99.3, 162.2, 232.9, 310.4, 400.3, 435.5, 530.5, 589.0},
{Epsilon, 13.0, 53.0, 102.0, 170.3, 242.0, 320.0, 427.9, 475.2, 570.0, 625.0}};
int level = Resistance.value();
int level = resistance;
if (level < 0) {
level = 0;
}

View File

@@ -39,13 +39,18 @@ class echelonconnectsport : public bike
Q_OBJECT
public:
echelonconnectsport(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain);
int pelotonToBikeResistance(int pelotonResistance);
uint8_t resistanceFromPowerRequest(uint16_t power);
bool connected();
void* VirtualBike();
void* VirtualDevice();
private:
const int max_resistance = 32;
double bikeResistanceToPeloton(double resistance);
double GetDistanceFromPacket(QByteArray packet);
uint16_t wattsFromResistance(double resistance);
QTime GetElapsedFromPacket(QByteArray packet);
void btinit();
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false, bool wait_for_response = false);

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

@@ -18,7 +18,6 @@
#include "gpx.h"
#include "qfit.h"
#include "material.h"
#include "screencapture.h"
#ifndef IO_UNDER_QT
#include "secret.h"
#endif
@@ -77,6 +76,9 @@ homeform::homeform(QQmlApplicationEngine* engine, bluetooth* bl)
resistance = new DataObject("Resistance (%)", "icons/icons/resistance.png", "0", true, "resistance", 48, labelFontSize);
peloton_resistance = new DataObject("Peloton R(%)", "icons/icons/resistance.png", "0", false, "peloton_resistance", 48, labelFontSize);
target_resistance = new DataObject("Target R(%)", "icons/icons/resistance.png", "0", true, "target_resistance", 48, labelFontSize);
target_peloton_resistance = new DataObject("T.Peloton R(%)", "icons/icons/resistance.png", "0", false, "target_peloton_resistance", 48, labelFontSize);
target_cadence = new DataObject("T.Cadence(rpm)", "icons/icons/cadence.png", "0", false, "target_cadence", 48, labelFontSize);
target_power = new DataObject("T.Power(W)", "icons/icons/watt.png", "0", false, "target_power", 48, labelFontSize);
watt = new DataObject("Watt", "icons/icons/watt.png", "0", false, "watt", 48, labelFontSize);
avgWatt = new DataObject("AVG Watt", "icons/icons/watt.png", "0", false, "avgWatt", 48, labelFontSize);
ftp = new DataObject("FTP Zone", "icons/icons/watt.png", "0", false, "ftp", 48, labelFontSize);
@@ -133,6 +135,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 +151,10 @@ homeform::homeform(QQmlApplicationEngine* engine, bluetooth* bl)
emit tile_orderChanged(tile_order());
pelotonHandler = new peloton(bl);
connect(pelotonHandler, SIGNAL(workoutStarted(QString, QString)), this, SLOT(pelotonWorkoutStarted(QString, QString)));
connect(pelotonHandler, SIGNAL(loginState(bool)), this, SLOT(pelotonLoginState(bool)));
//populate the UI
#if 0
#warning("disable me!")
@@ -202,6 +211,41 @@ 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::pelotonLoginState(bool ok)
{
m_pelotonLoginState = (ok?1:0);
emit pelotonLoginChanged(m_pelotonLoginState);
}
void homeform::pelotonWorkoutStarted(QString name, QString instructor)
{
QSettings settings;
if(!settings.value("top_bar_enabled", true).toBool()) return;
m_info = name;
emit infoChanged(m_info);
stravaPelotonActivityName = name;
stravaPelotonInstructorName = instructor;
m_pelotonAskStart = true;
emit(changePelotonAskStart(pelotonAskStart()));
}
void homeform::backup()
{
static uint8_t index = 0;
@@ -280,6 +324,9 @@ void homeform::trainProgramSignals()
disconnect(trainProgram, SIGNAL(changeFanSpeed(uint8_t)), ((treadmill*)bluetoothManager->device()), SLOT(changeFanSpeed(uint8_t)));
disconnect(trainProgram, SIGNAL(changeSpeedAndInclination(double, double)), ((treadmill*)bluetoothManager->device()), SLOT(changeSpeedAndInclination(double, double)));
disconnect(trainProgram, SIGNAL(changeResistance(int8_t)), ((bike*)bluetoothManager->device()), SLOT(changeResistance(int8_t)));
disconnect(trainProgram, SIGNAL(changeRequestedPelotonResistance(int8_t)), ((bike*)bluetoothManager->device()), SLOT(changeRequestedPelotonResistance(int8_t)));
disconnect(trainProgram, SIGNAL(changeCadence(int16_t)), ((bike*)bluetoothManager->device()), SLOT(changeCadence(int16_t)));
disconnect(trainProgram, SIGNAL(changePower(int32_t)), ((bike*)bluetoothManager->device()), SLOT(changePower(int32_t)));
disconnect(((treadmill*)bluetoothManager->device()), SIGNAL(tapeStarted()), trainProgram, SLOT(onTapeStarted()));
disconnect(((bike*)bluetoothManager->device()), SIGNAL(bikeStarted()), trainProgram, SLOT(onTapeStarted()));
@@ -290,6 +337,9 @@ void homeform::trainProgramSignals()
connect(trainProgram, SIGNAL(changeInclination(double)), ((treadmill*)bluetoothManager->device()), SLOT(changeInclination(double)));
connect(trainProgram, SIGNAL(changeSpeedAndInclination(double, double)), ((treadmill*)bluetoothManager->device()), SLOT(changeSpeedAndInclination(double, double)));
connect(trainProgram, SIGNAL(changeResistance(int8_t)), ((bike*)bluetoothManager->device()), SLOT(changeResistance(int8_t)));
connect(trainProgram, SIGNAL(changeRequestedPelotonResistance(int8_t)), ((bike*)bluetoothManager->device()), SLOT(changeRequestedPelotonResistance(int8_t)));
connect(trainProgram, SIGNAL(changeCadence(int16_t)), ((bike*)bluetoothManager->device()), SLOT(changeCadence(int16_t)));
connect(trainProgram, SIGNAL(changePower(int32_t)), ((bike*)bluetoothManager->device()), SLOT(changePower(int32_t)));
connect(((treadmill*)bluetoothManager->device()), SIGNAL(tapeStarted()), trainProgram, SLOT(onTapeStarted()));
connect(((bike*)bluetoothManager->device()), SIGNAL(bikeStarted()), trainProgram, SLOT(onTapeStarted()));
@@ -304,7 +354,7 @@ void homeform::trainProgramSignals()
QStringList homeform::tile_order()
{
QStringList r;
for(int i = 0; i < 19; i++)
for(int i = 0; i < 22; i++)
r.append(QString::number(i));
return r;
}
@@ -428,6 +478,15 @@ void homeform::deviceConnected()
if(settings.value("tile_target_resistance_enabled", true).toBool() && settings.value("tile_target_resistance_order", 0).toInt() == i)
dataList.append(target_resistance);
if(settings.value("tile_target_peloton_resistance_enabled", false).toBool() && settings.value("tile_target_peloton_resistance_order", 21).toInt() == i)
dataList.append(target_peloton_resistance);
if(settings.value("tile_target_cadence_enabled", false).toBool() && settings.value("tile_target_cadence_order", 19).toInt() == i)
dataList.append(target_cadence);
if(settings.value("tile_target_power_enabled", false).toBool() && settings.value("tile_target_power_order", 20).toInt() == i)
dataList.append(target_power);
if(settings.value("tile_lapelapsed_enabled", false).toBool() && settings.value("tile_lapelapsed_order", 18).toInt() == i)
dataList.append(lapElapsed);
}
@@ -679,6 +738,9 @@ void homeform::Start()
if(bluetoothManager->device())
bluetoothManager->device()->clearStats();
Session.clear();
stravaPelotonActivityName = "";
stravaPelotonInstructorName = "";
}
paused = false;
@@ -708,7 +770,7 @@ void homeform::Stop()
bluetoothManager->device()->stop();
paused = false;
stopped = true;
stopped = true;
fit_save_clicked();
@@ -822,6 +884,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,8 +930,12 @@ 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->target_peloton_resistance->setValue(QString::number(((bike*)bluetoothManager->device())->lastRequestedPelotonResistance().value(), 'f', 0));
this->target_cadence->setValue(QString::number(((bike*)bluetoothManager->device())->lastRequestedCadence().value(), 'f', 0));
this->target_power->setValue(QString::number(((bike*)bluetoothManager->device())->lastRequestedPower().value(), 'f', 0));
this->resistance->setValue(QString::number(resistance, 'f', 0));
this->cadence->setValue(QString::number(cadence));
@@ -1026,7 +1093,7 @@ void homeform::update()
{
speed = (double)r.bounded(settings.value("trainprogram_speed_min", 8).toUInt() * 10, settings.value("trainprogram_speed_max", 16).toUInt() * 10) / 10.0;
}
if(r.bounded(settings.value("trainprogram_incline_min", 0).toUInt() < settings.value("trainprogram_incline_max", 15).toUInt()))
if(settings.value("trainprogram_incline_min", 0).toUInt() < settings.value("trainprogram_incline_max", 15).toUInt())
{
incline = (double)r.bounded(settings.value("trainprogram_incline_min", 0).toUInt() * 10, settings.value("trainprogram_incline_max", 15).toUInt() * 10) / 10.0;
}
@@ -1064,6 +1131,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(),
@@ -1141,12 +1209,6 @@ void homeform::fit_save_clicked()
if(bluetoothManager->device())
{
QString filenameScreenshot = path + QDateTime::currentDateTime().toString().replace(":", "_") + ".jpg";
QObject *rootObject = engine->rootObjects().first();
QObject *stack = rootObject;
screenCapture s((QQuickView*) stack);
s.capture(filenameScreenshot);
QString filename = path + QDateTime::currentDateTime().toString().replace(":", "_") + ".fit";
qfit::save(filename, Session, bluetoothManager->device()->deviceType());
@@ -1349,13 +1411,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 + " - " + stravaPelotonInstructorName + 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,14 @@
#include <QQmlApplicationEngine>
#include <QOAuth2AuthorizationCodeFlow>
#include <QNetworkReply>
#include <QGraphicsScene>
#include <QChart>
#include <QColor>
#include "screencapture.h"
#include "bluetooth.h"
#include "sessionline.h"
#include "trainprogram.h"
#include "peloton.h"
class DataObject : public QObject
{
@@ -76,6 +81,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,8 +94,124 @@ 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(int pelotonLogin READ pelotonLogin NOTIFY pelotonLoginChanged)
Q_PROPERTY(QString workoutStartDate READ workoutStartDate)
Q_PROPERTY(QString workoutName READ workoutName)
Q_PROPERTY(QString instructorName READ instructorName)
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)
Q_PROPERTY(bool autoResistance READ autoResistance NOTIFY autoResistanceChanged WRITE setAutoResistance)
public:
Q_INVOKABLE void save_screenshot()
{
QString path = "";
#if defined(Q_OS_ANDROID) || defined(Q_OS_MACOS) || defined(Q_OS_OSX)
path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/";
#elif defined(Q_OS_IOS)
path = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/";
#endif
QString filenameScreenshot = path + QDateTime::currentDateTime().toString().replace(":", "_") + ".jpg";
QObject *rootObject = engine->rootObjects().first();
QObject *stack = rootObject;
screenCapture s((QQuickView*) stack);
s.capture(filenameScreenshot);
}
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;}
@@ -101,11 +223,28 @@ 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";}}
QString instructorName() {return stravaPelotonInstructorName;}
int pelotonLogin() {return m_pelotonLoginState;}
bool pelotonAskStart() {return m_pelotonAskStart;}
void setPelotonAskStart(bool value) {m_pelotonAskStart = value;}
bool generalPopupVisible();
bool labelHelp();
QStringList bluetoothDevices();
QStringList tile_order();
bool autoResistance() {return m_autoresistance;}
void setAutoResistance(bool value) { m_autoresistance = value; emit autoResistanceChanged(value); if(bluetoothManager->device()) bluetoothManager->device()->setAutoResistance(value); }
void setGeneralPopupVisible(bool value);
int workout_sample_points() { return Session.count();}
double wattMaxChart() {QSettings settings; if(bluetoothManager && bluetoothManager->device() && bluetoothManager->device()->wattsMetric().max() > (settings.value("ftp", 200.0).toDouble() * 2)) return bluetoothManager->device()->wattsMetric().max(); else { 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;
@@ -126,6 +265,14 @@ private:
bool stopped = false;
bool lapTrigger = false;
peloton* pelotonHandler = 0;
bool m_pelotonAskStart = false;
int m_pelotonLoginState = -1;
QString stravaPelotonActivityName = "";
QString stravaPelotonInstructorName = "";
bool m_autoresistance = true;
DataObject* speed;
DataObject* inclination;
DataObject* cadence;
@@ -143,6 +290,9 @@ private:
DataObject* elapsed;
DataObject* peloton_resistance;
DataObject* target_resistance;
DataObject* target_peloton_resistance;
DataObject* target_cadence;
DataObject* target_power;
DataObject* ftp;
DataObject* lapElapsed;
@@ -187,6 +337,9 @@ private slots:
void callbackReceived(const QVariantMap &values);
void writeFileCompleted();
void errorOccurredUploadStrava(QNetworkReply::NetworkError code);
void pelotonWorkoutStarted(QString name, QString instructor);
void pelotonLoginState(bool ok);
void peloton_start_workout();
signals:
@@ -204,7 +357,10 @@ signals:
void bluetoothDevicesChanged(QStringList value);
void tile_orderChanged(QStringList value);
void changeLabelHelp(bool value);
void changePelotonAskStart(bool value);
void generalPopupVisibleChanged(bool value);
void autoResistanceChanged(bool value);
void pelotonLoginChanged(int ok);
};
#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

@@ -15,10 +15,12 @@ inspirebike::inspirebike(bool noWriteResistance, bool noHeartService)
{
m_watt.setType(metric::METRIC_WATT);
refresh = new QTimer(this);
t_timeout = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
//initDone = false;
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
connect(t_timeout, SIGNAL(timeout()), this, SLOT(connection_timeout()));
refresh->start(200);
}
@@ -47,6 +49,12 @@ inspirebike::inspirebike(bool noWriteResistance, bool noHeartService)
loop.exec();
}*/
void inspirebike::connection_timeout()
{
qDebug() << "connection timeout triggered!";
m_control->disconnectFromDevice();
}
void inspirebike::update()
{
qDebug() << m_control->state() << bluetoothDevice.isValid() <<
@@ -200,6 +208,8 @@ void inspirebike::characteristicChanged(const QLowEnergyCharacteristic &characte
debug("Last CrankEventTime: " + QString::number(LastCrankEventTime));
debug("Current Watt: " + QString::number(watts()));
t_timeout->start(3000);
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
}

View File

@@ -49,6 +49,7 @@ private:
uint16_t watts();
QTimer* refresh;
QTimer* t_timeout;
virtualbike* virtualBike = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
@@ -86,6 +87,7 @@ private slots:
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void connection_timeout();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};

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

@@ -126,7 +126,7 @@ QCoreApplication* createApplication(int &argc, char *argv[])
if(nogui)
return new QCoreApplication(argc, argv);
else if(forceQml)
return new QGuiApplication(argc, argv);
return new QApplication(argc, argv);
else
{
QApplication* a = new QApplication(argc, argv);
@@ -238,7 +238,7 @@ int main(int argc, char *argv[])
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
#else
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QScopedPointer<QGuiApplication> app(new QGuiApplication(argc, argv));
QScopedPointer<QApplication> app(new QApplication(argc, argv));
#endif
app->setOrganizationName("Roberto Viola");
@@ -275,6 +275,13 @@ int main(int argc, char *argv[])
qInstallMessageHandler(myMessageOutput);
qDebug() << "version " << app->applicationVersion();
foreach(QString s, settings.allKeys())
{
if(!s.contains("password"))
{
qDebug() << s << settings.value(s);
}
}
#if 0 // test gpx or fit export
QList<SessionLine> l;

View File

@@ -2,6 +2,7 @@ import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtQuick.Dialogs 1.0
import QtGraphicalEffects 1.12
ApplicationWindow {
id: window
@@ -130,6 +131,48 @@ ApplicationWindow {
}
}
Popup {
id: popupAutoResistance
parent: Overlay.overlay
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
width: 380
height: 60
modal: true
focus: true
palette.text: "white"
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition
{
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
}
exit: Transition
{
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Auto Resistance " + (rootItem.autoResistance?"enabled":"disabled"))
}
}
}
Timer {
id: popupAutoResistanceAutoClose
interval: 2000; running: false; repeat: false
onTriggered: popupAutoResistance.close();
}
ToolButton {
id: toolButtonAutoResistance
icon.source: ( rootItem.autoResistance ? "icons/icons/resistance.png" : "icons/icons/pause.png")
onClicked: { rootItem.autoResistance = !rootItem.autoResistance; console.log("auto resistance toggled " + rootItem.autoResistance); popupAutoResistance.open(); popupAutoResistanceAutoClose.running = true; }
anchors.right: parent.right
}
Label {
text: stackView.currentItem.title
anchors.centerIn: parent
@@ -152,6 +195,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(),

269
src/peloton.cpp Normal file
View File

@@ -0,0 +1,269 @@
#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();
emit loginState(user_id.length());
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
current_workout_status = status;
}
else
{
timer->start(10000); // check for a status changed
// i don't need to set current_workout_status because, the bike was missing and than i didn't set the workout
}
}
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::instructor_onfinish(QNetworkReply* reply)
{
disconnect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(instructor_onfinish(QNetworkReply*)));
QByteArray payload = reply->readAll(); // JSON
QJsonParseError parseError;
instructor = QJsonDocument::fromJson(payload, &parseError);
current_instructor_name = instructor.object()["name"].toString();
if(log_request)
qDebug() << "instructor_onfinish" << instructor;
else
qDebug() << "instructor_onfinish";
getPerformance(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();
current_instructor_id = ride["instructor_id"].toString();
if(log_request)
qDebug() << "workout_onfinish" << workout;
else
qDebug() << "workout_onfinish";
getInstructor(current_instructor_id);
}
void peloton::performance_onfinish(QNetworkReply* reply)
{
QSettings settings;
QString difficulty = settings.value("peloton_difficulty", "lower").toString();
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 = resistances["graph_data"].toObject();
QJsonArray lower_resistances = graph_data_resistances[difficulty].toArray();
QJsonObject cadences = target_graph_metrics[0].toObject();
QJsonObject graph_data_cadences = cadences["graph_data"].toObject();
QJsonArray lower_cadences = graph_data_cadences[difficulty].toArray();
trainrows.clear();
for(int i=0; i<lower_resistances.count(); i++)
{
trainrow r;
r.duration = QTime(0,0,peloton_workout_second_resolution,0);
r.resistance = ((bike*)bluetoothManager->device())->pelotonToBikeResistance(lower_resistances.at(i).toInt());
r.requested_peloton_resistance = lower_resistances.at(i).toInt();
r.cadence = lower_cadences.at(i).toInt();
trainrows.append(r);
}
if(log_request)
qDebug() << "performance_onfinish" << performance;
else
qDebug() << "performance_onfinish" << trainrows.length();
if(trainrows.length())
{
emit workoutStarted(current_workout_name, current_instructor_name);
}
timer->start(30000); // check for a status changed
}
void peloton::getInstructor(QString instructor_id)
{
connect(mgr,SIGNAL(finished(QNetworkReply*)),this,SLOT(instructor_onfinish(QNetworkReply*)));
QUrl url("https://api.onepeloton.com/api/instructor/" + instructor_id);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::UserAgentHeader, "qdomyos-zwift");
mgr->get(request);
}
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 = 1; // for now we don't need more than 1 workout
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);
}

70
src/peloton.h Normal file
View File

@@ -0,0 +1,70 @@
#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 = "";
QString current_instructor_id = "";
QString current_instructor_name = "";
QJsonDocument current_workout;
QJsonDocument current_workout_summary;
QJsonDocument workout;
QJsonDocument instructor;
QJsonDocument performance;
QTimer* timer;
bluetooth* bluetoothManager = 0;
int total_workout;
void getWorkoutList(int num);
void getSummary(QString workout);
void getWorkout(QString workout);
void getInstructor(QString instructor_id);
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 instructor_onfinish(QNetworkReply* reply);
void startEngine();
signals:
void loginState(bool ok);
void workoutStarted(QString name, QString instructor);
};
#endif // PELOTON_H

View File

@@ -1,6 +1,6 @@
QT += bluetooth widgets xml positioning quick networkauth
!ios: QT+= charts
QT+= charts
unix:android: QT += androidextras gui-private
CONFIG += c++11 console debug app_bundle
@@ -23,6 +23,7 @@ SOURCES += \
bike.cpp \
bluetooth.cpp \
bluetoothdevice.cpp \
cscbike.cpp \
domyoselliptical.cpp \
domyostreadmill.cpp \
echelonconnectsport.cpp \
@@ -63,6 +64,7 @@ SOURCES += \
keepawakehelper.cpp \
main.cpp \
metric.cpp \
peloton.cpp \
proformbike.cpp \
proformtreadmill.cpp \
qfit.cpp \
@@ -98,6 +100,7 @@ HEADERS += \
bike.h \
bluetooth.h \
bluetoothdevice.h \
cscbike.h \
domyoselliptical.h \
domyostreadmill.h \
echelonconnectsport.h \
@@ -334,6 +337,7 @@ HEADERS += \
ios/M3iIOS-Interface.h \
material.h \
metric.h \
peloton.h \
proformbike.h \
proformtreadmill.h \
qfit.h \
@@ -420,4 +424,4 @@ ios {
QMAKE_TARGET_BUNDLE_PREFIX = org.cagnulein
}
VERSION = 2.5.12
VERSION = 2.5.22

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

@@ -19,7 +19,8 @@ void trainprogram::scheduler()
started == false ||
enabled == false ||
bluetoothManager->device() == nullptr ||
bluetoothManager->device()->currentSpeed().value() <= 0
bluetoothManager->device()->currentSpeed().value() <= 0 ||
bluetoothManager->device()->isPaused()
)
{
return;
@@ -50,6 +51,18 @@ void trainprogram::scheduler()
{
qDebug() << "trainprogram change resistance" + QString::number(rows[0].resistance);
emit changeResistance(rows[0].resistance);
if(rows[0].cadence != -1)
{
qDebug() << "trainprogram change cadence" + QString::number(rows[0].cadence);
emit changeCadence(rows[0].cadence);
}
if(rows[0].requested_peloton_resistance != -1)
{
qDebug() << "trainprogram change requested peloton resistance" + QString::number(rows[0].requested_peloton_resistance);
emit changeRequestedPelotonResistance(rows[0].requested_peloton_resistance);
}
}
if(rows[0].fanspeed != -1)
@@ -93,6 +106,18 @@ void trainprogram::scheduler()
{
qDebug() << "trainprogram change resistance" + QString::number(rows[currentStep].resistance);
emit changeResistance(rows[currentStep].resistance);
if(rows[currentStep].cadence != -1)
{
qDebug() << "trainprogram change cadence" + QString::number(rows[currentStep].cadence);
emit changeCadence(rows[currentStep].cadence);
}
if(rows[currentStep].requested_peloton_resistance != -1)
{
qDebug() << "trainprogram change requested peloton resistance" + QString::number(rows[currentStep].requested_peloton_resistance);
emit changeRequestedPelotonResistance(rows[currentStep].requested_peloton_resistance);
}
}
if(rows[currentStep].fanspeed != -1)
@@ -139,6 +164,8 @@ void trainprogram::save(QString filename)
stream.writeAttribute("speed", QString::number(row.speed));
stream.writeAttribute("inclination", QString::number(row.inclination));
stream.writeAttribute("resistance", QString::number(row.resistance));
stream.writeAttribute("requested_peloton_resistance", QString::number(row.requested_peloton_resistance));
stream.writeAttribute("cadence", QString::number(row.cadence));
stream.writeAttribute("forcespeed", row.forcespeed?"1":"0");
stream.writeAttribute("fanspeed", QString::number(row.fanspeed));
stream.writeEndElement();
@@ -168,6 +195,8 @@ trainprogram* trainprogram::load(QString filename, bluetooth* b)
row.fanspeed = -1;
row.inclination = atts.value("inclination").toDouble();
row.resistance = atts.value("resistance").toInt();
row.requested_peloton_resistance = atts.value("requested_peloton_resistance").toInt();
row.cadence = atts.value("cadence").toInt();
row.forcespeed = atts.value("forcespeed").toInt()?true:false ;
list.append(row);
}

View File

@@ -8,12 +8,14 @@
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;
int8_t requested_peloton_resistance = -1;
int16_t cadence = -1;
bool forcespeed = false;
};
class trainprogram: public QObject
@@ -48,6 +50,8 @@ signals:
bool changeFanSpeed(uint8_t speed);
void changeInclination(double inclination);
void changeResistance(int8_t resistance);
void changeRequestedPelotonResistance(int8_t resistance);
void changeCadence(int16_t cadence);
void changeSpeedAndInclination(double speed, double inclination);
private:

View File

@@ -107,6 +107,15 @@ void trxappgateusbbike::update()
const uint8_t noOpData[] = { 0xf0, 0xa2, 0x23, 0x01, 0xb6 };
writeCharacteristic((uint8_t*)noOpData, sizeof(noOpData), "noOp", false, true);
}
else if(bike_type == TYPE::JLL_IC400)
{
static unsigned char pollCounter = 0x0b;
uint8_t noOpData[] = { 0xf0, 0xa2, 0x00, 0xc8, 0x59 };
noOpData[2] = pollCounter;
noOpData[4] += pollCounter;
pollCounter += 0x0c;
writeCharacteristic((uint8_t*)noOpData, sizeof(noOpData), "noOp", false, true);
}
else
{
const uint8_t noOpData[] = { 0xf0, 0xa2, 0x23, 0xd3, 0x88 };
@@ -146,7 +155,7 @@ void trxappgateusbbike::characteristicChanged(const QLowEnergyCharacteristic &ch
debug(" << " + newValue.toHex(' '));
lastPacket = newValue;
if (newValue.length() != 21)
if ((newValue.length() != 21 && bike_type != JLL_IC400) || (newValue.length() != 19 && bike_type == JLL_IC400))
return;
/*
@@ -168,11 +177,22 @@ void trxappgateusbbike::characteristicChanged(const QLowEnergyCharacteristic &ch
}
*/
double speed = GetSpeedFromPacket(newValue);
double cadence = GetCadenceFromPacket(newValue);
double resistance = GetResistanceFromPacket(newValue);
double kcal = GetKcalFromPacket(newValue);
double watt = GetWattFromPacket(newValue);
double speed = 0;
double resistance = 0;
double kcal = 0;
double watt = 0;
if(bike_type != JLL_IC400)
{
speed = GetSpeedFromPacket(newValue);
resistance = GetResistanceFromPacket(newValue);
kcal = GetKcalFromPacket(newValue);
watt = GetWattFromPacket(newValue);
}
else
{
speed = cadence * 0.37407407407407407407407407407407;
}
#ifdef Q_OS_ANDROID
if(settings.value("ant_heart", false).toBool())
@@ -302,6 +322,43 @@ void trxappgateusbbike::btinit(bool startTape)
writeCharacteristic((uint8_t*)initData6, sizeof(initData6), "init", false, true);
QThread::msleep(400);
}
else if(bike_type == TYPE::JLL_IC400)
{
const uint8_t initData1[] = { 0xf0, 0xa0, 0x01, 0x01, 0x92 };
const uint8_t initData2[] = { 0xf0, 0xa0, 0x03, 0xc9, 0x5c };
const uint8_t initData3[] = { 0xf0, 0xa1, 0x00, 0xc8, 0x59 };
const uint8_t initData4[] = { 0xf0, 0xa0, 0x01, 0xc9, 0x5a };
const uint8_t initData5[] = { 0xf0, 0xa1, 0x05, 0xc8, 0x5e };
const uint8_t initData6[] = { 0xf0, 0xa2, 0x22, 0xc8, 0x7c };
const uint8_t initData7[] = { 0xf0, 0xa0, 0x39, 0xc9, 0x92 };
writeCharacteristic((uint8_t*)initData1, sizeof(initData1), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData2, sizeof(initData2), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData3, sizeof(initData3), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData4, sizeof(initData4), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData3, sizeof(initData3), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData4, sizeof(initData4), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData3, sizeof(initData3), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData4, sizeof(initData4), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData3, sizeof(initData3), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData4, sizeof(initData4), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData5, sizeof(initData5), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData6, sizeof(initData6), "init", false, true);
QThread::msleep(400);
writeCharacteristic((uint8_t*)initData7, sizeof(initData7), "init", false, true);
QThread::msleep(400);
}
else
{
const uint8_t initData1[] = { 0xf0, 0xa0, 0x01, 0x01, 0x92 };
@@ -339,7 +396,7 @@ void trxappgateusbbike::stateChanged(QLowEnergyService::ServiceState state)
QString uuidNotify1 = "0000fff1-0000-1000-8000-00805f9b34fb";
QString uuidNotify2 = "49535343-4c8a-39b3-2f49-511cff073b7e";
if(bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE)
if(bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE || bike_type == TYPE::JLL_IC400)
{
uuidWrite = "49535343-8841-43f4-a8d4-ecbe34729bb3";
uuidNotify1 = "49535343-1E4D-4BD9-BA61-23C647249616";
@@ -413,7 +470,7 @@ void trxappgateusbbike::serviceScanDone(void)
debug("serviceScanDone");
QString uuid = "0000fff0-0000-1000-8000-00805f9b34fb";
if(bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE)
if(bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE || bike_type == TYPE::JLL_IC400)
uuid = "49535343-FE7D-4AE5-8FA9-9FAFD205E455";
QBluetoothUuid _gattCommunicationChannelServiceId((QString)uuid);
@@ -443,6 +500,8 @@ void trxappgateusbbike::error(QLowEnergyController::Error err)
void trxappgateusbbike::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
QSettings settings;
bool JLL_IC400_bike = settings.value("jll_IC400_bike", false).toBool();
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
//if(device.name().startsWith("TOORX") || device.name().startsWith("V-RUN") || device.name().startsWith("FS-") || device.name().startsWith("i-Console+") || device.name().startsWith("i-Running"))
{
@@ -451,7 +510,12 @@ void trxappgateusbbike::deviceDiscovered(const QBluetoothDeviceInfo &device)
/*else
bike_type = TYPE::TRXAPPGATE;*/
if(device.address().toString().toUpper().startsWith("E8"))
if(JLL_IC400_bike)
{
bike_type = TYPE::JLL_IC400;
qDebug() << "JLL_IC400 bike found";
}
else if(device.address().toString().toUpper().startsWith("E8"))
{
bike_type = TYPE::CHANGYOW;
qDebug() << "CHANGYOW bike found";

View File

@@ -85,6 +85,7 @@ private:
CHANGYOW = 2,
SKANDIKAWIRY = 3,
ICONSOLE = 4,
JLL_IC400 = 5,
} TYPE;
TYPE bike_type = TRXAPPGATE;

View File

@@ -83,7 +83,7 @@ virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService, u
}
else
{
services << (QBluetoothUuid((QString)"0bf669f1-45f2-11e7-9598-0800200c9a66"));
services << (QBluetoothUuid((QString)"0bf669f0-45f2-11e7-9598-0800200c9a66"));
}
advertisingData.setServices(services);
@@ -261,9 +261,12 @@ virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService, u
}
else
{
serviceEchelon.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceEchelon.setUuid((QBluetoothUuid((QString)"0bf669f0-45f2-11e7-9598-0800200c9a66")));
QLowEnergyCharacteristicData charData;
charData.setUuid(QBluetoothUuid((QString)"0bf669f2-45f2-11e7-9598-0800200c9a66"));
charData.setProperties(QLowEnergyCharacteristic::Write);
charData.setProperties(QLowEnergyCharacteristic::Write | QLowEnergyCharacteristic::WriteNoResponse);
QLowEnergyCharacteristicData charData2;
charData2.setUuid(QBluetoothUuid((QString)"0bf669f4-45f2-11e7-9598-0800200c9a66"));
@@ -289,7 +292,7 @@ virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService, u
serviceData.setUuid(QBluetoothUuid((QString)"0bf669f1-45f2-11e7-9598-0800200c9a66"));
serviceData.addCharacteristic(charData);
serviceData.addCharacteristic(charData3);
serviceData.addCharacteristic(charData2);
serviceData.addCharacteristic(charData2);
}
if(battery)
@@ -355,6 +358,7 @@ virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService, u
}
else
{
service = leController->addService(serviceEchelon);
service = leController->addService(serviceData);
}
@@ -401,6 +405,8 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte
QByteArray reply;
QSettings settings;
bool force_resistance = settings.value("virtualbike_forceresistance", true).toBool();
bool erg_mode = settings.value("zwift_erg", false).toBool();
double erg_filter = settings.value("zwift_erg_filter", 0.0).toDouble();
qDebug() << "characteristicChanged " + QString::number(characteristic.uuid().toUInt16()) + " " + newValue.toHex(' ');
switch(characteristic.uuid().toUInt16())
@@ -411,7 +417,7 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte
// Set Target Resistance
uint8_t uresistance = newValue.at(1);
uresistance = uresistance / 10;
if(force_resistance)
if(force_resistance && !erg_mode)
Bike->changeResistance(uresistance);
qDebug() << "new requested resistance " + QString::number(uresistance) + " enabled " + force_resistance;
reply.append((quint8)FTMS_RESPONSE_CODE);
@@ -429,9 +435,24 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte
qDebug() << "new requested resistance zwift erg grade " + QString::number(iresistance) + " enabled " + force_resistance;
double resistance = ((double)iresistance * 1.5) / 100.0;
qDebug() << "calculated erg grade " + QString::number(resistance);
if(force_resistance)
if(force_resistance && !erg_mode)
Bike->changeResistance((int8_t)(round(resistance * bikeResistanceGain)) + bikeResistanceOffset + 1); // resistance start from 1
}
else if((char)newValue.at(0) == FTMS_SET_TARGET_POWER) // erg mode
{
qDebug() << "erg mode";
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_SET_TARGET_POWER);
reply.append((quint8)FTMS_SUCCESS);
uint16_t power = (((uint8_t)newValue.at(1)) + (newValue.at(2) << 8));
qDebug() << "power erg " + QString::number(power) + " " + erg_mode;
Bike->changePower(power);
double delta = fabs(Bike->wattsMetric().value() - ((double)power));
qDebug() << "filter " + QString::number(delta) + " " + QString::number(erg_filter);
if(force_resistance && erg_mode && delta > erg_filter)
Bike->changeResistance((int8_t)Bike->resistanceFromPowerRequest(power)); // resistance start from 1
}
else if((char)newValue.at(0) == FTMS_START_RESUME)
{
qDebug() << "start simulation!";
@@ -566,6 +587,7 @@ void virtualbike::reconnect()
}
else
{
service = leController->addService(serviceEchelon);
service = leController->addService(serviceData);
}
@@ -632,6 +654,12 @@ void virtualbike::bikeProvider()
value.append(char(Bike->currentHeart().value())); // Actual value.
value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix
if(!serviceFIT)
{
qDebug() << "serviceFIT not available";
return;
}
QLowEnergyCharacteristic characteristic
= serviceFIT->characteristic((QBluetoothUuid::CharacteristicType)0x2AD2);
Q_ASSERT(characteristic.isValid());
@@ -652,6 +680,12 @@ void virtualbike::bikeProvider()
value.append((char)(Bike->lastCrankEventTime() & 0xff)); // eventtime
value.append((char)(Bike->lastCrankEventTime() >> 8) & 0xFF); // eventtime
if(!service)
{
qDebug() << "service not available";
return;
}
QLowEnergyCharacteristic characteristic
= service->characteristic(QBluetoothUuid::CharacteristicType::CyclingPowerMeasurement);
Q_ASSERT(characteristic.isValid());
@@ -688,6 +722,12 @@ void virtualbike::bikeProvider()
value.append((char)(Bike->lastCrankEventTime() & 0xff)); // eventtime
value.append((char)(Bike->lastCrankEventTime() >> 8) & 0xFF); // eventtime
if(!service)
{
qDebug() << "service not available";
return;
}
QLowEnergyCharacteristic characteristic
= service->characteristic(QBluetoothUuid::CharacteristicType::CSCMeasurement);
Q_ASSERT(characteristic.isValid());
@@ -709,10 +749,10 @@ void virtualbike::bikeProvider()
value.append(0x09);
value.append((char)0x00); // elapsed
value.append((char)0x00); // elapsed
value.append((char)0x00);
value.append((char)0x00);
value.append((char)0x00); // distance
value.append(0x01); // distance
value.append((uint8_t)(((uint32_t)(Bike->odometer() * 100)) >> 24)); // distance
value.append((uint8_t)(((uint32_t)(Bike->odometer() * 100)) >> 16)); // distance
value.append((uint8_t)(((uint32_t)(Bike->odometer() * 100)) >> 8)); // distance
value.append((uint8_t)(Bike->odometer() * 100)); // distance
value.append((char)0x00);
value.append(Bike->currentCadence().value());
value.append((char)0x00);
@@ -724,6 +764,12 @@ void virtualbike::bikeProvider()
}
value.append(sum);
if(!service)
{
qDebug() << "service not available";
return;
}
QLowEnergyCharacteristic characteristic
= service->characteristic(QBluetoothUuid((QString)"0bf669f3-45f2-11e7-9598-0800200c9a66"));
Q_ASSERT(characteristic.isValid());
@@ -758,6 +804,12 @@ void virtualbike::bikeProvider()
if(battery)
{
if(!serviceBattery)
{
qDebug() << "serviceBattery not available";
return;
}
QByteArray valueBattery;
valueBattery.append(100); // Actual value.
QLowEnergyCharacteristic characteristicBattery
@@ -773,6 +825,12 @@ void virtualbike::bikeProvider()
if(!this->noHeartService || heart_only)
{
if(!serviceHR)
{
qDebug() << "serviceHR not available";
return;
}
QByteArray valueHR;
valueHR.append(char(0)); // Flags that specify the format of the value.
valueHR.append(char(Bike->currentHeart().value())); // Actual value.

View File

@@ -32,18 +32,19 @@ public:
bool connected();
private:
QLowEnergyController* leController;
QLowEnergyService* serviceHR;
QLowEnergyService* serviceBattery;
QLowEnergyService* serviceFIT;
QLowEnergyService* service;
QLowEnergyService* serviceChanged;
QLowEnergyController* leController = 0;
QLowEnergyService* serviceHR = 0;
QLowEnergyService* serviceBattery = 0;
QLowEnergyService* serviceFIT = 0;
QLowEnergyService* service = 0;
QLowEnergyService* serviceChanged = 0;
QLowEnergyAdvertisingData advertisingData;
QLowEnergyServiceData serviceDataHR;
QLowEnergyServiceData serviceDataBattery;
QLowEnergyServiceData serviceDataFIT;
QLowEnergyServiceData serviceData;
QLowEnergyServiceData serviceDataChanged;
QLowEnergyServiceData serviceEchelon;
QTimer bikeTimer;
bike* Bike;

View File

@@ -308,6 +308,12 @@ void virtualtreadmill::treadmillProvider()
value.append(treadMill->currentHeart().value()); // current heart rate
if(!service)
{
qDebug() << "service not available";
return;
}
QLowEnergyCharacteristic characteristic
= service->characteristic((QBluetoothUuid::CharacteristicType)0x2ACD); //TreadmillDataCharacteristicUuid
Q_ASSERT(characteristic.isValid());
@@ -324,6 +330,12 @@ void virtualtreadmill::treadmillProvider()
}
else
{
if(!service)
{
qDebug() << "service not available";
return;
}
value.append(0x02); // total distance
uint16_t speed = treadMill->currentSpeed().value() / 3.6;
uint32_t distance = treadMill->odometer() * 1000.0;
@@ -353,6 +365,12 @@ void virtualtreadmill::treadmillProvider()
if(noHeartService == false)
{
if(!serviceHR)
{
qDebug() << "serviceHR not available";
return;
}
QByteArray valueHR;
valueHR.append(char(0)); // Flags that specify the format of the value.
valueHR.append(char(treadMill->currentHeart().value())); // Actual value.

View File

@@ -32,9 +32,9 @@ public:
bool connected();
private:
QLowEnergyController* leController;
QLowEnergyService* service;
QLowEnergyService* serviceHR;
QLowEnergyController* leController = 0;
QLowEnergyService* service = 0;
QLowEnergyService* serviceHR = 0;
QLowEnergyAdvertisingData advertisingData;
QLowEnergyServiceData serviceData;
QLowEnergyServiceData serviceDataHR;