Compare commits

..

287 Commits

Author SHA1 Message Date
Roberto Viola
8a3235c7d3 very first alpha android version
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-23 08:00:04 +01:00
Roberto Viola
a5d823549a info label added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-21 12:26:04 +01:00
Roberto Viola
90970f75c8 plus and minus button handled in QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-21 09:54:18 +01:00
Roberto Viola
a175cc7f45 finally start and stop button work!
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-21 07:11:39 +01:00
Roberto Viola
f15437f84e giving life to QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-20 19:17:02 +01:00
Roberto Viola
5b80303a92 playing with QML models
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-20 17:07:46 +01:00
Roberto Viola
9350e342b2 new icons
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-20 08:57:52 +01:00
Roberto Viola
a0c3576b2e learning QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-19 19:45:16 +01:00
Roberto Viola
c33242f077 start working on QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-19 16:24:54 +01:00
Roberto Viola
65e1a7522c fixed midnight issue
Signed-off-by: Roberto Viola <roberto.viola83@gmail.com>
2020-11-15 08:56:43 +01:00
Roberto Viola
429cb2077a Update main.yml 2020-11-12 09:27:48 +01:00
Roberto Viola
49b47cd432 Update README.md 2020-11-12 09:12:33 +01:00
Roberto Viola
34780cb2b0 Update main.yml 2020-11-12 09:07:49 +01:00
Roberto Viola
f2e8d3e9e8 Update main.yml 2020-11-11 19:36:59 +01:00
Roberto Viola
64e4ee5974 Update main.yml 2020-11-11 19:21:31 +01:00
Roberto Viola
11173726fe Update main.yml
https://github.com/jurplel/install-qt-action/issues/63
2020-11-11 19:12:31 +01:00
Roberto Viola
3ce5f320c7 Update main.yml 2020-11-11 19:02:43 +01:00
Roberto Viola
e7153fd087 Update main.yml 2020-11-11 09:17:26 +01:00
Roberto Viola
a3b929fcc7 Update main.yml 2020-11-11 09:08:51 +01:00
Roberto Viola
7846341fd7 Update main.yml 2020-11-11 08:49:39 +01:00
Roberto Viola
4d2ead7e1b removed heartrate service from default (using FTMS instead). Tested on
android succesfully

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-11 08:31:13 +01:00
Roberto Viola
2e69a155fc Update main.yml 2020-11-10 16:27:11 +01:00
Roberto Viola
821d49e8e6 Update main.yml 2020-11-10 16:18:54 +01:00
Roberto Viola
c1800f65ea Update main.yml 2020-11-10 16:17:47 +01:00
Roberto Viola
9781f5de7d Update main.yml 2020-11-10 16:10:44 +01:00
Roberto Viola
e10d0d1127 Update main.yml 2020-11-10 16:07:40 +01:00
Roberto Viola
32213b6a31 grade received from zwift should be increased by 1 because the
resistance level starts from 1

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 14:54:56 +01:00
Roberto Viola
55047173a2 negative grade handled on virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 14:47:35 +01:00
Roberto Viola
07fe27abf5 Update README.md 2020-11-10 14:38:40 +01:00
Roberto Viola
ae0001b8fa put some boundaries on resistance on domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 14:35:50 +01:00
Roberto Viola
9a342a4724 typo on mainwindow.cpp
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 11:13:20 +01:00
Roberto Viola
b2dcbef3b8 added wait for response to writeCharacteristic to domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 10:31:21 +01:00
Roberto Viola
667a384232 added a wait for response to writeCharacteristic to domyostreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 10:24:59 +01:00
Roberto Viola
e40a170b61 added -test-resistance for alpat59
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 09:30:45 +01:00
Roberto Viola
0d3ecd702d fixed groupTrain clicked signal
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 09:17:42 +01:00
Roberto Viola
3ce4443adb android: log file generated to the download folder
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 09:06:20 +01:00
Roberto Viola
1837c0d120 elapsed time for domyosbike fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-10 07:44:55 +01:00
Roberto Viola
55a5def6a5 Create main.yml 2020-11-09 17:28:37 +01:00
Roberto Viola
96176bd36c Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-11-09 16:45:22 +01:00
Roberto Viola
0e400e967e started working on a Dockerfile 2020-11-09 16:45:11 +01:00
Roberto Viola
20d6ec74d8 Update FUNDING.yml 2020-11-09 16:05:01 +01:00
Roberto Viola
0aa1693dde added a stupid icon to the project
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-09 15:35:07 +01:00
Roberto Viola
fab495d68f alpha icons 2020-11-09 15:09:39 +01:00
Roberto Viola
75882d07b8 Merge branch 'android' 2020-11-09 14:53:53 +01:00
Roberto Viola
279adb0ea2 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-11-09 14:08:05 +01:00
Roberto Viola
fe69cc2c8f handled disconnection error on trxappgateusbtreadmill as did for
https://github.com/cagnulein/qdomyos-zwift/commit/
27f207b022

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-09 14:07:52 +01:00
Roberto Viola
3ca7e6ecb5 Create FUNDING.yml 2020-11-09 12:15:40 +01:00
Roberto Viola
b9e08d3cce Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-11-09 12:09:06 +01:00
Roberto Viola
02cb7003cc fixed speed on trxappgateusbtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-09 12:07:41 +01:00
Roberto Viola
1858fbb7d8 first build on android
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-09 11:48:51 +01:00
Roberto Viola
bf31f00ce7 Update README.md 2020-11-09 11:07:14 +01:00
Roberto Viola
60fa28417c added indicate to FTMS control point
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-09 08:14:04 +01:00
Roberto Viola
b1ce0973e7 added check to invalid resistance to domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-09 07:58:50 +01:00
Roberto Viola
a208b26a39 speed check added again to bike.cpp
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-09 07:54:46 +01:00
Roberto Viola
79f8942b06 /n added to new debug prints
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-08 19:58:27 +01:00
Roberto Viola
6030616209 console debug readded
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-08 19:53:44 +01:00
Roberto Viola
7e846d4228 fixed log file name
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-08 19:49:29 +01:00
Roberto Viola
b43dd6f80d fixed compatibility with old qt version
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-08 19:45:36 +01:00
Roberto Viola
07676e35c6 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-11-08 19:41:06 +01:00
Roberto Viola
53dc952ece logfile improved with qmessagehandler
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-08 19:40:37 +01:00
Roberto Viola
a48a40b8f2 Merge pull request #52 from kenands/master
Update bike.cpp
2020-11-08 06:27:44 +01:00
Roberto Viola
825d51f86b added the path https://github.com/cagnulein/qdomyos-zwift/commit/
27f207b022 to domyosbike
2020-11-08 06:20:47 +01:00
yusuf20
87be3f3837 Update bike.cpp
"Power Table for Wattbike pro" seems to be more near of power provided by kinomap with bike 500
(https://support.wattbike.com/hc/en-us/articles/115001881825-Power-Resistance-and-Cadence-Tables)
With kinomap:
- At resistance level 1 and 80 rpm, power was about 110w 
- At resistance level 15 and high rpm (up to 150) I reached 1200w
2020-11-08 01:54:52 +01:00
Roberto Viola
e3b7089d20 fixed decimal point on odometer on domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 19:15:28 +01:00
Roberto Viola
e9c114d285 trying to set resistance equal to grade
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 19:14:03 +01:00
Roberto Viola
e5412146f7 removed dependencies of speed in the math of watt on bike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 19:13:31 +01:00
Roberto Viola
4fa78ff8b4 added -only-virtualbike and -only-virtualtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 05:51:59 +01:00
Roberto Viola
31c85fdfd5 fixed error "incoming connection request in unexpected state"
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 05:46:29 +01:00
Roberto Viola
d10b0da265 watt debug print added to domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 05:19:13 +01:00
Roberto Viola
468c118ded added REQUEST_CONTROL to virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 05:00:11 +01:00
Roberto Viola
904251e05c fixed odometer on domyosbike console
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 04:55:19 +01:00
Roberto Viola
3dded9c9d3 fixed (again) speed on domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-07 04:51:56 +01:00
Roberto Viola
e48622a831 telink domyosbike init simiplified following the snoop log provided
https://github.com/cagnulein/qdomyos-zwift/issues/
48#issuecomment-723118458

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-06 16:03:37 +01:00
Roberto Viola
88e14ae23b added -no-console to domyostreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-06 13:54:29 +01:00
Roberto Viola
05a442832d changing some fixed data for T900C
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-06 13:49:03 +01:00
Roberto Viola
3b88809614 sent unsupported to zwift in case of unmanaged parameters
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-06 08:15:51 +01:00
Roberto Viola
d5e777fe96 odometer fixed on console of the domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-06 08:08:33 +01:00
Roberto Viola
85578558fb speed corrected on domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-06 08:02:31 +01:00
Roberto Viola
97a63846e7 added calories and correct visualization after 1h:39m:59s of running on
domyostreadmill console

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 17:23:20 +01:00
Roberto Viola
cf7f6ca1c2 domyostreadmill merged 2020-11-05 16:32:47 +01:00
Roberto Viola
55d632a01f added -poll-device-time for domyostreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 16:29:52 +01:00
Roberto Viola
7550022673 domyostreadmill T900C reverted to a non SEGFAULT commit https://
github.com/cagnulein/qdomyos-zwift/issues/13#issuecomment-722383562

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 16:23:06 +01:00
Roberto Viola
70f37dbebb Increased retry on domyos treadmill 2020-11-05 13:24:24 +01:00
Roberto Viola
37c3703283 adding some timeout delay for T900C https://github.com/cagnulein/
qdomyos-zwift/issues/13#issuecomment-722300134

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 12:03:44 +01:00
Roberto Viola
1260e1efc4 trying avoid packets collision on T900C
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 10:19:37 +01:00
Roberto Viola
62a76f5f13 fixed speed and inclination on trxappgateusbtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 09:52:08 +01:00
Roberto Viola
147ca95be1 telink domyosbike init updated: trying to follow the snoop log
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 09:35:59 +01:00
Roberto Viola
53fde43c3c added some better management to FTMS control point to virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 09:16:48 +01:00
Roberto Viola
09defca49b fixed odometer to the console of the domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 08:51:37 +01:00
Roberto Viola
541b150763 controller error debug print fixed on virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 08:50:24 +01:00
Roberto Viola
ac77d22eef added precisetimer to domyostreadmill; handled writeCharacteristic error
on domyostreadmill; added debug print in case of the update timer has
nothing to do on domyostreadmill

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 08:33:44 +01:00
Roberto Viola
aeb2d5d1bd trying to manage strange packets from T900C 2020-11-04 21:06:34 +01:00
Roberto Viola
60b00b978e trying fix odometer and calories on display of domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-04 15:37:15 +01:00
Roberto Viola
e924694fea started working on telink domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-04 14:56:57 +01:00
Roberto Viola
d510e61234 added device class id debug prints
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-04 12:15:29 +01:00
Roberto Viola
7f58393b5b removed the manufacturer print because they are avaiable only on qt 5.12
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-04 12:13:18 +01:00
Roberto Viola
4e7b73ab8d manufacturer id debug prints added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-04 12:06:24 +01:00
Roberto Viola
7b2a81e2f5 flush logs added every time writes a line (in order to keep the log
always updated)

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-04 09:29:10 +01:00
Roberto Viola
0fa8873e19 another lock to domyostreadmill (poll command) 2020-11-04 06:11:38 +01:00
Roberto Viola
eb0dd0c618 uncompleted packet improved on domyostreadmill 2020-11-03 20:22:43 +01:00
Roberto Viola
b7fdbbed90 QLowEnergyController::Error handled in virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-03 16:24:21 +01:00
Roberto Viola
13f341b6a9 added -no-heart-service parameter
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-03 14:24:05 +01:00
Roberto Viola
90523d388c Revert "removed HR service from virtualbike in order to a better compatibility"
This reverts commit bee124bdcf.
2020-11-03 14:15:49 +01:00
Roberto Viola
32b4ba21de Update README.md 2020-11-03 11:45:06 +01:00
Roberto Viola
fbe4571734 fixed packets collision on T900C
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-03 10:03:37 +01:00
Roberto Viola
8a248f7451 trying to get the T900C working
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-03 08:33:55 +01:00
Roberto Viola
bee124bdcf removed HR service from virtualbike in order to a better compatibility
with FTMS external apps
2020-11-02 22:03:23 +01:00
Roberto Viola
d015149365 added some new debug prints to domyostreadmill 2020-11-02 21:58:35 +01:00
Roberto Viola
27f207b022 fixed segmentation fault on domyos treadmill disconnection 2020-11-02 19:19:50 +01:00
Roberto Viola
54c7acf263 added button to enable charts because they are too heavy to run on a
raspi 0w

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-02 16:47:21 +01:00
Roberto Viola
53e25c8592 fixed decostructor on bluetooth.cpp (segfault when the device
disconnects)

Signed-off-by: Roberto Viola <roberto.viola83@gmail.com>
2020-11-02 10:07:42 +01:00
Roberto Viola
24600b0a01 virtualbike migrated to FTMS 2020-11-01 21:45:06 +01:00
Roberto Viola
850ea9144a cadence fix reverted
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-01 19:21:02 +01:00
Roberto Viola
bde4c5f5cc trying to fix cadence instability
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-01 19:02:42 +01:00
Roberto Viola
66f6f6ca97 added -no-write-resistance
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-01 18:18:38 +01:00
Roberto Viola
209da708c9 fixed zwift icon on UI 2020-10-30 14:46:35 +01:00
Roberto Viola
8243c936b5 fixed zwift icon 2020-10-30 10:17:38 +01:00
Roberto Viola
d35b1fa1ca fixed decimal position on distance on the console of the domyostreadmill 2020-10-30 09:41:39 +01:00
Roberto Viola
9a6f4617b6 fixed pace on chart 2020-10-30 09:23:43 +01:00
Roberto Viola
fee67bb812 charts restored 2020-10-30 09:13:45 +01:00
Roberto Viola
895d2c31a0 fixed precision on numbers on UI 2020-10-30 09:09:01 +01:00
Roberto Viola
7d37e333f1 added pace to charts
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-29 15:38:49 +01:00
Roberto Viola
70cf5040ee current pace added to bluetoothdevice and to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-29 15:29:15 +01:00
Roberto Viola
53b2bd4516 elapsed time added to the trxappgateusbtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-29 14:49:14 +01:00
Roberto Viola
d77fba6734 fix maths on trxappgateusbtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-29 12:25:09 +01:00
Roberto Viola
a7dcac02df tested virtualbike class on raspberry3 updated (sudo apt upgrade) 2020-10-29 11:50:45 +01:00
Roberto Viola
bb0a9cde92 Update README.md 2020-10-29 09:44:08 +01:00
Roberto Viola
f279c3689d Update README.md 2020-10-29 09:36:37 +01:00
Roberto Viola
5b36ad9e4f cleaned up chart math
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-29 09:07:50 +01:00
Roberto Viola
d5ddc4cabd temporary charts disabled 2020-10-28 19:31:45 +01:00
Roberto Viola
ea62337b16 adding visual studio project file 2020-10-28 16:38:37 +01:00
Roberto Viola
15f24b8d0c starting build on windows 10
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-28 16:06:16 +01:00
Roberto Viola
dacbb475c8 trying to have a stable connection on trxappgateusbtreadmill reducing
the polling time

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-28 15:07:37 +01:00
Roberto Viola
6f2b19b4aa fixed trxappgateusbtreadmill parse values
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-28 14:48:10 +01:00
Roberto Viola
b71ca62110 uuid chars trxappgateusb fixed 2020-10-28 11:14:16 +01:00
Roberto Viola
24da757c5e added plus and minus buttons to speed, inclination and resistance
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-28 08:39:22 +01:00
Roberto Viola
fe1ec40f6c resistance and cadence added to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-28 08:26:48 +01:00
Roberto Viola
9cbd054ab4 added debug to trxappgateusbtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-28 08:12:53 +01:00
Roberto Viola
62ae1f5c6d fixed uuid on trxappgateusbtreadmill 2020-10-27 22:45:37 +01:00
Roberto Viola
d57f265315 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-10-27 19:02:03 +00:00
Roberto Viola
ce56464046 fixed jitter on elapsed time on domyostreadmill 2020-10-27 19:01:39 +00:00
Roberto Viola
a162e0dcfb Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-10-27 16:31:19 +01:00
Roberto Viola
f0074697d6 fix typo on bluetooth
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-27 16:31:00 +01:00
Roberto Viola
e25b32bba4 Update README.md 2020-10-27 15:10:42 +01:00
Roberto Viola
ddbd41e79f added xcode cli to the dependencies 2020-10-27 14:17:59 +01:00
Roberto Viola
cecd624d13 info added 2020-10-27 10:53:28 +01:00
Roberto Viola
074f5d60f9 realtime-chart.png added 2020-10-27 09:35:15 +01:00
Roberto Viola
8795e5ec3c libqt5charts5-dev and libqt5charts5 added to dependency 2020-10-27 09:32:03 +01:00
Roberto Viola
b23183e308 Merge branch 'charts' 2020-10-27 09:29:17 +01:00
Roberto Viola
663ed67a02 charts seems stable :)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-27 09:26:11 +01:00
Roberto Viola
586ba40527 Delete mainwindow.o 2020-10-26 21:47:42 +01:00
Roberto Viola
f5d85eb1ae Delete bluetooth.o 2020-10-26 21:47:20 +01:00
Roberto Viola
1af0246490 Removed rssi check 2020-10-26 21:46:51 +01:00
Roberto Viola
40336b70c8 fixed build error 2020-10-26 20:28:05 +01:00
Roberto Viola
0460de0fe7 started working on charts
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-26 17:25:04 +01:00
Roberto Viola
61fde67eaf avoid connection to device with RSSI equals to 0
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-26 11:26:54 +01:00
Roberto Viola
e4ee05d2a7 added debug to virtualbike and virtualtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-26 10:01:23 +01:00
Roberto Viola
f49d1e69ff fix warnings
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-26 09:59:57 +01:00
Roberto Viola
4118ed914b fix cadence on virtual bike 2020-10-25 15:00:50 +01:00
Roberto Viola
5564720b56 supported resistance fixed on virtualbike 2020-10-23 20:31:41 +02:00
Roberto Viola
6d88cef84c trxappgateusbtreadmill init updated
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-23 15:41:13 +02:00
Roberto Viola
671c262288 experimental support added to trxappgateusbtreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-23 15:28:57 +02:00
Roberto Viola
d76ec7e32e Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-10-23 09:16:55 +02:00
Roberto Viola
5d3b9b3645 fixed characteristicChanged signal on virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-23 09:16:37 +02:00
Roberto Viola
f3e6fa9d61 fixed casting on distance calculated to domyos treadmill 2020-10-22 19:04:35 +01:00
Roberto Viola
4da9566713 elapsed time on domyostreadmill is now calculate from RTC (better
precision)

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-22 10:27:02 +02:00
Roberto Viola
a43b7b393c added distance calulated to domyostreadmill in order to increase
precision

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-22 10:22:07 +02:00
Roberto Viola
a6c369cb0b resistance level set target added to virtualbike (not tested)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-22 09:07:22 +02:00
Roberto Viola
60a8f7b93f odometer added to domyosbike console
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-22 07:58:20 +02:00
Roberto Viola
9f49d0eee9 TOORX device name added 2020-10-22 05:24:30 +01:00
Roberto Viola
7b4a3d3aca crank flags swapped on virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-21 08:43:15 +02:00
Roberto Viola
6aeb3c475f display speed on domyosbike fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-21 08:16:08 +02:00
Roberto Viola
04c8cb8be5 app_bundle added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-20 15:40:32 +02:00
Roberto Viola
34e69f55ae bundle added to deploy on mac
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-20 12:00:17 +02:00
Roberto Viola
19dcb2e600 speed to the domyosbike's console fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-20 10:28:50 +02:00
Roberto Viola
45d118b90e added cadence to console to domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-20 09:47:51 +02:00
Roberto Viola
384deeda16 bike watt maths fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-20 08:18:59 +02:00
Roberto Viola
dc5290ced9 added filter to bluetooth device 2020-10-19 22:30:32 +02:00
Roberto Viola
900f364cfe swapped bytes in virtualbike 2020-10-19 22:09:16 +02:00
Roberto Viola
fd819219da distance on treadmill fixed 2020-10-19 21:56:19 +02:00
Roberto Viola
91db440047 LastCrankEventTime added to domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-19 14:20:59 +02:00
Roberto Viola
98e5e50017 crank revolutions and watts added to domyosbike (not tested)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-19 12:14:56 +02:00
Roberto Viola
a93408aecd removed fan from domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-19 11:17:57 +02:00
Roberto Viola
20af3107b7 read resistance and cadence from domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-19 11:12:42 +02:00
Roberto Viola
23309060a4 added bike class
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-19 10:34:18 +02:00
Roberto Viola
26a64bce34 bluetoothdevice added to split bikes from treadmills
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-19 10:05:46 +02:00
Roberto Viola
117fce8f7c heartrate service missing on reconnect
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-19 08:00:28 +02:00
Roberto Viola
ebc842060c first implementation of virtualbike 2020-10-18 19:11:31 +02:00
Roberto Viola
d172350a13 add docs 2020-10-18 09:31:50 +02:00
Roberto Viola
8426f4640b virtualbike class added 2020-10-18 09:08:27 +02:00
Roberto Viola
e31c8b6ff9 domyosbike class added 2020-10-18 06:48:20 +01:00
Roberto Viola
bb2909750f Merge branch 'toorx' (not tested) 2020-10-18 06:12:47 +01:00
Roberto Viola
11d560d14b Update README.md 2020-10-17 22:42:52 +02:00
Roberto Viola
e774fbf575 Update README.md 2020-10-17 15:39:12 +02:00
Roberto Viola
4c2d82abf2 mac ui screenshot added 2020-10-17 15:36:26 +02:00
Roberto Viola
b5cd1d5915 distance added to domyos console 2020-10-17 13:44:22 +01:00
Roberto Viola
c5dd48219a virtualtreadmill advertising fixed 2020-10-17 13:03:55 +01:00
Roberto Viola
2320ef4124 virtualtreadmill advertising fixed 2020-10-17 12:55:40 +01:00
Roberto Viola
d3d54b19dc built with Qt 5.15.1 2020-10-16 19:17:06 +01:00
Roberto Viola
30a6008c82 train program rows not cleaned when load a new program
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-16 10:52:45 +02:00
Roberto Viola
0ca04756f3 added tooltip to difficulty slider
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-16 10:45:13 +02:00
Roberto Viola
8a1ee08d50 watts moved from virtualtreadmill to treadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-16 10:41:07 +02:00
Roberto Viola
9fae936586 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-10-15 13:29:38 +01:00
Roberto Viola
63c36bbafc arm32 binary added 2020-10-15 13:29:11 +01:00
Roberto Viola
2a59e75e57 Update README.md 2020-10-15 14:08:16 +02:00
Roberto Viola
15fdaec3c6 binary with debug symbols 2020-10-15 10:26:15 +01:00
Roberto Viola
29a2173e35 Update README.md 2020-10-15 11:21:44 +02:00
Roberto Viola
88dfacb0c3 new binary 2020-10-15 10:01:35 +01:00
Roberto Viola
06cb2d9586 Merge branch 'toorx' of https://github.com/cagnulein/qdomyos-zwift into toorx 2020-10-15 09:59:32 +02:00
Roberto Viola
b28effc4c0 fixed service discovered
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-15 09:59:00 +02:00
Roberto Viola
0ddd953b2e fan icon fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-15 09:25:25 +02:00
Roberto Viola
d5e2e98429 new icons added 2020-10-15 08:38:58 +02:00
Roberto Viola
1e1c977332 example file added 2020-10-15 08:21:13 +02:00
Roberto Viola
df16d94438 trainprogram argument added to the executable
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-15 08:18:06 +02:00
Roberto Viola
9425e751f2 difficulty slider added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-15 08:05:46 +02:00
Roberto Viola
eb8d354111 domyostreadmill: updateDisplay called every seconds. payload to be
tested and filled

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-14 16:48:34 +02:00
Roberto Viola
78c9c86227 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-10-14 15:51:54 +02:00
Roberto Viola
7cc40ffe30 dark theme added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-14 15:51:33 +02:00
Roberto Viola
9e6a5ca4af updated dependencies 2020-10-14 14:44:33 +02:00
Roberto Viola
37ec561409 Merge pull request #23 from cagnulein/gpx_import
Gpx import
2020-10-14 14:40:53 +02:00
Roberto Viola
ba8df2c2eb Update README.md 2020-10-14 09:26:42 +02:00
Roberto Viola
4b86f77a22 gpx imported correctly!
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-14 09:24:43 +02:00
Roberto Viola
e43276e52b gpx opened and analyzed. i have to put in the train program table, easy
step i guess

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-14 09:03:21 +02:00
Roberto Viola
33fdd4c979 Update README.md 2020-10-13 16:10:41 +02:00
Roberto Viola
87f8887ef3 added binary for testing purpose 2020-10-13 14:30:18 +01:00
Roberto Viola
ed7e4c6bf2 toorx attributes read (test needed)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-13 12:25:54 +02:00
Roberto Viola
12992df557 fixed build issue
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-13 12:03:13 +02:00
Roberto Viola
8d42d530cc toorx connection (need test)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-13 11:53:27 +02:00
Roberto Viola
7b9bd00ff4 bluetooth class added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-13 10:50:58 +02:00
Roberto Viola
5e7a8d938b fan speed supported
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-13 08:38:50 +02:00
cagnulein
a0c1e1b645 Merge pull request #21 from cagnulein/ui
Ui and much more!
2020-10-13 08:00:12 +02:00
cagnulein
aa53956a35 Merge branch 'master' into ui 2020-10-13 07:59:56 +02:00
Roberto Viola
cd1c10a090 -no-log added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 16:06:07 +02:00
Roberto Viola
c21e337bdd train program example added 2020-10-12 15:32:49 +02:00
Roberto Viola
1d23ac4b81 fix typo
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 15:13:42 +02:00
Roberto Viola
bbeaa5ec95 calories added to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 15:12:34 +02:00
Roberto Viola
9f6a4de4ac ui screenshot added 2020-10-12 15:06:20 +02:00
Roberto Viola
ee1c3e0118 connectivity icon on UI added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 15:00:02 +02:00
Roberto Viola
81ac8909c8 elevationGain added to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 12:23:27 +02:00
Roberto Viola
3a45935617 UI layout fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 12:12:20 +02:00
Roberto Viola
c89c381177 odometer added on UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 12:09:40 +02:00
Roberto Viola
ea57069f33 train program total distance added to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 11:51:05 +02:00
Roberto Viola
bf40c460a5 added currentRowElapsedTime, totalElapsedTime and Duration of the train
program on UI

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 11:39:58 +02:00
Roberto Viola
338b19f664 added the ability to enable/disable the train program; start button on
the console starts the program also is valid and enabled

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 11:03:42 +02:00
Roberto Viola
9f9000427f auto start tape on connect disabled
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 10:44:56 +02:00
Roberto Viola
92cd9baea3 updated installation from source steps 2020-10-12 10:25:57 +02:00
Roberto Viola
e21ad70ea9 getting kcal and distance from the treadmill (tests need)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 08:55:26 +02:00
cagnulein
d7ac459a3d fan speed buttons managed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 08:24:19 +02:00
Roberto Viola
d58db4100f fix typo on requestSpeed check 2020-10-11 16:15:08 +02:00
Roberto Viola
3c7cb254e6 fixed changing speed and inclination at the same time 2020-10-11 15:59:40 +02:00
Roberto Viola
259b53e8e0 change speed and inclination only if needed 2020-10-11 15:48:56 +02:00
Roberto Viola
b73092bd8f fix treadmill slots 2020-10-11 15:28:02 +02:00
Roberto Viola
787b9aa2c2 trainprogram debug lines added 2020-10-11 14:44:41 +02:00
Roberto Viola
73a0bd7c65 fix seg fault without a train program 2020-10-11 14:39:06 +02:00
Roberto Viola
d068526e55 save program xml issue fixed 2020-10-11 14:27:56 +02:00
Roberto Viola
8945063f30 support for future arguments to the executable 2020-10-11 14:27:35 +02:00
Roberto Viola
4625bccad3 no bluetooth dongle support added 2020-10-11 14:26:56 +02:00
Roberto Viola
5c723375d7 trainprogram start added 2020-10-11 10:34:59 +02:00
Roberto Viola
1212bc83f8 restart bt connection to domyos treadmill 2020-10-11 09:45:17 +02:00
Roberto Viola
efc9788c89 fixed build issues 2020-10-10 13:47:28 +02:00
Roberto Viola
42c43158e6 Merge remote-tracking branch 'origin/startup_sequence_fix' into ui 2020-10-10 13:27:53 +02:00
Roberto Viola
ff354fd20d speed and inclination writing finally works! 2020-10-10 09:39:04 +02:00
Roberto Viola
d21f92727e domyos init outside btle events 2020-10-09 16:39:37 +02:00
Roberto Viola
632991e58e bluez logging added 2020-10-09 14:39:34 +02:00
cagnulein
1d6c46a32b Merge branch 'cleaning_code' into ui 2020-10-09 11:25:35 +02:00
Roberto Viola
3ba0219ce4 added watt visualization 2020-10-09 10:36:27 +02:00
Roberto Viola
21ec2a890e cleaned useless comments 2020-10-09 10:19:19 +02:00
Roberto Viola
b434d1f1e6 fix typo 2020-10-09 09:48:18 +02:00
Roberto Viola
373eb3fbf9 virtualtreadmill created when the services are fully discovered 2020-10-09 09:34:38 +02:00
Roberto Viola
acfdff7b5c added timestamp to log filename 2020-10-09 08:06:32 +02:00
Roberto Viola
38a41451f3 descriptorWritten added 2020-10-09 08:05:46 +02:00
cagnulein
c2c5b7746f treadmill class created deleting all the external variables 2020-10-08 17:21:16 +02:00
cagnulein
4944f6d48d scheduler added 2020-10-08 16:35:36 +02:00
cagnulein
33a478b1ae load, save and reset added and tested 2020-10-08 15:19:38 +02:00
cagnulein
99fd62c8ec converted mutex to eventloop 2020-10-08 14:21:53 +02:00
cagnulein
4712d5780a adding ability to save and load training program (not finished yet) 2020-10-08 14:19:09 +02:00
cagnulein
eff5d1d2f3 elapsed timer should count only when tape is moving 2020-10-08 10:31:05 +02:00
cagnulein
22b6de14f1 characteristicWritten and error added with mutex 2020-10-08 08:56:00 +02:00
cagnulein
76d3139d79 added spaces on debug log 2020-10-08 08:16:21 +02:00
cagnulein
8fb8ed6f44 Merge pull request #14 from cagnulein/startup_sequence_fix
trying to fix startup sequence
2020-10-08 08:13:35 +02:00
Roberto Viola
483cc45643 domyos controller error managed and fixed the startup sequence of the
virtual treadmill
2020-10-07 17:30:39 +02:00
cagnulein
3d152b903a updated startup sequence via Intense Run and Kinomap. Splitted writing
inclination and speed
2020-10-07 17:30:38 +02:00
Cagnulein
1974dd26ee put all domyostreadmill debug string to file and to screen 2020-10-07 17:29:05 +02:00
cagnulein
78f81261d9 Merge pull request #11 from cagnulein/debug_log_bt_comms
log bluetooth communications added
2020-10-06 09:33:19 +02:00
cagnulein
d542aa819b log bluetooth communications added 2020-10-06 09:31:58 +02:00
cagnulein
5774e725a7 started adding a gui 2020-10-02 16:38:07 +02:00
cagnulein
cddd74e0d2 start button managed (untested) 2020-10-02 10:44:52 +02:00
97 changed files with 51391 additions and 258 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: cagnulein
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://www.buymeacoffee.com/cagnulein'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

103
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Cache Qt Linux Desktop
id: cache-qt-linux-desktop
uses: actions/cache@v1
with:
path: '${{ github.workspace }}/output/linux-desktop/'
key: ${{ runner.os }}-QtCache-Linux-Desktop
- name: Cache Qt Linux Android
id: cache-qt-android
uses: actions/cache@v1
with:
path: '${{ github.workspace }}/output/android/'
key: ${{ runner.os }}-QtCache-Android
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Runs a set of commands using the runners shell
- name: Install Qt Linux Desktop
uses: jurplel/install-qt-action@v2
with:
version: '5.12.9'
host: 'linux'
target: 'desktop'
modules: 'qtcharts debug_info'
dir: '${{ github.workspace }}/output/linux-desktop/'
cached: ${{ steps.cache-qt-linux-desktop.outputs.cache-hit }}
- name: Compile Linux Desktop
run: cd src; qmake; make -j4
- name: Archive linux-desktop binary
uses: actions/upload-artifact@v2
with:
name: linux-desktop-binary
path: src/qdomyos-zwift
- uses: actions/checkout@v2
with:
repository: nttld/setup-ndk
path: setup-ndk
# The packages.json in nttld/setup-ndk has already been updated,
# https://github.com/nttld/setup-ndk/commit/831db5b02a0f0cab80614619efe461a3dcc140e6
# but `dist/*` has not been rebuilt yet. Build it.
# https://github.com/nttld/setup-ndk/tree/main/dist
- name: Locally rebuilt setup-ndk
run: |
npm -prefix ./setup-ndk install
npm -prefix ./setup-ndk run all
# Install using locally rebuilt setup-ndk
- name: Setup Android NDK r21d
uses: ./setup-ndk
#- uses: nttld/setup-ndk@v1
with:
ndk-version: r21d
# waiting github.com/jurplel/install-qt-action/issues/63
- name: Install Qt Android
uses: jurplel/install-qt-action@v2
with:
version: '5.12.9'
host: 'linux'
target: 'android'
arch: 'android_armv7'
modules: 'qtcharts debug_info'
dir: '${{ github.workspace }}/output/android/'
cached: ${{ steps.cache-qt-android.outputs.cache-hit }}
- name: Compile Android
run: cd src; qmake; make -j4
# - name: Install Qt MacOS
# uses: jurplel/install-qt-action@v2
# with:
# version: '5.12.9'
# host: 'mac'
# target: 'desktop'
# modules: 'qtcharts debug_info'
# dir: '${{ github.workspace }}/output/macos/'
# - name: Compile MacOS
# run: cd src; qmake; make -j4

View File

@@ -1,25 +1,85 @@
# qdomyos-zwift
Zwift bridge for Domyos treadmills
Zwift bridge for Treadmills and Bike!
<a href="https://www.buymeacoffee.com/cagnulein" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
![UI](docs/treadmill-bridge-schema.png)
![UI](docs/ui.png)
![UI](docs/realtime-chart.png)
UI on Linux
![UI](docs/ui-mac.png)
UI on MacOS
### Features
1. Domyos compatible
2. Toorx TRX Route Key comaptible
3. Zwift compatible
4. Create, load and save train programs
5. Measure distance, elevation gain and watts
6. Gpx import (with difficulty slider)
7. Realtime Charts
![First Success](docs/first_success.jpg)
### Installation
### Installation from source
$ 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
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ sudo apt upgrade && sudo apt update # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ cd src
$ sudo apt install libqt5bluetooth5
$ qmake
$ sudo hciconfig hci0 leadv 0
$ 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
https://developer.apple.com/download/more/?=xcode
Download and install http://download.qt.io/official_releases/qt/5.12/5.12.9/qt-opensource-mac-x64-5.12.9.dmg and simply run the qdomyos-zwift relase for MacOs
### Tested on
Raspberry PI 0W and Domyos Intense Run
- Raspberry PI 0W and Domyos Intense Run
- MacBook Air 2011 and Domyos Intense Run
- Raspberry 3b+ and Domyos T900C
- Raspberry 3b+ and Toorx TRX Route Key
### 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. on your phone you should have a file called btsnoop_hci.log
10. attach the log file in a new issue with a short description of the steps you did in the app when you used it
### No gui version
run as
$ sudo ./qdomyos-zwift -no-gui
### Reference

10
docker/linux/Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM debian:stable
MAINTAINER cagnulein
ENV MAKEFLAGS -j8
WORKDIR /usr/local/src
# utils
RUN apt -y update
RUN apt -y upgrade
RUN apt -y install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default

BIN
docs/CPP_v1.1.pdf Normal file

Binary file not shown.

BIN
docs/CPS_v1.1.pdf Normal file

Binary file not shown.

BIN
docs/CSCP_SPEC_V10.pdf Normal file

Binary file not shown.

BIN
docs/CSCS_SPEC_V10.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?><!-- <?xml-stylesheet type="text/xsl" href="FieldBasedDisplay.xslt"?> --><!--Copyright 2016 Bluetooth SIG, Inc. All rights reserved.-->
<Characteristic xsi:noNamespaceSchemaLocation="http://schemas.bluetooth.org/Documents/characteristic.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Cycling Power Control Point"
type="org.bluetooth.characteristic.cycling_power_control_point" uuid="2A66" last-modified="2016-05-03"
approved="Yes">
<InformativeText>
<Summary>The Cycling Power Control Point characteristic is used to request a specific function to be executed
on the receiving device.
</Summary>
</InformativeText>
<Value>
<Field name="Op Codes">
<Requirement>Mandatory</Requirement>
<Format>uint8</Format>
<Enumerations>
<Enumeration key="1" value="Set Cumulative Value"
description="Initiate the procedure to set a cumulative value. The new value is sent as parameter following op code (parameter defined per service). The response to this control point is Op Code 0x20 followed by the appropriate Response Value."/>
<Enumeration key="2" value="Update Sensor Location"
description="Update to the location of the Sensor with the value sent as parameter to this op code. The response to this control point is Op Code 0x20 followed by the appropriate Response Value."/>
<Enumeration key="3" value="Request Supported Sensor Locations"
description="Request a list of supported locations where the Sensor can be attached. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including a list of supported Sensor locations in the Response Parameter."/>
<Enumeration key="4" value="Set Crank Length"
description="Initiate the procedure to set the crank length value to Sensor. The new value is sent as a parameter with preceding Op Code 0x04 operand. The response to this control point is Op Code 0x20 followed by the appropriate Response Value."/>
<Enumeration key="5" value="Request Crank Length"
description="Request the current crank length value set in the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the Crank Length in the Response Parameter."/>
<Enumeration key="6" value="Set Chain Length"
description="Initiate the procedure to set the chain length value to Sensor. The new value is sent as a parameter with preceding Op Code 0x06 operand. The response to this control point is Op Code 0x20 followed by the appropriate Response Value."/>
<Enumeration key="7" value="Request Chain Length"
description="Request the current chain length value set in the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the chain length in the Response Parameter."/>
<Enumeration key="8" value="Set Chain Weight"
description="Initiate the procedure to set the chain weight value to Sensor. The new value is sent as a parameter with preceding Op Code 0x08 operand. The response to this control point is Op Code 0x20 followed by the appropriate Response Value."/>
<Enumeration key="9" value="Request Chain Weight"
description="Request the current chain weight value set in the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the chain weight in the Response Parameter."/>
<Enumeration key="10" value="Set Span Length"
description="Initiate the procedure to set the span length value to Sensor. The new value is sent as a parameter with preceding Op Code 0x0A operand. The response to this control point is Op Code 0x20 followed by the appropriate Response Value."/>
<Enumeration key="11" value="Request Span Length"
description="Request the current span length value set in the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the span length in the Response Parameter."/>
<Enumeration key="12" value="Start Offset Compensation"
description="Starts the offset compensation process of the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the raw force or a raw torque in the Response Parameter (defined per Service)."/>
<Enumeration key="13" value="Mask Cycling Power Measurement Characteristic Content"
description="Initiate the procedure to set the content of Cycling Power Measurement Characteristic. The response to this control point is Op Code 0x20 followed by the appropriate Response Value."/>
<Enumeration key="14" value="Request Sampling Rate"
description="Request the sampling rate value set in the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the sampling rate in the Response Parameter."/>
<Enumeration key="15" value="Request Factory Calibration Date"
description="Request the Factory calibration date set in the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the Factory calibration date in the Response Parameter."/>
<Enumeration key="16" value="Start Enhanced Offset Compensation"
description="Starts the offset compensation process of the Sensor. The response to this control point is Op Code 0x20 followed by the appropriate Response Value, including the value of the raw force or a raw torque in the Response Parameter and an option for a manufacturer specific value (defined per Service)."/>
<Enumeration key="32" value="Response Code"
description="The Response Code is followed by the Request Op Code, the Response Value and optionally, the Response Parameter."/>
<ReservedForFutureUse start="0" end="0"/>
<ReservedForFutureUse start="17" end="31"/>
<ReservedForFutureUse start="33" end="255"/>
</Enumerations>
</Field>
<Field name="Parameter Value"><!--<InformativeText>Parameter Value for "Set Cumulative Value" Op Code</InformativeText>-->
<Requirement>Optional</Requirement>
<Format>variable</Format>
<Description>Defined per Service specification.</Description>
</Field>
<Field name="Request Op Code">
<InformativeText>The Request Op Code is a sub field of the Parameter Value for "Response Code" Op Code.
<br>
C1: This Field is Mandatory for "Response Code" Op Code, otherwise this field is Excluded.
</br>
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint8</Format>
<Description>Refer to the Op Code table above for additional information on the possible values for this
field.
</Description>
</Field>
<Field name="Response Value">
<InformativeText>The Response Value is a sub field of the Parameter Value for "Response Code" Op Code
<br>
C1: This Field is Mandatory for "Response Code" Op Code, otherwise this field is Excluded.
</br>
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint8</Format>
<Enumerations>
<Enumeration key="1" value="Success" description="Response for successful operation. "/>
<Enumeration key="2" value="Op Code not Supported"
description="Response if unsupported Op Code is received."/>
<Enumeration key="3" value="Invalid Parameter"
description="Response if Parameter received does not meet the requirements of the service or is outside of the supported range of the Sensor."/>
<Enumeration key="4" value="Operation Failed"
description="Response if the requested procedure failed."/>
<ReservedForFutureUse start="0" end="0"/>
<ReservedForFutureUse start="5" end="255"/>
</Enumerations>
</Field>
<Field name="Response Parameter">
<InformativeText>The Response Parameter is a sub field of the Parameter Value for "Response Code" Op Code.
<br>
C2:This Field is Optional for "Response Code" Op Code, otherwise this field is Excluded.
</br>
</InformativeText>
<Requirement>C2</Requirement>
<Format>variable</Format>
<Description>Note: The Response Parameter Value of the response to the Control Point is a variable length
field to allow a list of different values defined by the Service Specification
</Description>
</Field>
</Value>
<Note>
The fields in the above table, reading from top to bottom, are shown in the order of LSO to MSO, where LSO =
Least Significant Octet and MSO = Most Significant Octet.
The Least Significant Octet represents the eight bits numbered 0 to 7.
</Note>
</Characteristic>

View File

@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="utf-8"?><!-- <?xml-stylesheet type="text/xsl" href="FieldBasedDisplay.xslt"?> --><!--Copyright 2016 Bluetooth SIG, Inc. All rights reserved.-->
<Characteristic xsi:noNamespaceSchemaLocation="http://schemas.bluetooth.org/Documents/characteristic.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Cycling Power Feature"
type="org.bluetooth.characteristic.cycling_power_feature" uuid="2A65" last-modified="2016-05-03"
approved="Yes">
<InformativeText>
<Summary>The CP Feature characteristic is used to report a list of features supported by the device.</Summary>
</InformativeText>
<Value>
<Field name="Cycling Power Feature">
<Requirement>Mandatory</Requirement>
<Format>32bit</Format>
<BitField>
<Bit index="0" size="1" name="Pedal Power Balance Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="1" size="1" name="Accumulated Torque Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="2" size="1" name="Wheel Revolution Data Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="3" size="1" name="Crank Revolution Data Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="4" size="1" name="Extreme Magnitudes Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="5" size="1" name="Extreme Angles Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="6" size="1" name="Top and Bottom Dead Spot Angles Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="7" size="1" name="Accumulated Energy Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="8" size="1" name="Offset Compensation Indicator Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="9" size="1" name="Offset Compensation Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="10" size="1" name="Cycling Power Measurement Characteristic Content Masking Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="11" size="1" name="Multiple Sensor Locations Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="12" size="1" name="Crank Length Adjustment Supported ">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="13" size="1" name="Chain Length Adjustment Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="14" size="1" name="Chain Weight Adjustment Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="15" size="1" name="Span Length Adjustment Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="16" size="1" name="Sensor Measurement Context">
<Enumerations>
<Enumeration key="0" value="Force based"/>
<Enumeration key="1" value="Torque based"/>
</Enumerations>
</Bit>
<Bit index="17" size="1" name="Instantaneous Measurement Direction Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="18" size="1" name="Factory Calibration Date Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="19" size="1" name="Enhanced Offset Compensation Supported">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="20" size="2" name="Distribute System Support">
<Enumerations>
<Enumeration key="0" value="Unspecified (legacy sensor)"/>
<Enumeration key="1" value="Not for use in a distributed system"/>
<Enumeration key="2" value="Can be used in a distributed system"/>
<Enumeration key="3" value="RFU"/>
</Enumerations>
</Bit>
<ReservedForFutureUse index="22" size="10"/>
</BitField>
</Field>
</Value>
<Note>
The fields in the above table, reading from top to bottom, are shown in the order of LSO to MSO, where LSO =
Least Significant Octet and MSO = Most Significant Octet.
The Least Significant Octet represents the eight bits numbered 0 to 7.
</Note>
</Characteristic>

View File

@@ -0,0 +1,304 @@
<?xml version="1.0" encoding="utf-8"?><!--Copyright 2011 Bluetooth SIG, Inc. All rights reserved.-->
<Characteristic xsi:noNamespaceSchemaLocation="http://schemas.bluetooth.org/Documents/characteristic.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Cycling Power Measurement"
type="org.bluetooth.characteristic.cycling_power_measurement" uuid="2A63" last-modified="2014-07-02"
approved="Yes">
<InformativeText>
<Summary>The Cycling Power Measurement characteristic is a variable length structure containing a Flags field,
an Instantaneous Power field and, based on the contents of the Flags field, may contain one or more
additional fields as shown in the table below.
</Summary>
</InformativeText>
<Value>
<Field name="Flags">
<Requirement>Mandatory</Requirement>
<Format>16bit</Format>
<BitField>
<Bit index="0" size="1" name="Pedal Power Balance Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="1" size="1" name="Pedal Power Balance Reference">
<Enumerations>
<Enumeration key="0" value="Unknown"/>
<Enumeration key="1" value="Left"/>
</Enumerations>
</Bit>
<Bit index="2" size="1" name="Accumulated Torque Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="3" size="1" name="Accumulated Torque Source">
<Enumerations>
<Enumeration key="0" value="Wheel Based"/>
<Enumeration key="1" value="Crank Based"/>
</Enumerations>
</Bit>
<Bit index="4" size="1" name="Wheel Revolution Data Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="5" size="1" name="Crank Revolution Data Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="6" size="1" name="Extreme Force Magnitudes Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="7" size="1" name="Extreme Torque Magnitudes Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="8" size="1" name="Extreme Angles Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="9" size="1" name="Top Dead Spot Angle Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="10" size="1" name="Bottom Dead Spot Angle Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="11" size="1" name="Accumulated Energy Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="12" size="1" name="Offset Compensation Indicator ">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<ReservedForFutureUse index="13" size="3"/>
</BitField>
<b>C1:These Fields are dependent upon the Flags field</b>
<p></p>
</Field>
<Field name="Instantaneous Power">
<InformativeText>
Unit is in watts with a resolution of 1.
</InformativeText>
<Requirement>Mandatory</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.power.watt</Unit>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Pedal Power Balance">
<InformativeText>
Unit is in percentage with a resolution of 1/2.
</InformativeText>
<Requirement>Optional</Requirement>
<Format>uint8</Format>
<Unit>org.bluetooth.unit.percentage</Unit>
<BinaryExponent>-1</BinaryExponent>
</Field>
<Field name="Accumulated Torque">
<InformativeText>
Unit is in newton metres with a resolution of 1/32.
</InformativeText>
<Requirement>Optional</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.moment_of_force.newton_metre</Unit>
<BinaryExponent>-5</BinaryExponent>
</Field>
<Field name="Wheel Revolution Data - Cumulative Wheel Revolutions">
<InformativeText>
Unitless
<br>C1:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint32</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Wheel Revolution Data - Last Wheel Event Time">
<InformativeText>
Unit is in seconds with a resolution of 1/2048.
<br>C1:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.time.second</Unit>
<BinaryExponent>-11</BinaryExponent>
</Field>
<Field name="Crank Revolution Data- Cumulative Crank Revolutions">
<InformativeText>
Unitless
<br>C2:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C2</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Crank Revolution Data- Last Crank Event Time">
<InformativeText>
Unit is in seconds with a resolution of 1/1024.
<br>C2:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C2</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.time.second</Unit>
<BinaryExponent>-10</BinaryExponent>
</Field>
<Field name="Extreme Force Magnitudes - Maximum Force Magnitude">
<InformativeText>
Unit is in newtons with a resolution of 1.
<br>C3:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C3</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.force.newton</Unit>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Extreme Force Magnitudes - Minimum Force Magnitude">
<InformativeText>
Unit is in newtons with a resolution of 1.
<br>C3:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C3</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.force.newton</Unit>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Extreme Torque Magnitudes- Maximum Torque Magnitude">
<InformativeText>
Unit is in newton metres with a resolution of 1/32.
<br>C4:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C4</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.moment_of_force.newton_metre</Unit>
<BinaryExponent>-5</BinaryExponent>
</Field>
<Field name="Extreme Torque Magnitudes- Minimum Torque Magnitude">
<InformativeText>
Unit is in newton metres with a resolution of 1/32.
<br>C4:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C4</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.moment_of_force.newton_metre</Unit>
<BinaryExponent>-5</BinaryExponent>
</Field>
<Field name="Extreme Angles - Maximum Angle">
<InformativeText>
Unit is in degrees with a resolution of 1
<br>C5: When present, this field and the "Extreme Angles - Minimum Angle" field are always present as a
pair and are concatenated into a UINT24 value (3 octets). As an example, if the Maximum Angle is
0xABC and the Minimum Angle is 0x123, the transmitted value is 0x123ABC.
</br>
</InformativeText>
<Requirement>C5</Requirement>
<Format>uint12</Format>
<Unit>org.bluetooth.unit.plane_angle.degree
</Unit><!-- 2014-07-02 - Added the Description tag to show the informational text per request from SF WG -->
<Description>When observed with the front wheel to the right of the pedals, a value of 0 degrees represents
the angle when the crank is in the 12 o'clock position and a value of 90 degrees
represents the angle, measured clockwise, when the crank points towards the front wheel in a 3 o'clock
position. The left crank sensor (if fitted) detects the 0? when the crank it
is attached to is in the 12 o'clock position and the right sensor (if fitted) detects the 0? when the
crank it is attached to is in its 12 o'clock position; thus, there is a constant
180? difference between the right crank and the left crank position signals.
</Description>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Extreme Angles - Minimum Angle">
<InformativeText>
Unit is in degrees with a resolution of 1.
<br>C5: When present, this field and the "Extreme Angles - Maximum Angle" field are always present as a
pair and are concatenated into a UINT24 value (3 octets). As an example, if the Maximum Angle is
0xABC and the Minimum Angle is 0x123, the transmitted value is 0x123ABC.
</br>
</InformativeText>
<Requirement>C5</Requirement>
<Format>uint12</Format>
<Unit>org.bluetooth.unit.plane_angle.degree
</Unit><!-- 2014-07-02 - Added the Description tag to show the informational text per request from SF WG -->
<Description>When observed with the front wheel to the right of the pedals, a value of 0 degrees represents
the angle when the crank is in the 12 o'clock position and a value of 90 degrees
represents the angle, measured clockwise, when the crank points towards the front wheel in a 3 o'clock
position. The left crank sensor (if fitted) detects the 0? when the crank it
is attached to is in the 12 o'clock position and the right sensor (if fitted) detects the 0? when the
crank it is attached to is in its 12 o'clock position; thus, there is a constant
180? difference between the right crank and the left crank position signals.
</Description>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Top Dead Spot Angle">
<InformativeText>
Unit is in degrees with a resolution of 1.
</InformativeText>
<Requirement>Optional</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.plane_angle.degree
</Unit><!-- 2014-07-02 - Added the Description tag to show the informational text per request from SF WG -->
<Description>When observed with the front wheel to the right of the pedals, a value of 0 degrees represents
the angle when the crank is in the 12 o'clock position and a value of 90 degrees
represents the angle, measured clockwise, when the crank points towards the front wheel in a 3 o'clock
position. The left crank sensor (if fitted) detects the 0? when the crank it
is attached to is in the 12 o'clock position and the right sensor (if fitted) detects the 0? when the
crank it is attached to is in its 12 o'clock position; thus, there is a constant
180? difference between the right crank and the left crank position signals.
</Description>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Bottom Dead Spot Angle">
<InformativeText>
Unit is in degrees with a resolution of 1.
</InformativeText>
<Requirement>Optional</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.plane_angle.degree
</Unit><!-- 2014-07-02 - Added the Description tag to show the informational text per request from SF WG -->
<Description>When observed with the front wheel to the right of the pedals, a value of 0 degrees represents
the angle when the crank is in the 12 o'clock position and a value of 90 degrees
represents the angle, measured clockwise, when the crank points towards the front wheel in a 3 o'clock
position. The left crank sensor (if fitted) detects the 0? when the crank it
is attached to is in the 12 o'clock position and the right sensor (if fitted) detects the 0? when the
crank it is attached to is in its 12 o'clock position; thus, there is a constant
180? difference between the right crank and the left crank position signals.
</Description>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Accumulated Energy">
<InformativeText>
Unit is in kilojoules with a resolution of 1.
</InformativeText>
<Requirement>Optional</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.energy.joule</Unit>
<DecimalExponent>3</DecimalExponent>
</Field>
</Value>
<Note>
<p>
The fields in the above table, reading from top to bottom, are shown in the order of LSO to MSO, where LSO
= Least Significant Octet and MSO = Most Significant Octet.
The Least Significant Octet represents the eight bits numbered 0 to 7.
</p>
</Note>
</Characteristic>

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?><!--Copyright 2011 Bluetooth SIG, Inc. All rights reserved.-->
<Characteristic xsi:noNamespaceSchemaLocation="http://schemas.bluetooth.org/Documents/characteristic.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Cycling Power Vector"
type="org.bluetooth.characteristic.cycling_power_vector" uuid="2A64" last-modified="2014-07-02"
approved="Yes">
<InformativeText>
<Summary>The Cycling Power Vector characteristic is a variable length structure containing a Flags fieldand
based on the contents of the Flags field, may contain one or more additional fields as shown in the table
below.
</Summary>
</InformativeText>
<Value>
<Field name="Flags">
<Requirement>Mandatory</Requirement>
<Format>8bit</Format>
<BitField>
<Bit index="0" size="1" name="Crank Revolution Data Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="1" size="1" name="First Crank Measurement Angle Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="2" size="1" name="Instantaneous Force Magnitude Array Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="3" size="1" name="Instantaneous Torque Magnitude Array Present">
<Enumerations>
<Enumeration key="0" value="False"/>
<Enumeration key="1" value="True"/>
</Enumerations>
</Bit>
<Bit index="4" size="2" name="Instantaneous Measurement Direction">
<Enumerations>
<Enumeration key="0" value="Unknown"/>
<Enumeration key="1" value="Tangential Component"/>
<Enumeration key="2" value="Radial Component"/>
<Enumeration key="3" value="Lateral Component"/>
</Enumerations>
</Bit>
<ReservedForFutureUse index="6" size="2"/>
</BitField>
<br>C1:These Fields are dependent upon the Flags field</br>
<p></p>
</Field>
<Field name="Crank Revolution Data - Cumulative Crank Revolutions">
<InformativeText>
Unitless
<br>C1:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Crank Revolution Data - Last Crank Event Time">
<InformativeText>
Unit is in seconds with a resolution of 1/1024.
<br>C1:When present, these fields are always present as a pair.</br>
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.time.second</Unit>
<BinaryExponent>-10</BinaryExponent>
</Field>
<Field name="First Crank Measurement Angle ">
<InformativeText>
Unit is in degrees with a resolution of 1.
</InformativeText>
<Requirement>Optional</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.plane_angle.degree
</Unit><!-- 2014-07-02 - Added the Description tag to show the informational text per request from SF WG -->
<Description>When observed with the front wheel to the right of the pedals, a value of 0 degrees represents
the angle when the crank is in the 12 o'clock position and a value of 90 degrees
represents the angle, measured clockwise, when the crank points towards the front wheel in a 3 o'clock
position. The left crank sensor (if fitted) detects the 0? when the crank it
is attached to is in the 12 o'clock position and the right sensor (if fitted) detects the 0? when the
crank it is attached to is in its 12 o'clock position; thus, there is a constant
180? difference between the right crank and the left crank position signals.
</Description>
<DecimalExponent>0</DecimalExponent>
</Field>
<Field name="Instantaneous Force Magnitude Array">
<InformativeText>
The unit is in newtons with a resolution of 1
<br>Array Order - Older is towards the LSO and Newer is towards the MSO</br>
<br>C2: These fields are mutually exclusive. When this field is present, the presence of the
Instantaneous Torque Magnitude Array is excluded.
</br>
</InformativeText>
<Requirement>C2</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.force.newton</Unit>
<DecimalExponent>0</DecimalExponent>
<Repeated>1</Repeated>
</Field>
<Field name="Instantaneous Torque Magnitude Array">
<InformativeText>
Unit is in newton/meter with a resolution of 1/32
<br>Array Order - Older is towards the LSO and Newer is towards the MSO</br>
<br>C2: These fields are mutually exclusive. When this field is present, the presence of the
Instantaneous Force Magnitude Array is excluded.
</br>
</InformativeText>
<Requirement>C2</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.moment_of_force.newton_metre</Unit>
<BinaryExponent>-5</BinaryExponent>
<Repeated>1</Repeated>
</Field>
</Value>
<Note>
<p>
The fields in the above table, reading from top to bottom, are shown in the order of LSO to MSO, where LSO
= Least Significant Octet and MSO = Most Significant Octet.
The Least Significant Octet represents the eight bits numbered 0 to 7.
</p>
</Note>
</Characteristic>

View File

@@ -0,0 +1,219 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Copyright 2017 Bluetooth SIG, Inc. All rights reserved.-->
<Characteristic xsi:noNamespaceSchemaLocation="http://schemas.bluetooth.org/Documents/characteristic.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
name="Indoor Bike Data"
type="org.bluetooth.characteristic.indoor_bike_data" uuid="2AD2"
last-modified="2017-02-14" approved="Yes">
<InformativeText>
<Summary>The Indoor Bike Data characteristic is used to send
training-related data to the Client from an indoor bike
(Server).</Summary>
</InformativeText>
<Value>
<Field name="Flags">
<Requirement>Mandatory</Requirement>
<Format>16bit</Format>
<BitField>
<Bit index="0" size="1" name="More Data">
<Enumerations>
<Enumeration key="0" value="False" requires="C1" />
<Enumeration key="1" value="True" />
</Enumerations>
</Bit>
<Bit index="1" size="1"
name="Instantaneous Cadence present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C2" />
</Enumerations>
</Bit>
<Bit index="2" size="1" name="Average Speed present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C3" />
</Enumerations>
</Bit>
<Bit index="3" size="1" name="Average Candence present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C4" />
</Enumerations>
</Bit>
<Bit index="4" size="1" name="Total Distance Present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C5" />
</Enumerations>
</Bit>
<Bit index="5" size="1" name="Resistance Level present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C6" />
</Enumerations>
</Bit>
<Bit index="6" size="1" name="Instantaneous Power present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C7" />
</Enumerations>
</Bit>
<Bit index="7" size="1" name="Average Power present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C8" />
</Enumerations>
</Bit>
<Bit index="8" size="1" name="Expended Energy present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C9" />
</Enumerations>
</Bit>
<Bit index="9" size="1" name="Heart Rate present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C10" />
</Enumerations>
</Bit>
<Bit index="10" size="1"
name="Metabolic Equivalent present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C11" />
</Enumerations>
</Bit>
<Bit index="11" size="1" name="Elapsed Time present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C12" />
</Enumerations>
</Bit>
<Bit index="12" size="1" name="Remaining Time present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C13" />
</Enumerations>
</Bit>
<ReservedForFutureUse index="13" size="3" />
</BitField>
</Field>
<Field name="Instantaneous Speed">
<InformativeText>Kilometer per hour with a resolution of
0.01</InformativeText>
<Requirement>C1</Requirement>
<Format>uint16</Format>
<DecimalExponent>-2</DecimalExponent>
<Unit>org.bluetooth.unit.velocity.kilometre_per_hour</Unit>
</Field>
<Field name="Average Speed">
<InformativeText>Kilometer per hour with a resolution of
0.01</InformativeText>
<Requirement>C2</Requirement>
<Format>uint16</Format>
<DecimalExponent>-2</DecimalExponent>
<Unit>org.bluetooth.unit.velocity.kilometre_per_hour</Unit>
</Field>
<Field name="Instantaneous Cadence">
<InformativeText>1/minute with a resolution of
0.5</InformativeText>
<Requirement>C3</Requirement>
<BinaryExponent>-1</BinaryExponent>
<Format>uint16</Format>
<Unit>
org.bluetooth.unit.angular_velocity.revolution_per_minute</Unit>
</Field>
<Field name="Average Cadence">
<InformativeText>1/minute with a resolution of
0.5</InformativeText>
<Requirement>C4</Requirement>
<BinaryExponent>-1</BinaryExponent>
<Format>uint16</Format>
<Unit>
org.bluetooth.unit.angular_velocity.revolution_per_minute</Unit>
</Field>
<Field name="Total Distance">
<InformativeText>Meters with a resolution of
1</InformativeText>
<Requirement>C5</Requirement>
<Format>uint24</Format>
<Unit>org.bluetooth.unit.length.metre</Unit>
</Field>
<Field name="Resistance Level">
<InformativeText>Unitless with a resolution of
1</InformativeText>
<Requirement>C6</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
</Field>
<Field name="Instantaneous Power">
<InformativeText>Watts with a resolution of
1</InformativeText>
<Requirement>C7</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.power.watt</Unit>
</Field>
<Field name="Average Power">
<InformativeText>Watts with a resolution of
1</InformativeText>
<Requirement>C8</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.power.watt</Unit>
</Field>
<Field name="Total Energy">
<InformativeText>Kilo Calorie with a resolution of
1</InformativeText>
<Requirement>C9</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.energy.kilogram_calorie</Unit>
</Field>
<Field name="Energy Per Hour">
<InformativeText>Kilo Calorie with a resolution of
1</InformativeText>
<Requirement>C9</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.energy.kilogram_calorie</Unit>
</Field>
<Field name="Energy Per Minute">
<InformativeText>Kilo Calorie with a resolution of
1</InformativeText>
<Requirement>C9</Requirement>
<Format>uint8</Format>
<Unit>org.bluetooth.unit.energy.kilogram_calorie</Unit>
</Field>
<Field name="Heart Rate">
<InformativeText>Beats per minute with a resolution of
1</InformativeText>
<Requirement>C10</Requirement>
<Format>uint8</Format>
<Unit>org.bluetooth.unit.period.beats_per_minute</Unit>
</Field>
<Field name="Metabolic Equivalent">
<InformativeText>Metabolic Equivalent with a resolution of
0.1</InformativeText>
<Requirement>C11</Requirement>
<Format>uint8</Format>
<DecimalExponent>-1</DecimalExponent>
<Unit>org.bluetooth.unit.metabolic_equivalent</Unit>
</Field>
<Field name="Elapsed Time">
<InformativeText>Second with a resolution of
1</InformativeText>
<Requirement>C12</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.time.second</Unit>
</Field>
<Field name="Remaining Time">
<InformativeText>Second with a resolution of
1</InformativeText>
<Requirement>C13</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.time.second</Unit>
</Field>
</Value>
<Note>The fields in the above table, reading from top to bottom,
are shown in the order of LSO to MSO, where LSO = Least
Significant Octet and MSO = Most Significant Octet. The Least
Significant Octet represents the eight bits numbered 0 to
7.</Note>
</Characteristic>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Copyright 2017 Bluetooth SIG, Inc. All rights reserved.-->
<Characteristic xsi:noNamespaceSchemaLocation="http://schemas.bluetooth.org/Documents/characteristic.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
name="Supported Resistance Level Range"
type="org.bluetooth.characteristic.supported_resistance_level_range"
uuid="2AD6" last-modified="2017-02-14" approved="Yes">
<InformativeText>
<Summary>The Supported Resistance Level Range characteristic is
used to send the supported resistance level range as well as
the minimum resistance increment supported by the
Server.</Summary>
</InformativeText>
<Value>
<Field name="Minimum Resistance Level">
<InformativeText>Unitless with a resolution of
0.1</InformativeText>
<Requirement>Mandatory</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
<DecimalExponent>-1</DecimalExponent>
</Field>
<Field name="Maximum Resistance Level">
<InformativeText>Unitless with a resolution of
0.1</InformativeText>
<Requirement>Mandatory</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
<DecimalExponent>-1</DecimalExponent>
</Field>
<Field name="Minimum Increment">
<InformativeText>Unitless with a resolution of
0.1</InformativeText>
<Requirement>Mandatory</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
<DecimalExponent>-1</DecimalExponent>
</Field>
</Value>
</Characteristic>

BIN
docs/realtime-chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/ui-mac.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
docs/ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

114
src/Home.qml Normal file
View File

@@ -0,0 +1,114 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtGraphicalEffects 1.12
HomeForm{
objectName: "home"
signal start_clicked;
signal stop_clicked;
signal plus_clicked(string name)
signal minus_clicked(string name)
start.onClicked: { start_clicked(); }
stop.onClicked: { stop_clicked(); }
Component.onCompleted: { console.log("completed"); }
GridView {
anchors.horizontalCenter: parent.horizontalCenter
anchors.fill: parent
cellWidth: 175
cellHeight: 125
focus: true
model: appModel
leftMargin: { (parent.width % 175) / 2; }
anchors.topMargin: 150
id: gridView
objectName: "gridview"
// highlight: Rectangle {
// width: 150
// height: 150
// color: "lightsteelblue"
// }
delegate: Item {
id: id1
width: 175
height: 125
Component.onCompleted: console.log("completed " + objectName)
Rectangle {
width: 173
height: 123
radius: 3
border.width: 1
color: Material.backgroundColor
}
Image {
id: myIcon
x: 5
anchors {
top: myValue.bottom
}
width: 48
height: 48
source: icon
}
Text {
objectName: "value"
id: myValue
color: Material.textSelectionColor
y: 0
anchors {
horizontalCenter: parent.horizontalCenter
}
text: value
horizontalAlignment: Text.AlignHCenter
font.pointSize: 48
font.bold: true
}
Text {
id: myText
anchors {
top: myIcon.top
}
font.bold: true
color: "white"
text: name
anchors.left: parent.left
anchors.leftMargin: 55
anchors.topMargin: 20
}
RoundButton {
objectName: minusName
text: "-"
onClicked: minus_clicked(objectName)
visible: writable
anchors.top: myValue.top
anchors.left: parent.left
anchors.leftMargin: 2
width: 48
height: 48
}
RoundButton {
objectName: plusName
text: "+"
onClicked: plus_clicked(objectName)
visible: writable
anchors.top: myValue.top
anchors.right: parent.right
anchors.rightMargin: 2
width: 48
height: 48
}
/*MouseArea {
anchors.fill: parent
onClicked: parent.GridView.view.currentIndex = index
}*/
}
}
}

120
src/HomeForm.ui.qml Normal file
View File

@@ -0,0 +1,120 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtGraphicalEffects 1.12
Page {
title: qsTr("qDomyos-Zwift")
id: page
property alias start: start
property alias stop: stop
property alias row: row
Item {
width: parent.width
height: 120
Row {
id: row
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
height: 100
spacing: 5
padding: 5
Rectangle {
width: 50
height: 100
color: Material.backgroundColor
Image {
anchors.verticalCenter: parent.verticalCenter
id: treadmill_connection
width: 48
height: 48
source: "icons/icons/bluetooth-icon.png"
enabled: rootItem.device
smooth: true
}
ColorOverlay {
anchors.fill: treadmill_connection
source: treadmill_connection
color: treadmill_connection.enabled ? "#00000000" : "#B0D3d3d3"
}
}
Rectangle {
width: 120
height: 100
color: Material.backgroundColor
RoundButton {
icon.source: "icons/icons/start.png"
icon.height: 46
icon.width: 46
text: "Start"
enabled: true
id: start
width: 120
height: 96
}
}
Rectangle {
width: 120
height: 100
color: Material.backgroundColor
RoundButton {
icon.source: "icons/icons/stop.png"
icon.height: 46
icon.width: 46
text: "Stop"
enabled: true
id: stop
width: 120
height: 96
}
}
Rectangle {
id: item2
width: 50
height: 100
color: Material.backgroundColor
Image {
anchors.verticalCenter: parent.verticalCenter
id: zwift_connection
width: 48
height: 48
source: "icons/icons/zwift-on.png"
enabled: rootItem.zwift
smooth: true
}
ColorOverlay {
anchors.fill: zwift_connection
source: zwift_connection
color: zwift_connection.enabled ? "#00000000" : "#B0D3d3d3"
}
}
}
Row {
id: row1
width: parent.width
anchors.bottom: row.bottom
anchors.bottomMargin: -10
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: rootItem.info
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;formeditorZoom:0.6600000262260437;height:480;width:640}
}
##^##*/

14
src/Page1Form.ui.qml Normal file
View File

@@ -0,0 +1,14 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
Page {
width: 600
height: 400
title: qsTr("Page 1")
Label {
text: qsTr("You are on Page 1.")
anchors.centerIn: parent
}
}

14
src/Page2Form.ui.qml Normal file
View File

@@ -0,0 +1,14 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
Page {
width: 600
height: 400
title: qsTr("Page 2")
Label {
text: qsTr("You are on Page 2.")
anchors.centerIn: parent
}
}

View File

@@ -0,0 +1,79 @@
<?xml version="1.0"?>
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0" android:versionCode="1" 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 -->
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:debuggable="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="qdomyos-zwift" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Application arguments -->
<!-- meta-data android:name="android.app.arguments" android:value="arg1 arg2 arg3"/ -->
<!-- Application arguments -->
<meta-data android:name="android.app.lib_name" android:value="qdomyos-zwift"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Used to specify custom system library path to run with local system libs -->
<!-- <meta-data android:name="android.app.system_libs_prefix" android:value="/system/lib/"/> -->
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<meta-data android:value="@string/unsupported_android_version" android:name="android.app.unsupported_android_version"/>
<!-- Messages maps -->
<!-- Splash screen -->
<!-- Orientation-specific (portrait/landscape) data is checked first. If not available for current orientation,
then android.app.splash_screen_drawable. For best results, use together with splash_screen_sticky and
use hideSplashScreen() with a fade-out animation from Qt Android Extras to hide the splash screen when you
are done populating your window with content. -->
<!-- meta-data android:name="android.app.splash_screen_drawable_portrait" android:resource="@drawable/logo_portrait" / -->
<!-- meta-data android:name="android.app.splash_screen_drawable_landscape" android:resource="@drawable/logo_landscape" / -->
<!-- meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/logo"/ -->
<!-- meta-data android:name="android.app.splash_screen_sticky" android:value="true"/ -->
<!-- Splash screen -->
<!-- Background running -->
<!-- Warning: changing this value to true may cause unexpected crashes if the
application still try to draw after
"applicationStateChanged(Qt::ApplicationSuspended)"
signal is sent! -->
<meta-data android:name="android.app.background_running" android:value="false"/>
<!-- Background running -->
<!-- auto screen scale factor -->
<meta-data android:name="android.app.auto_screen_scale_factor" android:value="full"/>
<!-- auto screen scale factor -->
<!-- extract android style -->
<!-- available android:values :
* default - In most cases this will be the same as "full", but it can also be something else if needed, e.g., for compatibility reasons
* full - useful QWidget & Quick Controls 1 apps
* minimal - useful for Quick Controls 2 apps, it is much faster than "full"
* none - useful for apps that don't use any of the above Qt modules
-->
<meta-data android:name="android.app.extract_android_style" android:value="default"/>
<!-- extract android style -->
</activity>
<activity android:name="org.cagnulen.qdomyoszwift.MyActivity" />
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_CHECKIN_PROPERTIES"/>
</manifest>

68
src/android/build.gradle Normal file
View File

@@ -0,0 +1,68 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.0'
}
}
repositories {
google()
jcenter()
}
apply plugin: 'com.android.application'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}
android {
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion '28.0.3'
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
resources.srcDirs = ['resources']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
lintOptions {
abortOnError false
}
// Do not compress Qt binary resources file
aaptOptions {
noCompress 'rcc'
}
defaultConfig {
resConfig "en"
minSdkVersion = qtMinSdkVersion
targetSdkVersion = qtTargetSdkVersion
}
}

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
src/android/gradlew vendored Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
src/android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,22 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<array name="qt_sources">
<item>https://download.qt.io/ministro/android/qt5/qt-5.14</item>
</array>
<!-- The following is handled automatically by the deployment tool. It should
not be edited manually. -->
<array name="bundled_libs">
<!-- %%INSERT_EXTRA_LIBS%% -->
</array>
<array name="qt_libs">
<!-- %%INSERT_QT_LIBS%% -->
</array>
<array name="load_local_libs">
<!-- %%INSERT_LOCAL_LIBS%% -->
</array>
</resources>

View File

@@ -0,0 +1,8 @@
package org.cagnulen.qdomyoszwift;
public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
@Override
public void onCreate(android.os.Bundle savedInstanceState){
super.onCreate(savedInstanceState);
this.getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}

122
src/bike.cpp Normal file
View File

@@ -0,0 +1,122 @@
#include <QDebug>
#include "bike.h"
bike::bike()
{
}
void bike::changeResistance(int8_t resistance) { requestResistance = resistance;}
double bike::currentCrankRevolutions() { return CrankRevs;}
uint16_t bike::lastCrankEventTime() { return LastCrankEventTime;}
int8_t bike::currentResistance() { return Resistance;}
uint8_t bike::currentCadence() { return Cadence;}
uint8_t bike::fanSpeed() { return FanSpeed; }
bool bike::connected() { return false; }
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }
uint16_t bike::watts()
{
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
const uint16_t watt_cad40_min = 25;
const uint16_t watt_cad40_max = 55;
const uint16_t watt_cad45_min = 35;
const uint16_t watt_cad45_max = 65;
const uint16_t watt_cad50_min = 40;
const uint16_t watt_cad50_max = 80;
const uint16_t watt_cad55_min = 50;
const uint16_t watt_cad55_max = 105;
const uint16_t watt_cad60_min = 60;
const uint16_t watt_cad60_max = 125;
const uint16_t watt_cad65_min = 70;
const uint16_t watt_cad65_max = 160;
const uint16_t watt_cad70_min = 85;
const uint16_t watt_cad70_max = 190;
const uint16_t watt_cad75_min = 100;
const uint16_t watt_cad75_max = 240;
const uint16_t watt_cad80_min = 115;
const uint16_t watt_cad80_max = 280;
const uint16_t watt_cad85_min = 130;
const uint16_t watt_cad85_max = 340;
const uint16_t watt_cad90_min = 150;
const uint16_t watt_cad90_max = 390;
const uint16_t watt_cad95_min = 175;
const uint16_t watt_cad95_max = 450;
const uint16_t watt_cad100_min = 195;
const uint16_t watt_cad100_max = 520;
const uint16_t watt_cad105_min = 210;
const uint16_t watt_cad105_max = 600;
const uint16_t watt_cad110_min = 245;
const uint16_t watt_cad110_max = 675;
const uint16_t watt_cad115_min = 270;
const uint16_t watt_cad115_max = 760;
const uint16_t watt_cad120_min = 300;
const uint16_t watt_cad120_max = 850;
const uint16_t watt_cad125_min = 330;
const uint16_t watt_cad125_max = 945;
const uint16_t watt_cad130_min = 360;
const uint16_t watt_cad130_max = 1045;
if(currentSpeed() <= 0) return 0;
if(currentCadence() < 41)
return((((watt_cad40_max-watt_cad40_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad40_min);
else if(currentCadence() < 46)
return((((watt_cad45_max-watt_cad45_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad45_min);
else if(currentCadence() < 51)
return((((watt_cad50_max-watt_cad50_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad50_min);
else if(currentCadence() < 56)
return((((watt_cad55_max-watt_cad55_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad55_min);
else if(currentCadence() < 61)
return((((watt_cad60_max-watt_cad60_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad60_min);
else if(currentCadence() < 66)
return((((watt_cad65_max-watt_cad65_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad65_min);
else if(currentCadence() < 71)
return((((watt_cad70_max-watt_cad70_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad70_min);
else if(currentCadence() < 76)
return((((watt_cad75_max-watt_cad75_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad75_min);
else if(currentCadence() < 81)
return((((watt_cad80_max-watt_cad80_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad80_min);
else if(currentCadence() < 86)
return((((watt_cad85_max-watt_cad85_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad85_min);
else if(currentCadence() < 91)
return((((watt_cad90_max-watt_cad90_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad90_min);
else if(currentCadence() < 96)
return((((watt_cad95_max-watt_cad95_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad95_min);
else if(currentCadence() < 101)
return((((watt_cad100_max-watt_cad100_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad100_min);
else if(currentCadence() < 106)
return((((watt_cad105_max-watt_cad105_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad105_min);
else if(currentCadence() < 111)
return((((watt_cad110_max-watt_cad110_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad110_min);
else if(currentCadence() < 116)
return((((watt_cad115_max-watt_cad115_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad115_min);
else if(currentCadence() < 121)
return((((watt_cad120_max-watt_cad120_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad120_min);
else if(currentCadence() < 126)
return((((watt_cad125_max-watt_cad125_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad125_min);
else
return((((watt_cad130_max-watt_cad130_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad130_min);
return 0;
}

36
src/bike.h Normal file
View File

@@ -0,0 +1,36 @@
#ifndef BIKE_H
#define BIKE_H
#include <QObject>
#include "bluetoothdevice.h"
class bike:public bluetoothdevice
{
Q_OBJECT
public:
bike();
virtual int8_t currentResistance();
virtual uint8_t currentCadence();
virtual uint8_t fanSpeed();
virtual double currentCrankRevolutions();
virtual uint16_t lastCrankEventTime();
virtual bool connected();
uint16_t watts();
bluetoothdevice::BLUETOOTH_TYPE deviceType();
public slots:
virtual void changeResistance(int8_t res);
signals:
void bikeStarted();
protected:
uint8_t Cadence = 0;
int8_t Resistance = 0;
uint16_t LastCrankEventTime = 0;
int8_t requestResistance = -1;
double CrankRevs = 0;
};
#endif // BIKE_H

133
src/bluetooth.cpp Normal file
View File

@@ -0,0 +1,133 @@
#include "bluetooth.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
bluetooth::bluetooth(bool logs, QString deviceName, bool noWriteResistance, bool noHeartService, uint32_t pollDeviceTime, bool noConsole, bool testResistance) : QObject(nullptr)
{
QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
filterDevice = deviceName;
this->testResistance = testResistance;
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
this->pollDeviceTime = pollDeviceTime;
this->noConsole = noConsole;
this->logs = logs;
#ifndef WIN32
if(!QBluetoothLocalDevice::allDevices().count())
{
debug("no bluetooth dongle found!");
}
else
#endif
{
// Create a discovery agent and connect to its signals
discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
this, SLOT(deviceDiscovered(QBluetoothDeviceInfo)));
// Start a discovery
discoveryAgent->start();
}
}
void bluetooth::debug(QString text)
{
QString debug = QDateTime::currentDateTime().toString() + " " + QString::number(QDateTime::currentMSecsSinceEpoch()) + " " + text;
if(logs)
qDebug() << debug;
}
void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
emit deviceFound(device.name());
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')' + " " + device.majorDeviceClass() + ":" + device.minorDeviceClass());
/* only on qt 5.12
foreach(quint16 i, device.manufacturerIds())
{
debug("manufacturer id: " + QString::number(i) + " -> " + device.manufacturerData(i));
}*/
bool filter = true;
if(filterDevice.length())
{
filter = (device.name().compare(filterDevice, Qt::CaseInsensitive) == 0);
}
if(device.name().startsWith("Domyos-Bike") && !device.name().startsWith("DomyosBridge") && filter)
{
discoveryAgent->stop();
domyosBike = new domyosbike(noWriteResistance, noHeartService, testResistance);
emit(deviceConnected());
connect(domyosBike, SIGNAL(disconnected()), this, SLOT(restart()));
connect(domyosBike, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
domyosBike->deviceDiscovered(device);
}
else if(device.name().startsWith("Domyos") && !device.name().startsWith("DomyosBridge") && filter)
{
discoveryAgent->stop();
domyos = new domyostreadmill(this->pollDeviceTime, noConsole, noHeartService);
emit(deviceConnected());
connect(domyos, SIGNAL(disconnected()), this, SLOT(restart()));
connect(domyos, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
domyos->deviceDiscovered(device);
}
else if((device.name().startsWith("TRX ROUTE KEY")) && filter)
{
discoveryAgent->stop();
toorx = new toorxtreadmill();
emit(deviceConnected());
connect(toorx, SIGNAL(disconnected()), this, SLOT(restart()));
connect(toorx, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
toorx->deviceDiscovered(device);
}
else if((device.name().startsWith("TOORX")) && filter)
{
discoveryAgent->stop();
trxappgateusb = new trxappgateusbtreadmill();
emit(deviceConnected());
connect(trxappgateusb, SIGNAL(disconnected()), this, SLOT(restart()));
connect(trxappgateusb, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
trxappgateusb->deviceDiscovered(device);
}
}
void bluetooth::restart()
{
if(domyos)
{
delete domyos;
domyos = 0;
}
if(domyosBike)
{
delete domyosBike;
domyosBike = 0;
}
if(toorx)
{
delete toorx;
toorx = 0;
}
if(trxappgateusb)
{
delete trxappgateusb;
trxappgateusb = 0;
}
discoveryAgent->start();
}
bluetoothdevice* bluetooth::device()
{
if(domyos)
return domyos;
else if(domyosBike)
return domyosBike;
else if(toorx)
return toorx;
else if(trxappgateusb)
return trxappgateusb;
return nullptr;
}

62
src/bluetooth.h Normal file
View File

@@ -0,0 +1,62 @@
#ifndef BLUETOOTH_H
#define BLUETOOTH_H
#include <QObject>
#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>
#include <QtCore/qloggingcategory.h>
#include <QFile>
#include "treadmill.h"
#include "domyostreadmill.h"
#include "domyosbike.h"
#include "trxappgateusbtreadmill.h"
#include "toorxtreadmill.h"
#include "bluetoothdevice.h"
class bluetooth : public QObject
{
Q_OBJECT
public:
explicit bluetooth(bool logs, QString deviceName = "", bool noWriteResistance = false, bool noHeartService = false, uint32_t pollDeviceTime = 200, bool noConsole = false, bool testResistance = false);
bluetoothdevice* device();
private:
QFile* debugCommsLog = 0;
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
domyostreadmill* domyos = 0;
domyosbike* domyosBike = 0;
toorxtreadmill* toorx = 0;
trxappgateusbtreadmill* trxappgateusb = 0;
QString filterDevice = "";
bool testResistance = false;
bool noWriteResistance = false;
bool noHeartService = false;
bool noConsole = false;
bool logs = true;
uint32_t pollDeviceTime = 200;
signals:
void deviceConnected();
void deviceFound(QString name);
public slots:
void restart();
void debug(QString string);
private slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
signals:
};
#endif // BLUETOOTH_H

20
src/bluetoothdevice.cpp Normal file
View File

@@ -0,0 +1,20 @@
#include "bluetoothdevice.h"
#include <QTime>
bluetoothdevice::bluetoothdevice()
{
}
bluetoothdevice::BLUETOOTH_TYPE bluetoothdevice::deviceType() { return bluetoothdevice::UNKNOWN; }
void bluetoothdevice::start(){ requestStart = 1; }
void bluetoothdevice::stop(){ requestStop = 1; }
unsigned char bluetoothdevice::currentHeart(){ return Heart; }
double bluetoothdevice::currentSpeed(){ return Speed; }
QTime bluetoothdevice::currentPace(){ return QTime(0, (int)(1.0 / (Speed / 60.0)), (((double)(1.0 / (Speed / 60.0)) - ((double)((int)(1.0 / (Speed / 60.0))))) * 60.0), 0 ); }
double bluetoothdevice::odometer(){ return Distance; }
double bluetoothdevice::calories(){ return KCal; }
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
void* bluetoothdevice::VirtualDevice() { return nullptr; }
bool bluetoothdevice::changeFanSpeed(uint8_t speed) { Q_UNUSED(speed); return false; }
bool bluetoothdevice::connected() { return false; }

49
src/bluetoothdevice.h Normal file
View File

@@ -0,0 +1,49 @@
#ifndef BLUETOOTHDEVICE_H
#define BLUETOOTHDEVICE_H
#include <QObject>
class bluetoothdevice : public QObject
{
Q_OBJECT
public:
bluetoothdevice();
virtual unsigned char currentHeart();
virtual double currentSpeed();
virtual QTime currentPace();
virtual double odometer();
virtual double calories();
virtual uint8_t fanSpeed();
virtual bool connected();
virtual void* VirtualDevice();
uint16_t watts(double weight=75.0);
virtual bool changeFanSpeed(uint8_t speed);
enum BLUETOOTH_TYPE {
UNKNOWN = 0,
TREADMILL,
BIKE,
ROWING
};
virtual BLUETOOTH_TYPE deviceType();
public slots:
virtual void start();
virtual void stop();
protected:
double elapsed = 0;
double Speed = 0;
double KCal = 0;
double Distance = 0;
uint8_t FanSpeed = 0;
uint8_t Heart = 0;
int8_t requestStart = -1;
int8_t requestStop = -1;
int8_t requestIncreaseFan = -1;
int8_t requestDecreaseFan = -1;
};
#endif // BLUETOOTHDEVICE_H

163
src/charts.cpp Normal file
View File

@@ -0,0 +1,163 @@
#include "charts.h"
#include "ui_charts.h"
charts::charts(MainWindow *parent) :
QDialog(parent),
ui(new Ui::charts)
{
ui->setupUi(this);
this->parent = parent;
chart = new QtCharts::QChart();
chart_view = new QtCharts::QChartView(chart, ui->widget);
ui->widget->setVisible(false);
chart_series_speed = new QtCharts::QLineSeries();
chart_series_pace = new QtCharts::QLineSeries();
chart_series_inclination = new QtCharts::QLineSeries();
chart_series_heart = new QtCharts::QLineSeries();
chart_series_watt = new QtCharts::QLineSeries();
chart_series_resistance = new QtCharts::QLineSeries();
chart_series_speed->setPointLabelsVisible(false); // is false by default
chart_series_speed->setPointLabelsColor(Qt::black);
chart_series_speed->setPointLabelsFormat("@yPoint km/h");
chart_series_pace->setPointLabelsVisible(false); // is false by default
chart_series_pace->setPointLabelsColor(Qt::black);
chart_series_pace->setPointLabelsFormat("@yPoint min/km");
chart_series_inclination->setPointLabelsVisible(false); // is false by default
chart_series_inclination->setPointLabelsColor(Qt::black);
chart_series_inclination->setPointLabelsFormat("@yPoint%");
chart_series_heart->setPointLabelsVisible(false); // is false by default
chart_series_heart->setPointLabelsColor(Qt::black);
chart_series_heart->setPointLabelsFormat("@yPoint bpm");
chart_series_watt->setPointLabelsVisible(false); // is false by default
chart_series_watt->setPointLabelsColor(Qt::black);
chart_series_watt->setPointLabelsFormat("@yPoint W");
chart_series_resistance->setPointLabelsVisible(false); // is false by default
chart_series_resistance->setPointLabelsColor(Qt::black);
chart_series_resistance->setPointLabelsFormat("@yPoint lvl");
chart_series_speed->setName("Speed (km/h)");
chart_series_pace->setName("Pace (min/km)");
chart_series_inclination->setName("Inclination (%)");
chart_series_heart->setName("Heart (bpm)");
chart_series_watt->setName("Watt (W)");
chart_series_resistance->setName("Resistance (lvl)");
chart->legend()->setAlignment(Qt::AlignBottom);
chart_view->setRenderHint(QPainter::Antialiasing);
chart_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
ui->frame->layout()->addWidget(chart_view);
}
void charts::update()
{
if(chart->series().count())
{
if(ui->speed->isChecked())
chart->removeSeries(chart_series_speed);
if(ui->pace->isChecked())
chart->removeSeries(chart_series_pace);
if(ui->inclination->isChecked())
chart->removeSeries(chart_series_inclination);
if(ui->heart->isChecked())
chart->removeSeries(chart_series_heart);
if(ui->watt->isChecked())
chart->removeSeries(chart_series_watt);
if(ui->resistance->isChecked())
chart->removeSeries(chart_series_resistance);
}
chart_series_inclination->clear();
chart_series_speed->clear();
chart_series_pace->clear();
chart_series_heart->clear();
chart_series_watt->clear();
chart_series_resistance->clear();
const int maxQueue = 100;
for(int g=0; g<(parent->Session.count() > maxQueue ? maxQueue : parent->Session.count()); g++)
{
int index = g + (parent->Session.count() > maxQueue ? parent->Session.count() % maxQueue : 0);
if(ui->inclination->isChecked())
chart_series_inclination->append(g, static_cast<double>(parent->Session[index].inclination));
if(ui->speed->isChecked())
chart_series_speed->append(g, static_cast<qreal>(parent->Session[index].speed));
if(ui->pace->isChecked())
chart_series_pace->append(g, static_cast<qreal>(parent->Session[index].pace));
if(ui->heart->isChecked())
chart_series_heart->append(g, static_cast<qreal>(parent->Session[index].heart));
if(ui->watt->isChecked())
chart_series_watt->append(g, static_cast<qreal>(parent->Session[index].watt));
if(ui->resistance->isChecked())
chart_series_resistance->append(g, static_cast<qreal>(parent->Session[index].resistance));
}
if(ui->inclination->isChecked())
chart->addSeries(chart_series_inclination);
if(ui->speed->isChecked())
chart->addSeries(chart_series_speed);
if(ui->pace->isChecked())
chart->addSeries(chart_series_pace);
if(ui->heart->isChecked())
chart->addSeries(chart_series_heart);
if(ui->watt->isChecked())
chart->addSeries(chart_series_watt);
if(ui->resistance->isChecked())
chart->addSeries(chart_series_resistance);
chart->createDefaultAxes();
}
charts::~charts()
{
delete ui;
}
void charts::on_valueOnChart_stateChanged(int arg1)
{
Q_UNUSED(arg1);
if(ui->valueOnChart->checkState() == Qt::Checked)
{
chart_series_speed->setPointLabelsVisible(true);
chart_series_pace->setPointLabelsVisible(true);
chart_series_inclination->setPointLabelsVisible(true); // is false by default
chart_series_heart->setPointLabelsVisible(true); // is false by default
chart_series_watt->setPointLabelsVisible(true); // is false by default
chart_series_resistance->setPointLabelsVisible(true); // is false by default
}
else
{
chart_series_speed->setPointLabelsVisible(false);
chart_series_pace->setPointLabelsVisible(false);
chart_series_inclination->setPointLabelsVisible(false); // is false by default
chart_series_heart->setPointLabelsVisible(false); // is false by default
chart_series_watt->setPointLabelsVisible(false); // is false by default
chart_series_resistance->setPointLabelsVisible(false); // is false by default
}
}
void charts::on_speed_clicked()
{
}
void charts::on_Inclination_clicked()
{
}
void charts::on_watt_clicked()
{
}
void charts::on_resistance_clicked()
{
}
void charts::on_heart_clicked()
{
}

48
src/charts.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef CHARTS_H
#define CHARTS_H
#include <QDialog>
#include <QtCharts>
#include "mainwindow.h"
namespace Ui {
class charts;
}
class charts : public QDialog
{
Q_OBJECT
public:
explicit charts(MainWindow *parent = nullptr);
void update();
~charts();
private slots:
void on_valueOnChart_stateChanged(int arg1);
void on_speed_clicked();
void on_Inclination_clicked();
void on_watt_clicked();
void on_resistance_clicked();
void on_heart_clicked();
private:
Ui::charts *ui;
MainWindow* parent = 0;
QtCharts::QChart* chart = 0;
QtCharts::QChartView* chart_view = 0;
QtCharts::QLineSeries* chart_series_speed = 0;
QtCharts::QLineSeries* chart_series_inclination = 0;
QtCharts::QLineSeries* chart_series_heart = 0;
QtCharts::QLineSeries* chart_series_watt = 0;
QtCharts::QLineSeries* chart_series_resistance = 0;
QtCharts::QLineSeries* chart_series_pace = 0;
};
#endif // CHARTS_H

210
src/charts.ui Normal file
View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>charts</class>
<widget class="QDialog" name="charts">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>480</height>
</rect>
</property>
<property name="windowTitle">
<string>Charts</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="speed">
<property name="text">
<string>Speed</string>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/icons/speed.png</normalon>
</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="inclination">
<property name="text">
<string>Inclination</string>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/icons/inclination.png</normalon>
</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="watt">
<property name="text">
<string>Watt</string>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/icons/watt.png</normalon>
</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resistance">
<property name="text">
<string>Resistance</string>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/icons/inclination.png</normalon>
</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="heart">
<property name="text">
<string>Heart</string>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/icons/heart_red.png</normalon>
</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pace">
<property name="text">
<string>Pace</string>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/icons/pace.png</normalon>
</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="valueOnChart">
<property name="text">
<string>Value on Chart</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

518
src/domyosbike.cpp Normal file
View File

@@ -0,0 +1,518 @@
#include "domyosbike.h"
#include "virtualbike.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testResistance)
{
refresh = new QTimer(this);
this->testResistance = testResistance;
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
initDone = false;
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
refresh->start(200);
}
void domyosbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool wait_for_response)
{
QEventLoop loop;
if(wait_for_response)
{
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
}
else
{
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
}
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)data, data_len));
if(!disable_log)
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
loop.exec();
}
void domyosbike::updateDisplay(uint16_t elapsed)
{
uint8_t display[] = {0xf0, 0xcb, 0x03, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x02,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00,
0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x00};
display[3] = (elapsed / 60) & 0xFF; // high byte for elapsed time (in seconds)
display[4] = (elapsed % 60 & 0xFF); // low byte for elasped time (in seconds)
display[7] = ((uint8_t)((uint16_t)(currentSpeed()) >> 8)) & 0xFF;
display[8] = (uint8_t)(currentSpeed()) & 0xFF;
display[12] = currentHeart();
//display[13] = ((((uint8_t)calories())) >> 8) & 0xFF;
//display[14] = (((uint8_t)calories())) & 0xFF;
display[16] = (uint8_t)currentCadence();
display[19] = ((((uint16_t)calories())) >> 8) & 0xFF;
display[20] = (((uint16_t)calories())) & 0xFF;
for(uint8_t i=0; i<sizeof(display)-1; i++)
{
display[26] += display[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(display, 20, "updateDisplay elapsed=" + QString::number(elapsed) );
writeCharacteristic(&display[20], sizeof (display) - 20, "updateDisplay elapsed=" + QString::number(elapsed) );
//if(bike_type == CHANG_YOW)
{
uint8_t display2[] = {0xf0, 0xcd, 0x01, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00};
display2[3] = ((((uint16_t)(odometer() * 10))) >> 8) & 0xFF;
display2[4] = (((uint16_t)(odometer() * 10))) & 0xFF;
for(uint8_t i=0; i<sizeof(display2)-1; i++)
{
display2[26] += display2[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(display2, 20, "updateDisplay2");
writeCharacteristic(&display2[20], sizeof (display2) - 20, "updateDisplay2");
}
}
void domyosbike::forceResistance(int8_t requestResistance)
{
uint8_t write[] = {0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0xff,
0xff, 0xff, 0x00};
write[10] = requestResistance;
for(uint8_t i=0; i<sizeof(write)-1; i++)
{
write[22] += write[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(write, 20, "forceResistance " + QString::number(requestResistance));
writeCharacteristic(&write[20], sizeof (write) - 20, "forceResistance " + QString::number(requestResistance));
}
void domyosbike::update()
{
static QDateTime lastTime;
static bool first = true;
uint8_t noOpData[] = { 0xf0, 0xac, 0x9c };
// stop tape
uint8_t initDataF0C800B8[] = { 0xf0, 0xc8, 0x00, 0xb8 };
static uint8_t sec1 = 0;
if(m_control->state() == QLowEnergyController::UnconnectedState)
{
emit disconnected();
return;
}
if(initRequest)
{
initRequest = false;
//if(bike_type == CHANG_YOW)
btinit_changyow(false);
//else
// btinit_telink(false);
}
else if(btbike.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotifyCharacteristic.isValid() &&
initDone)
{
QDateTime current = QDateTime::currentDateTime();
if(currentSpeed() > 0.0 && !first)
elapsed += (((double)lastTime.msecsTo(current)) / ((double)1000.0));
lastTime = current;
// updating the treadmill console every second
if(sec1++ == (500 / refresh->interval()))
{
sec1 = 0;
updateDisplay(elapsed);
}
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true);
if(testResistance)
{
if((((int)elapsed) % 5) == 0)
{
uint8_t new_res = currentResistance() + 1;
if(new_res > 15)
new_res = 1;
forceResistance(new_res);
}
}
if(requestResistance != -1)
{
if(requestResistance > 15) requestResistance = 15;
else if(requestResistance == 0) requestResistance = 1;
if(requestResistance != currentResistance())
{
debug("writing resistance " + QString::number(requestResistance));
forceResistance(requestResistance);
}
requestResistance = -1;
}
if(requestStart != -1)
{
debug("starting...");
//if(bike_type == CHANG_YOW)
btinit_changyow(true);
//else
// btinit_telink(true);
requestStart = -1;
emit bikeStarted();
}
if(requestStop != -1)
{
debug("stopping...");
writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
}
first = false;
}
void domyosbike::serviceDiscovered(const QBluetoothUuid &gatt)
{
debug("serviceDiscovered " + gatt.toString());
}
static QByteArray lastPacket;
void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
Q_UNUSED(characteristic);
static QDateTime lastRefresh = QDateTime::currentDateTime();
debug(" << " + newValue.toHex(' '));
if (lastPacket.length() && lastPacket == newValue)
return;
lastPacket = newValue;
if (newValue.length() != 26)
return;
if (newValue.at(22) == 0x06)
{
debug("start button pressed!");
requestStart = 1;
}
else if (newValue.at(22) == 0x07)
{
debug("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
return;*/
double speed = GetSpeedFromPacket(newValue);
double kcal = GetKcalFromPacket(newValue);
double distance = GetDistanceFromPacket(newValue);
Cadence = newValue.at(9);
Resistance = newValue.at(14);
if(Resistance < 1)
{
debug("invalid resistance value " + QString::number(Resistance) + " putting to default");
Resistance = 1;
}
Heart = newValue.at(18);
CrankRevs += ((double)(lastRefresh.msecsTo(QDateTime::currentDateTime())) * ((double)Cadence / 60000.0) );
LastCrankEventTime += (uint16_t)((lastRefresh.msecsTo(QDateTime::currentDateTime())) * 1.024);
lastRefresh = QDateTime::currentDateTime();
debug("Current speed: " + QString::number(speed));
debug("Current cadence: " + QString::number(Cadence));
debug("Current resistance: " + QString::number(Resistance));
debug("Current heart: " + QString::number(Heart));
debug("Current KCal: " + QString::number(kcal));
debug("Current Distance: " + QString::number(distance));
debug("Current CrankRevs: " + QString::number(CrankRevs));
debug("Last CrankEventTime: " + QString::number(LastCrankEventTime));
debug("Current Watt: " + QString::number(watts()));
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
Speed = speed;
KCal = kcal;
Distance = distance;
}
double domyosbike::GetSpeedFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(6) << 8) | packet.at(7);
double data = (double)convertedData / 10.0f;
return data;
}
double domyosbike::GetKcalFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(10) << 8) | packet.at(11);
return (double)convertedData;
}
double domyosbike::GetDistanceFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(12) << 8) | packet.at(13);
double data = ((double)convertedData) / 10.0f;
return data;
}
void domyosbike::btinit_changyow(bool startTape)
{
// set speed and incline to 0
uint8_t initData1[] = { 0xf0, 0xc8, 0x01, 0xb9 };
uint8_t initData2[] = { 0xf0, 0xc9, 0xb9 };
// main startup sequence
uint8_t initDataStart[] = { 0xf0, 0xa3, 0x93 };
uint8_t initDataStart2[] = { 0xf0, 0xa4, 0x94 };
uint8_t initDataStart3[] = { 0xf0, 0xa5, 0x95 };
uint8_t initDataStart4[] = { 0xf0, 0xab, 0x9b };
uint8_t initDataStart5[] = { 0xf0, 0xc4, 0x03, 0xb7 };
uint8_t initDataStart6[] =
{
0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff
};
uint8_t initDataStart7[] = { 0xff, 0xff, 0x8b }; // power on bt icon
uint8_t initDataStart8[] =
{
0xf0, 0xcb, 0x02, 0x00, 0x08, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00
};
uint8_t initDataStart9[] = { 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xb6 }; // power on bt word
uint8_t initDataStart10[] =
{
0xf0, 0xad, 0xff, 0xff, 0x00, 0x05, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0x01, 0xff
};
uint8_t initDataStart11[] = { 0xff, 0xff, 0x94 }; // start tape
uint8_t initDataStart12[] =
{
0xf0, 0xcb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x14, 0x01, 0xff, 0xff
};
uint8_t initDataStart13[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbd };
writeCharacteristic(initData1, sizeof(initData1), "init", false, true);
writeCharacteristic(initData2, sizeof(initData2), "init", false, true);
writeCharacteristic(initDataStart, sizeof(initDataStart), "init", false, true);
writeCharacteristic(initDataStart2, sizeof(initDataStart2), "init", false, true);
writeCharacteristic(initDataStart3, sizeof(initDataStart3), "init", false, true);
writeCharacteristic(initDataStart4, sizeof(initDataStart4), "init", false, true);
writeCharacteristic(initDataStart5, sizeof(initDataStart5), "init", false, true);
writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init", false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init", false, true);
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init", false, false);
if(startTape)
{
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init", false, true);
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init", false, false);
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init", false, true);
}
initDone = true;
}
void domyosbike::btinit_telink(bool startTape)
{
Q_UNUSED(startTape)
// set speed and incline to 0
uint8_t initData1[] = { 0xf0, 0xc8, 0x01, 0xb9 };
uint8_t initData2[] = { 0xf0, 0xc9, 0xb9 };
uint8_t noOpData[] = { 0xf0, 0xac, 0x9c };
// main startup sequence
uint8_t initDataStart[] = { 0xf0, 0xcc, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff, 0xb8 };
writeCharacteristic(initData1, sizeof(initData1), "init");
writeCharacteristic(initData2, sizeof(initData2), "init");
writeCharacteristic(noOpData, sizeof(noOpData), "noOp");
writeCharacteristic(initDataStart, sizeof(initDataStart), "init");
updateDisplay(0);
initDone = true;
}
void domyosbike::stateChanged(QLowEnergyService::ServiceState state)
{
QBluetoothUuid _gattWriteCharacteristicId((QString)"49535343-8841-43f4-a8d4-ecbe34729bb3");
QBluetoothUuid _gattNotifyCharacteristicId((QString)"49535343-1e4d-4bd9-ba61-23c647249616");
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
debug("BTLE stateChanged " + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
if(state == QLowEnergyService::ServiceDiscovered)
{
//qDebug() << gattCommunicationChannelService->characteristics();
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotifyCharacteristic.isValid());
// establish hook into notifications
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
this, SLOT(characteristicChanged(QLowEnergyCharacteristic,QByteArray)));
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)),
this, SLOT(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)));
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)),
this, SLOT(errorService(QLowEnergyService::ServiceError)));
connect(gattCommunicationChannelService, SIGNAL(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)), this,
SLOT(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)));
// ******************************************* virtual bike init *************************************
static uint8_t first = 0;
if(!first)
{
debug("creating virtual bike interface...");
virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
connect(virtualBike,&virtualbike::debug ,this,&domyosbike::debug);
}
first = 1;
// ********************************************************************************************************
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattCommunicationChannelService->writeDescriptor(gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
}
}
void domyosbike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
{
debug("descriptorWritten " + descriptor.name() + " " + newValue.toHex(' '));
initRequest = true;
}
void domyosbike::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
Q_UNUSED(characteristic);
debug("characteristicWritten " + newValue.toHex(' '));
}
void domyosbike::serviceScanDone(void)
{
debug("serviceScanDone");
QBluetoothUuid _gattCommunicationChannelServiceId((QString)"49535343-fe7d-4ae5-8fa9-9fafd205e455");
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
connect(gattCommunicationChannelService, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(stateChanged(QLowEnergyService::ServiceState)));
gattCommunicationChannelService->discoverDetails();
}
void domyosbike::errorService(QLowEnergyService::ServiceError err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
debug("domyosbike::errorService" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
}
void domyosbike::error(QLowEnergyController::Error err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
debug("domyosbike::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
m_control->disconnect();
}
void domyosbike::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("Domyos-Bike") && !device.name().startsWith("DomyosBridge"))
{
btbike = device;
if(device.address().toString().startsWith("57"))
{
debug("domyos telink bike found");
bike_type = TELINK;
}
else
{
debug("domyos changyow bike found");
bike_type = CHANG_YOW;
}
m_control = QLowEnergyController::createCentral(btbike, 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, 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 domyosbike::connected()
{
if(!m_control)
return false;
return m_control->state() == QLowEnergyController::DiscoveredState;
}
void* domyosbike::VirtualBike()
{
return virtualBike;
}
void* domyosbike::VirtualDevice()
{
return VirtualBike();
}

95
src/domyosbike.h Normal file
View File

@@ -0,0 +1,95 @@
#ifndef DOMYOSBIKE_H
#define DOMYOSBIKE_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 "virtualbike.h"
#include "bike.h"
class domyosbike : public bike
{
Q_OBJECT
public:
domyosbike(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false);
bool connected();
void* VirtualBike();
void* VirtualDevice();
private:
double GetSpeedFromPacket(QByteArray packet);
double GetInclinationFromPacket(QByteArray packet);
double GetKcalFromPacket(QByteArray packet);
double GetDistanceFromPacket(QByteArray packet);
void forceResistance(int8_t requestResistance);
void updateDisplay(uint16_t elapsed);
void btinit_changyow(bool startTape);
void btinit_telink(bool startTape);
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false, bool wait_for_response = false);
void startDiscover();
QTimer* refresh;
virtualbike* virtualBike = 0;
QBluetoothDeviceInfo btbike;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotifyCharacteristic;
bool initDone = false;
bool initRequest = false;
bool noWriteResistance = false;
bool noHeartService = false;
bool testResistance = false;
enum _BIKE_TYPE {
CHANG_YOW,
TELINK,
};
_BIKE_TYPE bike_type = CHANG_YOW;
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 stateChanged(QLowEnergyService::ServiceState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};
#endif // DOMYOSBIKE_H

View File

@@ -1,6 +1,9 @@
#include "domyostreadmill.h"
#include "virtualtreadmill.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
// set speed and incline to 0
uint8_t initData1[] = { 0xf0, 0xc8, 0x01, 0xb9 };
@@ -11,17 +14,6 @@ uint8_t noOpData[] = { 0xf0, 0xac, 0x9c };
// stop tape
uint8_t initDataF0C800B8[] = { 0xf0, 0xc8, 0x00, 0xb8 };
#if 0
uint8_t initDataStart[] = { 0xf0, 0xc8, 0x00, 0xb8 };
uint8_t initDataStart2[] = { 0xf0, 0xcb, 0x01, 0x00, 0x00, 0x02, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00 };
uint8_t initDataStart3[] = { 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xb6 };
uint8_t initDataStart4[] = { 0xf0, 0xc8, 0x00, 0xb8 };
uint8_t initDataStart5[] = { 0xf0, 0xcb, 0x03, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x02,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00 };
uint8_t initDataStart6[] = { 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc1 };
#endif
// main startup sequence
uint8_t initDataStart[] = { 0xf0, 0xa3, 0x93 };
uint8_t initDataStart2[] = { 0xf0, 0xa4, 0x94 };
@@ -57,129 +49,253 @@ QBluetoothUuid _gattCommunicationChannelServiceId((QString)"49535343-fe7d-4ae5-8
QBluetoothUuid _gattWriteCharacteristicId((QString)"49535343-8841-43f4-a8d4-ecbe34729bb3");
QBluetoothUuid _gattNotifyCharacteristicId((QString)"49535343-1e4d-4bd9-ba61-23c647249616");
QBluetoothDeviceInfo treadmill;
QBluetoothDeviceInfo bttreadmill;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotifyCharacteristic;
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
bool initDone = false;
bool initRequest = false;
extern volatile double currentSpeed;
extern volatile double currentIncline;
extern volatile uint8_t currentHeart;
extern volatile double requestSpeed;
extern volatile double requestIncline;
extern volatile int8_t requestStart;
extern volatile int8_t requestStop;
domyostreadmill::domyostreadmill()
domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService)
{
QTimer* refresh = new QTimer(this);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
refresh = new QTimer(this);
initDone = false;
// Create a discovery agent and connect to its signals
discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
this, SLOT(deviceDiscovered(QBluetoothDeviceInfo)));
// Start a discovery
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
refresh->start(200);
refresh->start(pollDeviceTime);
}
void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestIncline, uint16_t elapsed)
void domyostreadmill::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool wait_for_response)
{
uint8_t writeIncline[] = {0xf0, 0xcb, 0x03, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x02,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00,
(uint8_t)(requestSpeed * 10), 0x01, 0xff, 0xff, 0xff, 0xff, 0x00};
QEventLoop loop;
if(wait_for_response)
{
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
}
else
{
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
}
writeIncline[3] = (elapsed >> 8) & 0xFF; // high byte for elapsed time (in seconds)
writeIncline[4] = (elapsed & 0xFF); // low byte for elasped time (in seconds)
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)data, data_len));
writeIncline[12] = currentHeart;
if(!disable_log)
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
writeIncline[16] = (uint8_t)(requestIncline * 10);
loop.exec();
}
void domyostreadmill::updateDisplay(uint16_t elapsed)
{
uint8_t display[] = {0xf0, 0xcb, 0x03, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x02,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x01, 0x01, 0x00,
0x0c, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00};
if(elapsed > 5999) // 99:59
{
display[3] = ((elapsed / 60) / 60) & 0xFF; // high byte for elapsed time (in seconds)
display[4] = ((elapsed / 60) % 60) & 0xFF; // low byte for elasped time (in seconds)
}
else
{
display[3] = (elapsed / 60) & 0xFF; // high byte for elapsed time (in seconds)
display[4] = (elapsed % 60 & 0xFF); // low byte for elasped time (in seconds)
}
if(odometer() < 10.0)
{
display[7] = ((uint8_t)((uint16_t)(odometer() * 100) >> 8)) & 0xFF;
display[8] = (uint8_t)(odometer() * 100) & 0xFF;
display[9] = 0x02; // decimal position
}
else if(odometer() < 100.0)
{
display[7] = ((uint8_t)(odometer() * 10) >> 8) & 0xFF;
display[8] = (uint8_t)(odometer() * 10) & 0xFF;
display[9] = 0x01; // decimal position
}
else
{
display[7] = ((uint8_t)(odometer()) >> 8) & 0xFF;
display[8] = (uint8_t)(odometer()) & 0xFF;
display[9] = 0x00; // decimal position
}
display[12] = currentHeart();
display[23] = ((uint8_t)(calories()) >> 8) & 0xFF;
display[24] = (uint8_t)(calories()) & 0xFF;
for(uint8_t i=0; i<sizeof(display)-1; i++)
{
display[26] += display[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(display, 20, "updateDisplay elapsed=" + QString::number(elapsed) );
writeCharacteristic(&display[20], sizeof (display) - 20, "updateDisplay elapsed=" + QString::number(elapsed) );
}
void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestIncline)
{
uint8_t writeIncline[] = {0xf0, 0xad, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00};
writeIncline[4] = ((uint16_t)(requestSpeed*10) >> 8) & 0xFF;
writeIncline[5] = ((uint16_t)(requestSpeed*10) & 0xFF);
writeIncline[13] = ((uint16_t)(requestIncline*10) >> 8) & 0xFF;
writeIncline[14] = ((uint16_t)(requestIncline*10) & 0xFF);
for(uint8_t i=0; i<sizeof(writeIncline)-1; i++)
{
//qDebug() << QString::number(writeIncline[i], 16);
writeIncline[26] += writeIncline[i]; // the last byte is a sort of a checksum
writeIncline[22] += writeIncline[i]; // the last byte is a sort of a checksum
}
//qDebug() << "writeIncline crc" << QString::number(writeIncline[26], 16);
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)writeIncline, sizeof(writeIncline)));
writeCharacteristic(writeIncline, 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline));
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline));
}
bool domyostreadmill::changeFanSpeed(uint8_t speed)
{
uint8_t fanSpeed[] = {0xf0, 0xca, 0x00, 0x00};
if(speed > 5) return false;
fanSpeed[2] = speed;
for(uint8_t i=0; i<sizeof(fanSpeed)-1; i++)
{
fanSpeed[3] += fanSpeed[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(fanSpeed, 4, "changeFanSpeed speed=" + QString::number(speed));
return true;
}
void domyostreadmill::update()
{
static uint8_t first = 0;
static virtualtreadmill* v;
static uint32_t counter = 0;
Q_UNUSED(v);
//qDebug() << treadmill.isValid() << m_control->state() << gattCommunicationChannelService << gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
if(treadmill.isValid() &&
(m_control->state() == QLowEnergyController::ConnectedState || m_control->state() == QLowEnergyController::DiscoveredState) &&
static uint8_t sec1 = 0;
static QDateTime lastTime;
static bool first = true;
if(m_control->state() == QLowEnergyController::UnconnectedState)
{
emit disconnected();
return;
}
if(initRequest)
{
initRequest = false;
btinit(false);
}
else if(bttreadmill.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotifyCharacteristic.isValid() &&
initDone)
{
counter++;
if(!first)
QDateTime current = QDateTime::currentDateTime();
if(currentSpeed() > 0.0 && !first)
elapsed += (((double)lastTime.msecsTo(current)) / ((double)1000.0));
lastTime = current;
// updating the treadmill console every second
if(sec1++ >= (500 / refresh->interval()))
{
qDebug() << "creating virtual treadmill interface...";
v = new virtualtreadmill();
if(incompletePackets == false && noConsole == false)
{
sec1 = 0;
updateDisplay(elapsed);
}
}
first = 1;
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)noOpData, sizeof(noOpData)));
if(incompletePackets == false)
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true);
// byte 3 - 4 = elapsed time
// byte 17 = inclination
if(incompletePackets == false)
{
if(requestSpeed != -1)
{
if(requestSpeed != currentSpeed())
{
debug("writing speed " + QString::number(requestSpeed));
double inc = Inclination;
if(requestInclination != -1)
{
inc = requestInclination;
requestInclination = -1;
}
forceSpeedOrIncline(requestSpeed, inc);
}
requestSpeed = -1;
}
if(requestInclination != -1)
{
if(requestInclination != currentInclination())
{
debug("writing incline " + QString::number(requestInclination));
double speed = currentSpeed();
if(requestSpeed != -1)
{
speed = requestSpeed;
requestSpeed = -1;
}
forceSpeedOrIncline(speed, requestInclination);
}
requestInclination = -1;
}
if(requestStart != -1)
{
debug("starting...");
btinit(true);
requestStart = -1;
emit tapeStarted();
}
if(requestStop != -1)
{
debug("stopping...");
writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
if(requestIncreaseFan != -1)
{
debug("increasing fan speed...");
changeFanSpeed(FanSpeed + 1);
requestIncreaseFan = -1;
}
else if(requestDecreaseFan != -1)
{
debug("decreasing fan speed...");
changeFanSpeed(FanSpeed - 1);
requestDecreaseFan = -1;
}
}
if(requestSpeed != -1)
{
qDebug() << "writing speed" << requestSpeed;
forceSpeedOrIncline(requestSpeed, currentIncline, counter/5);
requestSpeed = -1;
}
if(requestIncline != -1)
{
qDebug() << "writing incline" << requestIncline;
forceSpeedOrIncline(currentSpeed, requestIncline, counter/5);
requestIncline = -1;
}
if(requestStart != -1)
{
qDebug() << "starting...(TODO)";
requestStart = -1;
}
if(requestStop != -1)
{
qDebug() << "stopping...";
/*gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initData1, sizeof(initData1)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initData2, sizeof(initData2)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart, sizeof(initDataStart)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart2, sizeof(initDataStart2)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart3, sizeof(initDataStart3)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart4, sizeof(initDataStart4)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart5, sizeof(initDataStart5)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart6, sizeof(initDataStart6)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart7, sizeof(initDataStart7)));*/
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataF0C800B8, sizeof(initDataF0C800B8)));
requestStop = -1;
}
elevationAcc += (currentSpeed() / 3600.0) * 1000 * (currentInclination() / 100) * (refresh->interval() / 1000);
}
first = false;
}
void domyostreadmill::serviceDiscovered(const QBluetoothUuid &gatt)
{
qDebug() << "serviceDiscovered" << gatt;
debug("serviceDiscovered " + gatt.toString());
}
static QByteArray lastPacket;
@@ -187,40 +303,100 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
{
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
Q_UNUSED(characteristic);
static QDateTime lastTime;
static bool first = true;
QByteArray value = newValue;
if (lastPacket.length() && lastPacket == newValue)
debug(" << " + QString::number(value.length()) + " " + value.toHex(' '));
if (lastPacket.length() && lastPacket == value)
return;
lastPacket = newValue;
if (newValue.length() != 26)
return;
QByteArray startBytes;
startBytes.append(0xf0);
startBytes.append(0xbc);
if (newValue.at(22) == 0x07)
QByteArray startBytes2;
startBytes2.append(0xf0);
startBytes2.append(0xdb);
// 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))
{
qDebug() << "STOP PRESSED!";
requestStop = 1;
incompletePackets = false;
debug("...final bytes received");
lastPacket.append(value);
value = lastPacket;
}
/*if ((uint8_t)newValue.at(1) != 0xbc && newValue.at(2) != 0x04) // intense run, these are the bytes for the inclination and speed status
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)))
{
debug("waiting for other bytes...");
incompletePackets = true;
}
debug("packet ignored");
return;
}
if (value.at(22) == 0x06)
{
debug("start button pressed!");
requestStart = 1;
}
else if (value.at(22) == 0x07)
{
debug("stop button pressed!");
requestStop = 1;
}
else if (value.at(22) == 0x0b)
{
debug("increase speed fan pressed!");
requestIncreaseFan = 1;
}
else if (value.at(22) == 0x0a)
{
debug("decrease speed fan pressed!");
requestDecreaseFan = 1;
}
/*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 incline = GetInclinationFromPacket(newValue);
//var isStartPressed = GetIsStartPressedFromPacket(currentPacket);
//var isStopPressed = GetIsStopPressedFromPacket(currentPacket);
double speed = GetSpeedFromPacket(value);
double incline = GetInclinationFromPacket(value);
double kcal = GetKcalFromPacket(value);
double distance = GetDistanceFromPacket(value);
#if DEBUG
Debug.WriteLine(args.CharacteristicValue.ToArray().HexDump());
#endif
Heart = value.at(18);
FanSpeed = value.at(23);
currentHeart = newValue.at(18);
if(!first)
DistanceCalculated += ((speed / 3600.0) / ( 1000.0 / (lastTime.msecsTo(QDateTime::currentDateTime()))));
qDebug() << "Current speed: " << speed;
qDebug() << "Current incline: " << incline;
qDebug() << "Current heart:" << currentHeart;
debug("Current speed: " + QString::number(speed));
debug("Current incline: " + QString::number(incline));
debug("Current heart: " + QString::number(Heart));
debug("Current KCal: " + QString::number(kcal));
debug("Current Distance: " + QString::number(distance));
debug("Current Distance Calculated: " + QString::number(DistanceCalculated));
currentSpeed = speed;
currentIncline = incline;
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
Speed = speed;
Inclination = incline;
KCal = kcal;
Distance = distance;
lastTime = QDateTime::currentDateTime();
first = false;
}
double domyostreadmill::GetSpeedFromPacket(QByteArray packet)
@@ -230,94 +406,157 @@ double domyostreadmill::GetSpeedFromPacket(QByteArray packet)
return data;
}
double domyostreadmill::GetKcalFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(10) << 8) | packet.at(11);
return (double)convertedData;
}
double domyostreadmill::GetDistanceFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(12) << 8) | packet.at(13);
double data = ((double)convertedData) / 10.0f;
return data;
}
double domyostreadmill::GetInclinationFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(2) << 8) | packet.at(3);
qDebug() << convertedData;
double data = ((double)convertedData - 1000.0f) / 10.0f;
if (data < 0) return 0;
return data;
}
void domyostreadmill::btinit(bool startTape)
{
writeCharacteristic(initData1, sizeof(initData1), "init", false, true);
writeCharacteristic(initData2, sizeof(initData2), "init", false, true);
writeCharacteristic(initDataStart, sizeof(initDataStart), "init", false, true);
writeCharacteristic(initDataStart2, sizeof(initDataStart2), "init", false, true);
writeCharacteristic(initDataStart3, sizeof(initDataStart3), "init", false, true);
writeCharacteristic(initDataStart4, sizeof(initDataStart4), "init", false, true);
writeCharacteristic(initDataStart5, sizeof(initDataStart5), "init", false, true);
writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init", false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init", false, true);
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init", false, false);
if(startTape)
{
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init", false, true);
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init", false, false);
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init", false, true);
}
initDone = true;
}
void domyostreadmill::stateChanged(QLowEnergyService::ServiceState state)
{
qDebug() << "stateChanged" << state;
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
debug("BTLE stateChanged " + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
if(state == QLowEnergyService::ServiceDiscovered)
{
//qDebug() << gattCommunicationChannelService->characteristics();
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotifyCharacteristic.isValid());
// establish hook into notifications
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
this, SLOT(characteristicChanged(QLowEnergyCharacteristic,QByteArray)));
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)),
this, SLOT(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)));
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)),
this, SLOT(errorService(QLowEnergyService::ServiceError)));
connect(gattCommunicationChannelService, SIGNAL(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)), this,
SLOT(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)));
// ******************************************* virtual treadmill init *************************************
static uint8_t first = 0;
if(!first)
{
debug("creating virtual treadmill interface...");
virtualTreadMill = new virtualtreadmill(this, noHeartService);
connect(virtualTreadMill,&virtualtreadmill::debug ,this,&domyostreadmill::debug);
}
first = 1;
// ********************************************************************************************************
// await _gattNotifyCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattCommunicationChannelService->writeDescriptor(gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initData1, sizeof(initData1)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initData2, sizeof(initData2)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart, sizeof(initDataStart)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart2, sizeof(initDataStart2)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart3, sizeof(initDataStart3)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart4, sizeof(initDataStart4)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart5, sizeof(initDataStart5)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart6, sizeof(initDataStart6)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart7, sizeof(initDataStart7)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart8, sizeof(initDataStart8)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart9, sizeof(initDataStart9)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart10, sizeof(initDataStart10)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart11, sizeof(initDataStart11)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart12, sizeof(initDataStart12)));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart13, sizeof(initDataStart13)));
initDone = true;
}
}
void domyostreadmill::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
{
debug("descriptorWritten " + descriptor.name() + " " + newValue.toHex(' '));
initRequest = true;
}
void domyostreadmill::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
Q_UNUSED(characteristic);
debug("characteristicWritten " + newValue.toHex(' '));
}
void domyostreadmill::serviceScanDone(void)
{
qDebug() << "serviceScanDone";
debug("serviceScanDone");
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
connect(gattCommunicationChannelService, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(stateChanged(QLowEnergyService::ServiceState)));
gattCommunicationChannelService->discoverDetails();
}
void domyostreadmill::errorService(QLowEnergyService::ServiceError err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
debug("domyostreadmill::errorService " + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
}
void domyostreadmill::error(QLowEnergyController::Error err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
debug("domyostreadmill::error " + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
m_control->disconnect();
}
void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
qDebug() << "Found new device:" << device.name() << '(' << device.address().toString() << ')';
if(device.name().startsWith("Domyos"))
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("Domyos") && !device.name().startsWith("DomyosBridge"))
{
discoveryAgent->stop();
treadmill = device;
m_control = QLowEnergyController::createCentral(treadmill, this);
bttreadmill = device;
m_control = QLowEnergyController::createCentral(bttreadmill, 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, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, [this](QLowEnergyController::Error error) {
Q_UNUSED(error);
Q_UNUSED(this);
qDebug() << "Cannot connect to remote device.";
exit(1);
debug("Cannot connect to remote device.");
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
qDebug() << "Controller connected. Search services...";
debug("Controller connected. Search services...");
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
qDebug() << "LowEnergy controller disconnected";
exit(2);
debug("LowEnergy controller disconnected");
emit disconnected();
});
// Connect
@@ -325,3 +564,25 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
return;
}
}
bool domyostreadmill::connected()
{
if(!m_control)
return false;
return m_control->state() == QLowEnergyController::DiscoveredState;
}
void* domyostreadmill::VirtualTreadMill()
{
return virtualTreadMill;
}
void* domyostreadmill::VirtualDevice()
{
return VirtualTreadMill();
}
double domyostreadmill::odometer()
{
return DistanceCalculated;
}

View File

@@ -18,33 +18,65 @@
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QtCore/qmutex.h>
#include <QObject>
class domyostreadmill : QObject
#include "virtualtreadmill.h"
#include "treadmill.h"
class domyostreadmill : public treadmill
{
Q_OBJECT
public:
domyostreadmill();
domyostreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false);
bool connected();
bool changeFanSpeed(uint8_t speed);
double odometer();
void* VirtualTreadMill();
void* VirtualDevice();
private:
double GetSpeedFromPacket(QByteArray packet);
double GetInclinationFromPacket(QByteArray packet);
void forceSpeedOrIncline(double requestSpeed, double requestIncline, uint16_t elapsed);
double GetKcalFromPacket(QByteArray packet);
double GetDistanceFromPacket(QByteArray packet);
void forceSpeedOrIncline(double requestSpeed, double requestIncline);
void updateDisplay(uint16_t elapsed);
void btinit(bool startTape);
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log = false, bool wait_for_response = false);
void startDiscover();
double DistanceCalculated = 0;
volatile bool incompletePackets = false;
bool noConsole = false;
bool noHeartService = false;
uint32_t pollDeviceTime = 200;
QTimer* refresh;
virtualtreadmill* virtualTreadMill = 0;
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 stateChanged(QLowEnergyService::ServiceState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void deviceDiscovered(const QBluetoothDeviceInfo &device);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};
#endif // DOMYOSTREADMILL_H

49
src/gpx.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "gpx.h"
#include <QDomDocument>
#include <QDebug>
#include "math.h"
gpx::gpx(QObject *parent) : QObject(parent)
{
}
QList<gpx_altitude_point_for_treadmill> gpx::open(QString gpx)
{
QFile input(gpx);
input.open(QIODevice::ReadOnly);
QDomDocument doc;
doc.setContent(&input);
QDomNodeList points = doc.elementsByTagName("trkpt");
for (int i = 0; i < points.size(); i++)
{
QDomNode point = points.item(i);
QDomNamedNodeMap att = point.attributes();
QString lat = att.namedItem("lat").nodeValue();
QString lon = att.namedItem("lon").nodeValue();
QDomElement ele = point.firstChildElement("ele");
QDomElement time = point.firstChildElement("time");
gpx_point g;
//2020-10-10T10:54:45
g.time = QDateTime::fromString(time.text(), Qt::ISODate);
g.p.setAltitude(ele.text().toFloat());
g.p.setLatitude(lat.toFloat());
g.p.setLongitude(lon.toFloat());
this->points.append(g);
}
const uint8_t secondsInclination = 60;
QList<gpx_altitude_point_for_treadmill> inclinationList;
for(int32_t i=secondsInclination; i<this->points.count(); i+=secondsInclination)
{
double distance = this->points[i].p.distanceTo(this->points[i-secondsInclination].p);
double elevation = this->points[i].p.altitude() - this->points[i-secondsInclination].p.altitude();
gpx_altitude_point_for_treadmill g;
g.seconds = secondsInclination;
g.speed = (distance / 1000.0) * (3600 / secondsInclination);
g.inclination = (elevation / distance) * 100;
inclinationList.append(g);
}
return inclinationList;
}

38
src/gpx.h Normal file
View File

@@ -0,0 +1,38 @@
#ifndef GPX_H
#define GPX_H
#include <QObject>
#include <QFile>
#include <QTime>
#include <QGeoCoordinate>
class gpx_altitude_point_for_treadmill
{
public:
uint32_t seconds;
float inclination;
float speed;
};
class gpx_point
{
public:
QDateTime time;
QGeoCoordinate p;
};
class gpx : public QObject
{
Q_OBJECT
public:
explicit gpx(QObject *parent = nullptr);
QList<gpx_altitude_point_for_treadmill> open(QString gpx);
private:
QList<gpx_point> points;
signals:
};
#endif // GPX_H

252
src/homeform.cpp Normal file
View File

@@ -0,0 +1,252 @@
#include "homeform.h"
#include <QQmlContext>
#include <QTime>
DataObject::DataObject(QString name, QString icon, QString value, bool writable, QString id)
{
m_name = name;
m_icon = icon;
m_value = value;
m_writable = writable;
m_id = id;
emit plusNameChanged(plusName());
emit minusNameChanged(minusName());
}
void DataObject::setValue(QString v) {m_value = v; emit valueChanged(m_value);}
homeform::homeform(QQmlApplicationEngine* engine, bluetooth* bl)
{
this->bluetoothManager = bl;
connect(bluetoothManager, SIGNAL(deviceFound(QString)), this, SLOT(deviceFound(QString)));
engine->rootContext()->setContextProperty("rootItem", (QObject *)this);
dataList = {
speed,
inclination,
cadence,
elevation,
calories,
odometer,
pace,
resistance,
watt,
heart,
fan
};
engine->rootContext()->setContextProperty("appModel", QVariant::fromValue(dataList));
QObject *rootObject = engine->rootObjects().first();
QObject *home = rootObject->findChild<QObject*>("home");
QObject::connect(home, SIGNAL(start_clicked()),
this, SLOT(Start()));
QObject::connect(home, SIGNAL(stop_clicked()),
this, SLOT(Stop()));
QObject::connect(home, SIGNAL(plus_clicked(QString)),
this, SLOT(Plus(QString)));
QObject::connect(home, SIGNAL(minus_clicked(QString)),
this, SLOT(Minus(QString)));
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &homeform::update);
timer->start(1000);
}
void homeform::deviceFound(QString name)
{
m_info = name + " founded";
emit infoChanged(m_info);
}
void homeform::Plus(QString name)
{
if(name.contains("speed"))
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeSpeed(((treadmill*)bluetoothManager->device())->currentSpeed() + 0.5);
}
}
}
else if(name.contains("inclination"))
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeInclination(((treadmill*)bluetoothManager->device())->currentInclination() + 0.5);
}
}
}
else if(name.contains("resistance"))
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
((bike*)bluetoothManager->device())->changeResistance(((bike*)bluetoothManager->device())->currentResistance() + 1);
}
}
}
else if(name.contains("fan"))
{
if(bluetoothManager->device())
bluetoothManager->device()->changeFanSpeed(bluetoothManager->device()->fanSpeed() + 1);
}
else
{
qDebug() << name << "not handled";
}
}
void homeform::Minus(QString name)
{
if(name.contains("speed"))
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeSpeed(((treadmill*)bluetoothManager->device())->currentSpeed() - 0.5);
}
}
}
else if(name.contains("inclination"))
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeInclination(((treadmill*)bluetoothManager->device())->currentInclination() - 0.5);
}
}
}
else if(name.contains("resistance"))
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
((bike*)bluetoothManager->device())->changeResistance(((bike*)bluetoothManager->device())->currentResistance() - 1);
}
}
}
else if(name.contains("fan"))
{
if(bluetoothManager->device())
bluetoothManager->device()->changeFanSpeed(bluetoothManager->device()->fanSpeed() - 1);
}
else
{
qDebug() << name << "not handled";
}
}
void homeform::Start()
{
//trainProgram->restart();
if(bluetoothManager->device())
bluetoothManager->device()->start();
}
void homeform::Stop()
{
if(bluetoothManager->device())
bluetoothManager->device()->stop();
}
void homeform::update()
{
if(bluetoothManager->device())
{
double inclination = 0;
double resistance = 0;
double watts = 0;
double pace = 0;
speed->setValue(QString::number(bluetoothManager->device()->currentSpeed(), 'f', 2));
heart->setValue(QString::number(bluetoothManager->device()->currentHeart()));
odometer->setValue(QString::number(bluetoothManager->device()->odometer(), 'f', 2));
calories->setValue(QString::number(bluetoothManager->device()->calories(), 'f', 0));
fan->setValue(QString::number(bluetoothManager->device()->fanSpeed()));
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
pace = 10000 / (((treadmill*)bluetoothManager->device())->currentPace().second() + (((treadmill*)bluetoothManager->device())->currentPace().minute() * 60));
if(pace < 0) pace = 0;
watts = ((treadmill*)bluetoothManager->device())->watts(/*weight->text().toFloat()*/); // TODO: add weight to settings
inclination = ((treadmill*)bluetoothManager->device())->currentInclination();
this->pace->setValue(((treadmill*)bluetoothManager->device())->currentPace().toString("m:ss"));
watt->setValue(QString::number(watts, 'f', 0));
this->inclination->setValue(QString::number(inclination, 'f', 1));
elevation->setValue(QString::number(((treadmill*)bluetoothManager->device())->elevationGain(), 'f', 1));
}
else if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
resistance = ((bike*)bluetoothManager->device())->currentResistance();
watts = ((bike*)bluetoothManager->device())->watts();
watt->setValue(QString::number(watts));
this->resistance->setValue(QString::number(resistance));
}
/*
if(trainProgram)
{
trainProgramElapsedTime->setText(trainProgram->totalElapsedTime().toString("hh:mm:ss"));
trainProgramCurrentRowElapsedTime->setText(trainProgram->currentRowElapsedTime().toString("hh:mm:ss"));
trainProgramDuration->setText(trainProgram->duration().toString("hh:mm:ss"));
double distance = trainProgram->totalDistance();
if(distance > 0)
{
trainProgramTotalDistance->setText(QString::number(distance));
}
else
trainProgramTotalDistance->setText("N/A");
}
*/
SessionLine s(
bluetoothManager->device()->currentSpeed(),
inclination,
bluetoothManager->device()->odometer(),
watts,
resistance,
bluetoothManager->device()->currentHeart(),
pace);
Session.append(s);
}
emit changeOfdevice();
emit changeOfzwift();
}
bool homeform::getDevice()
{
if(!this->bluetoothManager->device())
return false;
return this->bluetoothManager->device()->connected();
}
bool homeform::getZwift()
{
if(!this->bluetoothManager->device())
return false;
if(!this->bluetoothManager->device()->VirtualDevice())
return false;
if(this->bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL &&
((virtualtreadmill*)((treadmill*)bluetoothManager->device())->VirtualDevice())->connected())
{
return true;
}
else if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE &&
((virtualbike*)((bike*)bluetoothManager->device())->VirtualDevice())->connected())
{
return true;
}
return false;
}

94
src/homeform.h Normal file
View File

@@ -0,0 +1,94 @@
#ifndef HOMEFORM_H
#define HOMEFORM_H
#include <QQuickItem>
#include <QQmlApplicationEngine>
#include "bluetooth.h"
#include "sessionline.h"
class DataObject : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged)
Q_PROPERTY(QString value READ value WRITE setValue NOTIFY valueChanged)
Q_PROPERTY(bool writable READ writable NOTIFY writableChanged)
Q_PROPERTY(QString plusName READ plusName NOTIFY plusNameChanged)
Q_PROPERTY(QString minusName READ minusName NOTIFY minusNameChanged)
public:
DataObject(QString name, QString icon, QString value, bool writable, QString id);
void setValue(QString value);
QString name() {return m_name;}
QString icon() {return m_icon;}
QString value() {return m_value;}
bool writable() {return m_writable;}
QString plusName() {return m_id + "_plus";}
QString minusName() {return m_id + "_minus";}
QString m_id;
QString m_name;
QString m_icon;
QString m_value;
bool m_writable;
signals:
void valueChanged(QString value);
void nameChanged(QString value);
void iconChanged(QString value);
void writableChanged(bool value);
void plusNameChanged(QString value);
void minusNameChanged(QString value);
};
class homeform: public QObject
{
Q_OBJECT
Q_PROPERTY( bool device READ getDevice NOTIFY changeOfdevice)
Q_PROPERTY( bool zwift READ getZwift NOTIFY changeOfzwift)
Q_PROPERTY(QString info READ info NOTIFY infoChanged)
public:
homeform(QQmlApplicationEngine* engine, bluetooth* bl);
QString info() {return m_info;}
private:
QList<QObject *> dataList;
QList<SessionLine> Session;
bluetooth* bluetoothManager;
QString m_info = "Connecting...";
DataObject* speed = new DataObject("Speed (km/h)", "icons/icons/speed.png", "0.0", true, "speed");
DataObject* inclination = new DataObject("Inclination (%)", "icons/icons/inclination.png", "0.0", true, "inclination");
DataObject* cadence = new DataObject("Cadence (bpm)", "icons/icons/cadence.png", "0", false, "cadence");
DataObject* elevation = new DataObject("Elev. Gain (m)", "icons/icons/elevationgain.png", "0", false, "elevation");
DataObject* calories = new DataObject("Calories (KCal)", "icons/icons/kcal.png", "0", false, "calories");
DataObject* odometer = new DataObject("Odometer (km)", "icons/icons/odometer.png", "0.0", false, "odometer");
DataObject* pace = new DataObject("Pace (m/km)", "icons/icons/pace.png", "0:00", false, "pace");
DataObject* resistance = new DataObject("Resistance (%)", "icons/icons/resistance.png", "0", true, "resistance");
DataObject* watt = new DataObject("Watt", "icons/icons/watt.png", "0", false, "watt");
DataObject* heart = new DataObject("Heart (bpm)", "icons/icons/heart_red.png", "0", false, "heart");
DataObject* fan = new DataObject("Fan Speed", "icons/icons/fan.png", "0", true, "fan");
QTimer* timer;
void update();
bool getDevice();
bool getZwift();
private slots:
void Start();
void Stop();
void Minus(QString);
void Plus(QString);
void deviceFound(QString name);
signals:
void changeOfdevice();
void changeOfzwift();
void infoChanged(QString value);
};
#endif // HOMEFORM_H

22
src/icons.qrc Normal file
View File

@@ -0,0 +1,22 @@
<RCC>
<qresource prefix="/icons">
<file>icons/bluetooth-icon.png</file>
<file>icons/zwift-on.png</file>
<file>icons/fan.png</file>
<file>icons/speed.png</file>
<file>icons/inclination.png</file>
<file>icons/heart_red.png</file>
<file>icons/watt.png</file>
<file>icons/odometer.png</file>
<file>icons/elevationgain.png</file>
<file>icons/kcal.png</file>
<file>icons/weight.png</file>
<file>icons/start.png</file>
<file>icons/stop.png</file>
<file>icons/cadence.png</file>
<file>icons/resistance.png</file>
<file>icons/pace.png</file>
<file>icons/chart.png</file>
<file>icons/icon.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
src/icons/cadence.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
src/icons/chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
src/icons/elevationgain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/icons/fan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/icons/heart_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
src/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
src/icons/icon.xcf Normal file

Binary file not shown.

BIN
src/icons/inclination.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/icons/kcal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
src/icons/odometer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

BIN
src/icons/pace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
src/icons/resistance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
src/icons/speed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/icons/start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/icons/stop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/icons/watt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
src/icons/weight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/icons/zwift-on.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,21 +1,191 @@
#include <QtCore/qcoreapplication.h>
#include <QApplication>
#include <QStyleFactory>
#include <stdio.h>
#include <stdlib.h>
#include <QStandardPaths>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "virtualtreadmill.h"
#include "domyostreadmill.h"
#include "bluetooth.h"
#include "mainwindow.h"
#include "homeform.h"
bool nologs = false;
bool noWriteResistance = false;
bool noHeartService = true;
bool noConsole = false;
bool onlyVirtualBike = false;
bool onlyVirtualTreadmill = false;
bool testResistance = false;
QString trainProgram;
QString deviceName = "";
uint32_t pollDeviceTime = 200;
static QString logfilename = "debug-" + QDateTime::currentDateTime().toString().replace(":", "_") + ".log";
QCoreApplication* createApplication(int &argc, char *argv[])
{
bool nogui = false;
for (int i = 1; i < argc; ++i) {
if (!qstrcmp(argv[i], "-no-gui"))
nogui = true;
if (!qstrcmp(argv[i], "-no-console"))
noConsole = true;
if (!qstrcmp(argv[i], "-test-resistance"))
testResistance = true;
if (!qstrcmp(argv[i], "-no-log"))
nologs = true;
if (!qstrcmp(argv[i], "-no-write-resistance"))
noWriteResistance = true;
if (!qstrcmp(argv[i], "-no-heart-service"))
noHeartService = true;
if (!qstrcmp(argv[i], "-heart-service"))
noHeartService = false;
if (!qstrcmp(argv[i], "-only-virtualbike"))
onlyVirtualBike = true;
if (!qstrcmp(argv[i], "-only-virtualtreadmill"))
onlyVirtualTreadmill = true;
if (!qstrcmp(argv[i], "-train"))
{
trainProgram = argv[++i];
}
if (!qstrcmp(argv[i], "-name"))
{
deviceName = argv[++i];
}
if (!qstrcmp(argv[i], "-poll-device-time"))
{
pollDeviceTime = atol(argv[++i]);
}
}
if(nogui)
return new QCoreApplication(argc, argv);
else
{
QApplication* a = new QApplication(argc, argv);
a->setStyle(QStyleFactory::create("Fusion"));
/*QFont defaultFont = QApplication::font();
defaultFont.setPointSize(defaultFont.pointSize()+2);
qApp->setFont(defaultFont);*/
// modify palette to dark
QPalette darkPalette;
darkPalette.setColor(QPalette::Window,QColor(53,53,53));
darkPalette.setColor(QPalette::WindowText,Qt::white);
darkPalette.setColor(QPalette::Disabled,QPalette::WindowText,QColor(127,127,127));
darkPalette.setColor(QPalette::Base,QColor(42,42,42));
darkPalette.setColor(QPalette::AlternateBase,QColor(66,66,66));
darkPalette.setColor(QPalette::ToolTipBase,Qt::white);
darkPalette.setColor(QPalette::ToolTipText,Qt::white);
darkPalette.setColor(QPalette::Text,Qt::white);
darkPalette.setColor(QPalette::Disabled,QPalette::Text,QColor(127,127,127));
darkPalette.setColor(QPalette::Dark,QColor(35,35,35));
darkPalette.setColor(QPalette::Shadow,QColor(20,20,20));
darkPalette.setColor(QPalette::Button,QColor(53,53,53));
darkPalette.setColor(QPalette::ButtonText,Qt::white);
darkPalette.setColor(QPalette::Disabled,QPalette::ButtonText,QColor(127,127,127));
darkPalette.setColor(QPalette::BrightText,Qt::red);
darkPalette.setColor(QPalette::Link,QColor(42,130,218));
darkPalette.setColor(QPalette::Highlight,QColor(42,130,218));
darkPalette.setColor(QPalette::Disabled,QPalette::Highlight,QColor(80,80,80));
darkPalette.setColor(QPalette::HighlightedText,Qt::white);
darkPalette.setColor(QPalette::Disabled,QPalette::HighlightedText,QColor(127,127,127));
qApp->setPalette(darkPalette);
return a;
}
}
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
QByteArray localMsg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
QString txt;
switch (type) {
case QtInfoMsg:
txt = QString("Info: %1 %2 %3\n").arg(file).arg(function).arg(msg);
break;
case QtDebugMsg:
txt = QString("Debug: %1 %2 %3\n").arg(file).arg(function).arg(msg);
break;
case QtWarningMsg:
txt = QString("Warning: %1 %2 %3\n").arg(file).arg(function).arg(msg);
break;
case QtCriticalMsg:
txt = QString("Critical: %1 %2 %3\n").arg(file).arg(function).arg(msg);
break;
case QtFatalMsg:
txt = QString("Fatal: %1 %2 %3\n").arg(file).arg(function).arg(msg);
abort();
}
if(nologs == false)
{
QString path = "";
#ifdef Q_OS_ANDROID
path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/";
#endif
QFile outFile(path + logfilename);
outFile.open(QIODevice::WriteOnly | QIODevice::Append);
QTextStream ts(&outFile);
ts << txt;
fprintf(stderr, txt.toLocal8Bit());
}
}
int main(int argc, char *argv[])
{
//QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
#ifndef Q_OS_ANDROID
QCoreApplication app(argc, argv);
#else
QGuiApplication app(argc, argv);
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
qInstallMessageHandler(myMessageOutput);
if(onlyVirtualBike)
{
virtualbike* V = new virtualbike(new bike(), noWriteResistance, noHeartService);
return app->exec();
}
else if(onlyVirtualTreadmill)
{
virtualtreadmill* V = new virtualtreadmill(new treadmill(), noHeartService);
return app->exec();
}
#endif
bluetooth* bl = new bluetooth(!nologs, deviceName, noWriteResistance, noHeartService, pollDeviceTime, testResistance);
//virtualtreadmill* V = new virtualtreadmill();
domyostreadmill* D = new domyostreadmill();
//Q_UNUSED(V);
Q_UNUSED(D);
#ifdef Q_OS_ANDROID
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
new homeform(&engine, bl);
return app.exec();
#else
if (qobject_cast<QApplication *>(app.data())) {
// start GUI version...
MainWindow* W = 0;
if(trainProgram.isEmpty())
W = new MainWindow(bl);
else
W = new MainWindow(bl, trainProgram);
W->show();
} else {
// start non-GUI version...
}
return app->exec();
#endif
}

67
src/main.qml Normal file
View File

@@ -0,0 +1,67 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
ApplicationWindow {
id: window
width: 640
height: 480
visible: true
title: qsTr("Stack")
header: ToolBar {
contentHeight: toolButton.implicitHeight
ToolButton {
id: toolButton
icon.source: "icons/icons/icon.png"
text: stackView.depth > 1 ? "\u25C0" : "\u2630"
font.pixelSize: Qt.application.font.pixelSize * 1.6
onClicked: {
if (stackView.depth > 1) {
stackView.pop()
} else {
drawer.open()
}
}
}
Label {
text: stackView.currentItem.title
anchors.centerIn: parent
}
}
Drawer {
id: drawer
width: window.width * 0.66
height: window.height
Column {
anchors.fill: parent
ItemDelegate {
text: qsTr("by Roberto Viola")
width: parent.width
/* onClicked: {
stackView.push("Page1Form.ui.qml")
drawer.close()
}*/
}
/* ItemDelegate {
text: qsTr("Page 2")
width: parent.width
onClicked: {
stackView.push("Page2Form.ui.qml")
drawer.close()
}
}*/
}
}
StackView {
id: stackView
initialItem: "Home.qml"
anchors.fill: parent
}
}

540
src/mainwindow.cpp Normal file
View File

@@ -0,0 +1,540 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include "gpx.h"
#include "charts.h"
charts* Charts = 0;
void MainWindow::load(bluetooth* b)
{
ui->setupUi(this);
this->bluetoothManager = b;
connect(this->bluetoothManager, SIGNAL(deviceConnected()), this, SLOT(trainProgramSignals()));
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &MainWindow::update);
timer->start(1000);
#ifdef Q_OS_ANDROID
ui->groupTrain->setVisible(false);
ui->fanBar->setVisible(false);
ui->fanSpeedMinus->setVisible(false);
ui->fanSpeedPlus->setVisible(false);
ui->weight->setVisible(false);
ui->weightDescription->setVisible(false);
ui->weightIcon->setVisible(false);
ui->load->setVisible(false);
ui->reset->setVisible(false);
ui->save->setVisible(false);
#endif
update();
}
MainWindow::MainWindow(bluetooth* b) :
QDialog(nullptr),
ui(new Ui::MainWindow)
{
load(b);
this->trainProgram = new trainprogram(QList<trainrow>(), b);
}
MainWindow::MainWindow(bluetooth* b, QString trainProgram) :
QDialog(nullptr),
ui(new Ui::MainWindow)
{
load(b);
loadTrainProgram(trainProgram);
}
void MainWindow::update()
{
if(bluetoothManager->device())
{
double inclination = 0;
double resistance = 0;
double watts = 0;
double pace = 0;
ui->speed->setText(QString::number(bluetoothManager->device()->currentSpeed(), 'f', 2));
ui->heartrate->setText(QString::number(bluetoothManager->device()->currentHeart()));
ui->odometer->setText(QString::number(bluetoothManager->device()->odometer(), 'f', 2));
ui->calories->setText(QString::number(bluetoothManager->device()->calories(), 'f', 0));
ui->fanBar->setValue(bluetoothManager->device()->fanSpeed());
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
pace = 10000 / (((treadmill*)bluetoothManager->device())->currentPace().second() + (((treadmill*)bluetoothManager->device())->currentPace().minute() * 60));
if(pace < 0) pace = 0;
watts = ((treadmill*)bluetoothManager->device())->watts(ui->weight->text().toFloat());
inclination = ((treadmill*)bluetoothManager->device())->currentInclination();
ui->pace->setText(((treadmill*)bluetoothManager->device())->currentPace().toString("m:ss"));
ui->watt->setText(QString::number(watts, 'f', 0));
ui->inclination->setText(QString::number(inclination, 'f', 1));
ui->elevationGain->setText(QString::number(((treadmill*)bluetoothManager->device())->elevationGain(), 'f', 1));
}
else if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
resistance = ((bike*)bluetoothManager->device())->currentResistance();
watts = ((bike*)bluetoothManager->device())->watts();
ui->watt->setText(QString::number(watts));
ui->resistance->setText(QString::number(resistance));
}
if(trainProgram)
{
ui->trainProgramElapsedTime->setText(trainProgram->totalElapsedTime().toString("hh:mm:ss"));
ui->trainProgramCurrentRowElapsedTime->setText(trainProgram->currentRowElapsedTime().toString("hh:mm:ss"));
ui->trainProgramDuration->setText(trainProgram->duration().toString("hh:mm:ss"));
double distance = trainProgram->totalDistance();
if(distance > 0)
{
ui->trainProgramTotalDistance->setText(QString::number(distance));
}
else
ui->trainProgramTotalDistance->setText("N/A");
}
if(bluetoothManager->device()->connected())
{
ui->connectionToTreadmill->setEnabled(true);
if(bluetoothManager->device()->VirtualDevice())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL &&
((virtualtreadmill*)((treadmill*)bluetoothManager->device())->VirtualDevice())->connected())
{
ui->connectionToZwift->setEnabled(true);
}
else if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE &&
((virtualbike*)((bike*)bluetoothManager->device())->VirtualDevice())->connected())
{
ui->connectionToZwift->setEnabled(true);
}
else
ui->connectionToZwift->setEnabled(false);
}
else
ui->connectionToZwift->setEnabled(false);
}
else
ui->connectionToTreadmill->setEnabled(false);
SessionLine s(
bluetoothManager->device()->currentSpeed(),
inclination,
bluetoothManager->device()->odometer(),
watts,
resistance,
bluetoothManager->device()->currentHeart(),
pace);
Session.append(s);
if(ui->chart->isChecked())
{
if(!Charts)
{
Charts = new charts(this);
Charts->show();
}
Charts->update();
}
}
else
{
ui->connectionToTreadmill->setEnabled(false);
ui->connectionToZwift->setEnabled(false);
/*
* DEBUG CHARTS
*
if(!Charts)
{
Charts = new charts(this);
Charts->show();
}
SessionLine s(
(double)QRandomGenerator::global()->bounded(22),
QRandomGenerator::global()->bounded(15),
(double)QRandomGenerator::global()->bounded(15),
QRandomGenerator::global()->bounded(150),
0,
QRandomGenerator::global()->bounded(180));
Session.append(s);
Charts->update();*/
}
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::addEmptyRow()
{
int row = ui->tableWidget->rowCount();
editing = true;
ui->tableWidget->insertRow(row);
ui->tableWidget->setItem(row, 0, new QTableWidgetItem("00:00:00"));
ui->tableWidget->setItem(row, 1, new QTableWidgetItem("10"));
ui->tableWidget->setItem(row, 2, new QTableWidgetItem("0"));
ui->tableWidget->setItem(row, 3, new QTableWidgetItem(""));
ui->tableWidget->item(row, 0)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidget->item(row, 1)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidget->item(row, 2)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidget->item(row, 3)->setCheckState(Qt::CheckState::Checked);
editing = false;
}
void MainWindow::on_tableWidget_cellChanged(int row, int column)
{
if(editing) return;
if(column == 0)
{
switch(ui->tableWidget->currentItem()->text().length())
{
case 4:
ui->tableWidget->currentItem()->setText("00:0" + ui->tableWidget->currentItem()->text());
break;
case 5:
ui->tableWidget->currentItem()->setText("00:" + ui->tableWidget->currentItem()->text());
break;
case 7:
ui->tableWidget->currentItem()->setText("0" + ui->tableWidget->currentItem()->text());
break;
}
QString fmt = "hh:mm:ss";
QTime dt = QTime::fromString(ui->tableWidget->currentItem()->text());
QString timeStr = dt.toString("hh:mm:ss");
ui->tableWidget->currentItem()->setText(timeStr);
}
if(row + 1 == ui->tableWidget->rowCount() && ui->tableWidget->currentItem()->text().length() )
addEmptyRow();
QList<trainrow> rows;
for(int i = 0; i < ui->tableWidget->rowCount(); i++)
{
if(!ui->tableWidget->item(i, 0)->text().contains("00:00:00"))
{
trainrow t;
t.duration = QTime::fromString(ui->tableWidget->item(i, 0)->text(), "hh:mm:ss");
t.speed = ui->tableWidget->item(i, 1)->text().toFloat();
t.inclination = ui->tableWidget->item(i, 2)->text().toFloat();
t.forcespeed = ui->tableWidget->item(i, 3)->checkState() == Qt::CheckState::Checked;
rows.append(t);
}
else
{
break;
}
createTrainProgram(rows);
}
}
void MainWindow::trainProgramSignals()
{
if(bluetoothManager->device())
{
disconnect(trainProgram, SIGNAL(start()), bluetoothManager->device(), SLOT(start()));
disconnect(trainProgram, SIGNAL(stop()), bluetoothManager->device(), SLOT(stop()));
disconnect(trainProgram, SIGNAL(changeSpeed(double)), ((treadmill*)bluetoothManager->device()), SLOT(changeSpeed(double)));
disconnect(trainProgram, SIGNAL(changeInclination(double)), ((treadmill*)bluetoothManager->device()), SLOT(changeInclination(double)));
disconnect(trainProgram, SIGNAL(changeSpeedAndInclination(double, double)), ((treadmill*)bluetoothManager->device()), SLOT(changeSpeedAndInclination(double, double)));
disconnect(trainProgram, SIGNAL(changeResistance(double)), ((bike*)bluetoothManager->device()), SLOT(changeResistance(double)));
disconnect(((treadmill*)bluetoothManager->device()), SIGNAL(tapeStarted()), trainProgram, SLOT(onTapeStarted()));
disconnect(((bike*)bluetoothManager->device()), SIGNAL(bikeStarted()), trainProgram, SLOT(onTapeStarted()));
connect(trainProgram, SIGNAL(start()), bluetoothManager->device(), SLOT(start()));
connect(trainProgram, SIGNAL(stop()), bluetoothManager->device(), SLOT(stop()));
connect(trainProgram, SIGNAL(changeSpeed(double)), ((treadmill*)bluetoothManager->device()), SLOT(changeSpeed(double)));
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(double)), ((bike*)bluetoothManager->device()), SLOT(changeResistance(double)));
connect(((treadmill*)bluetoothManager->device()), SIGNAL(tapeStarted()), trainProgram, SLOT(onTapeStarted()));
connect(((bike*)bluetoothManager->device()), SIGNAL(bikeStarted()), trainProgram, SLOT(onTapeStarted()));
qDebug() << "trainProgram associated to a device";
}
else
{
qDebug() << "trainProgram NOT associated to a device";
}
}
void MainWindow::createTrainProgram(QList<trainrow> rows)
{
if(trainProgram) delete trainProgram;
trainProgram = new trainprogram(rows, bluetoothManager);
if(rows.length() == 0)
addEmptyRow();
trainProgramSignals();
}
void MainWindow::on_tableWidget_currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous)
{
Q_UNUSED(current);
Q_UNUSED(previous);
}
void MainWindow::on_save_clicked()
{
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"),
"train.xml",
tr("Train Program (*.xml)"));
if(!fileName.isEmpty() && trainProgram)
trainProgram->save(fileName);
}
void MainWindow::loadTrainProgram(QString fileName)
{
if(!fileName.isEmpty())
{
ui->difficulty->setValue(50);
int rows = ui->tableWidget->rowCount();
for(int i = 0; i<rows; i++)
ui->tableWidget->removeRow(ui->tableWidget->rowCount() - 1);
if(fileName.endsWith("xml"))
{
if(trainProgram)
delete trainProgram;
trainProgram = trainprogram::load(fileName, bluetoothManager);
}
else if(fileName.endsWith("gpx"))
{
if(trainProgram)
delete trainProgram;
gpx g;
QList<trainrow> list;
foreach(gpx_altitude_point_for_treadmill p, g.open(fileName))
{
trainrow r;
r.speed = p.speed;
r.duration = QTime(0,0,0,0);
r.duration = r.duration.addSecs(p.seconds);
r.inclination = p.inclination;
r.forcespeed = true;
list.append(r);
}
trainProgram = new trainprogram(list, bluetoothManager);
}
else
{
return;
}
int countRow = 0;
foreach(trainrow row, trainProgram->rows)
{
if(ui->tableWidget->rowCount() <= countRow)
addEmptyRow();
QTableWidgetItem* i;
editing = true;
i = ui->tableWidget->takeItem(countRow, 0);
i->setText(row.duration.toString("hh:mm:ss"));
ui->tableWidget->setItem(countRow, 0, i);
i = ui->tableWidget->takeItem(countRow, 1);
i->setText(QString::number(row.speed));
ui->tableWidget->setItem(countRow, 1, i);
i = ui->tableWidget->takeItem(countRow, 2);
i->setText(QString::number(row.inclination));
ui->tableWidget->setItem(countRow, 2, i);
i = ui->tableWidget->takeItem(countRow, 3);
i->setCheckState(row.forcespeed?Qt::CheckState::Checked:Qt::CheckState::Unchecked);
ui->tableWidget->setItem(countRow, 3, i);
editing = false;
countRow++;
}
trainProgramSignals();
ui->groupTrain->setChecked(true);
}
}
void MainWindow::on_load_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),
"train.xml",
tr("Train Program (*.xml *.gpx)"));
loadTrainProgram(fileName);
}
void MainWindow::on_reset_clicked()
{
if(bluetoothManager->device() && bluetoothManager->device()->currentSpeed() > 0) return;
int countRow = 0;
foreach(trainrow row, trainProgram->rows)
{
QTableWidgetItem* i;
editing = true;
i = ui->tableWidget->takeItem(countRow, 0);
i->setText("00:00:00");
ui->tableWidget->setItem(countRow, 0, i);
i = ui->tableWidget->takeItem(countRow, 1);
i->setText("0");
ui->tableWidget->setItem(countRow, 1, i);
i = ui->tableWidget->takeItem(countRow, 2);
i->setText("0");
ui->tableWidget->setItem(countRow, 2, i);
i = ui->tableWidget->takeItem(countRow, 3);
i->setCheckState(row.forcespeed?Qt::CheckState::Checked:Qt::CheckState::Unchecked);
ui->tableWidget->setItem(countRow, 3, i);
editing = false;
countRow++;
}
createTrainProgram(QList<trainrow>());
}
void MainWindow::on_stop_clicked()
{
if(bluetoothManager->device())
bluetoothManager->device()->stop();
}
void MainWindow::on_start_clicked()
{
trainProgram->restart();
if(bluetoothManager->device())
bluetoothManager->device()->start();
}
void MainWindow::on_groupTrain_clicked()
{
if(!trainProgram)
createTrainProgram(QList<trainrow>());
trainProgram->enabled = ui->groupTrain->isChecked();
}
void MainWindow::on_fanSpeedMinus_clicked()
{
if(bluetoothManager->device())
bluetoothManager->device()->changeFanSpeed(bluetoothManager->device()->fanSpeed() - 1);
}
void MainWindow::on_fanSpeedPlus_clicked()
{
if(bluetoothManager->device())
bluetoothManager->device()->changeFanSpeed(bluetoothManager->device()->fanSpeed() + 1);
}
void MainWindow::on_difficulty_valueChanged(int value)
{
if(editing) return;
for(int i=0;i<trainProgram->rows.count(); i++)
{
trainProgram->rows[i].speed = trainProgram->loadedRows[i].speed +
(trainProgram->loadedRows[i].speed * (0.02 * (value - 50)));
trainProgram->rows[i].inclination = trainProgram->loadedRows[i].inclination +
(trainProgram->loadedRows[i].inclination * (0.02 * (value - 50)));
}
int countRow = 0;
foreach(trainrow row, trainProgram->rows)
{
QTableWidgetItem* i;
editing = true;
i = ui->tableWidget->takeItem(countRow, 1);
i->setText(QString::number(row.speed));
ui->tableWidget->setItem(countRow, 1, i);
i = ui->tableWidget->takeItem(countRow, 2);
i->setText(QString::number(row.inclination));
ui->tableWidget->setItem(countRow, 2, i);
editing = false;
countRow++;
}
ui->difficulty->setToolTip(QString::number(value) + "%");
}
void MainWindow::on_speedMinus_clicked()
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeSpeed(((treadmill*)bluetoothManager->device())->currentSpeed() - 0.5);
}
}
}
void MainWindow::on_speedPlus_clicked()
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeSpeed(((treadmill*)bluetoothManager->device())->currentSpeed() + 0.5);
}
}
}
void MainWindow::on_inclinationMinus_clicked()
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeInclination(((treadmill*)bluetoothManager->device())->currentInclination() - 0.5);
}
}
}
void MainWindow::on_inclinationPlus_clicked()
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
((treadmill*)bluetoothManager->device())->changeInclination(((treadmill*)bluetoothManager->device())->currentInclination() + 0.5);
}
}
}
void MainWindow::on_resistanceMinus_clicked()
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
((bike*)bluetoothManager->device())->changeResistance(((bike*)bluetoothManager->device())->currentResistance() - 1);
}
}
}
void MainWindow::on_resistancePlus_clicked()
{
if(bluetoothManager->device())
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
((bike*)bluetoothManager->device())->changeResistance(((bike*)bluetoothManager->device())->currentResistance() + 1);
}
}
}
void MainWindow::on_chart_clicked()
{
}

63
src/mainwindow.h Normal file
View File

@@ -0,0 +1,63 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QDialog>
#include <QTimer>
#include <QTime>
#include <QDebug>
#include <QTableWidgetItem>
#include "trainprogram.h"
#include "domyostreadmill.h"
#include "sessionline.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QDialog
{
Q_OBJECT
public:
QList<SessionLine> Session;
explicit MainWindow(bluetooth* t);
explicit MainWindow(bluetooth* t, QString trainProgram);
~MainWindow();
private:
void addEmptyRow();
void load(bluetooth* device);
void loadTrainProgram(QString fileName);
void createTrainProgram(QList<trainrow> rows);
bool editing = false;
trainprogram* trainProgram = 0;
Ui::MainWindow *ui;
QTimer *timer;
bluetooth* bluetoothManager;
private slots:
void update();
void on_tableWidget_cellChanged(int row, int column);
void on_tableWidget_currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous);
void on_save_clicked();
void on_load_clicked();
void on_reset_clicked();
void on_stop_clicked();
void on_start_clicked();
void on_groupTrain_clicked();
void on_fanSpeedMinus_clicked();
void on_fanSpeedPlus_clicked();
void on_difficulty_valueChanged(int value);
void trainProgramSignals();
void on_speedMinus_clicked();
void on_speedPlus_clicked();
void on_inclinationMinus_clicked();
void on_inclinationPlus_clicked();
void on_resistanceMinus_clicked();
void on_resistancePlus_clicked();
void on_chart_clicked();
};
#endif // MAINWINDOW_H

1182
src/mainwindow.ui Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
QT -= gui
QT += bluetooth xml
QT += bluetooth widgets xml positioning charts quick
unix:android: QT += androidextras
CONFIG += c++11 console
CONFIG -= app_bundle
CONFIG += c++11 console debug app_bundle
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
@@ -16,9 +15,23 @@ DEFINES += QT_DEPRECATED_WARNINGS
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
bike.cpp \
bluetooth.cpp \
bluetoothdevice.cpp \
charts.cpp \
domyostreadmill.cpp \
gpx.cpp \
homeform.cpp \
main.cpp \
virtualtreadmill.cpp
sessionline.cpp \
toorxtreadmill.cpp \
treadmill.cpp \
mainwindow.cpp \
trainprogram.cpp \
trxappgateusbtreadmill.cpp \
virtualbike.cpp \
virtualtreadmill.cpp \
domyosbike.cpp
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
@@ -26,5 +39,41 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
HEADERS += \
bike.h \
bluetooth.h \
bluetoothdevice.h \
charts.h \
domyostreadmill.h \
virtualtreadmill.h
homeform.h \
sessionline.h \
toorxtreadmill.h \
gpx.h \
treadmill.h \
mainwindow.h \
trainprogram.h \
trxappgateusbtreadmill.h \
virtualbike.h \
virtualtreadmill.h \
domyosbike.h
FORMS += \
charts.ui \
mainwindow.ui
RESOURCES += \
icons.qrc \
qml.qrc
DISTFILES += \
android/AndroidManifest.xml \
android/build.gradle \
android/gradle/wrapper/gradle-wrapper.jar \
android/gradle/wrapper/gradle-wrapper.properties \
android/gradlew \
android/gradlew.bat \
android/res/values/libs.xml \
android/src/MyActivity.java
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
ANDROID_ABIS = armeabi-v7a

25
src/qdomyos-zwift.sln Normal file
View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28307.271
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "qdomyos-zwift", "qdomyos-zwift.vcxproj", "{9D0092A7-A461-39F4-9B1F-4C5838A323A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9D0092A7-A461-39F4-9B1F-4C5838A323A6}.Debug|x64.ActiveCfg = Debug|x64
{9D0092A7-A461-39F4-9B1F-4C5838A323A6}.Debug|x64.Build.0 = Debug|x64
{9D0092A7-A461-39F4-9B1F-4C5838A323A6}.Release|x64.ActiveCfg = Release|x64
{9D0092A7-A461-39F4-9B1F-4C5838A323A6}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7EDA6C26-B7B8-4AFB-AD08-977E1DEF7E75}
EndGlobalSection
EndGlobal

298
src/qdomyos-zwift.vcxproj Normal file
View File

@@ -0,0 +1,298 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{9D0092A7-A461-39F4-9B1F-4C5838A323A6}</ProjectGuid>
<RootNamespace>qdomyos-zwift</RootNamespace>
<Keyword>QtVS_v303</Keyword>
<WindowsTargetPlatformVersion>10.0.17763.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.17763.0</WindowsTargetPlatformMinVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' or !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<PlatformToolset>v141</PlatformToolset>
<OutputDirectory>debug\</OutputDirectory>
<ATLMinimizesCRunTimeLibraryUsage>false</ATLMinimizesCRunTimeLibraryUsage>
<CharacterSet>NotSet</CharacterSet>
<ConfigurationType>Application</ConfigurationType>
<IntermediateDirectory>debug\</IntermediateDirectory>
<PrimaryOutput>qdomyos-zwift</PrimaryOutput>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<PlatformToolset>v140</PlatformToolset>
<OutputDirectory>release\</OutputDirectory>
<ATLMinimizesCRunTimeLibraryUsage>false</ATLMinimizesCRunTimeLibraryUsage>
<CharacterSet>NotSet</CharacterSet>
<ConfigurationType>Application</ConfigurationType>
<IntermediateDirectory>release\</IntermediateDirectory>
<PrimaryOutput>qdomyos-zwift</PrimaryOutput>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>release\</OutDir>
<IntDir>release\</IntDir>
<TargetName>qdomyos-zwift</TargetName>
<IgnoreImportLibrary>true</IgnoreImportLibrary>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>debug\</OutDir>
<IntDir>debug\</IntDir>
<TargetName>qdomyos-zwift</TargetName>
<IgnoreImportLibrary>true</IgnoreImportLibrary>
</PropertyGroup>
<PropertyGroup Label="QtSettings" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<QtInstall>msvc2015_64</QtInstall>
<QtModules>core;xml;gui;widgets;bluetooth;positioning;charts</QtModules>
</PropertyGroup>
<PropertyGroup Label="QtSettings" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<QtInstall>msvc2015_64</QtInstall>
<QtModules>core;xml;gui;widgets;bluetooth;positioning;charts</QtModules>
</PropertyGroup>
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.props')">
<Import Project="$(QtMsBuild)\qt.props" />
</ImportGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>GeneratedFiles\$(ConfigurationName);GeneratedFiles;.;debug;/include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>-Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
<AssemblerListingLocation>debug\</AssemblerListingLocation>
<BrowseInformation>false</BrowseInformation>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<DisableSpecificWarnings>4577;4467;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<ExceptionHandling>Sync</ExceptionHandling>
<ObjectFileName>debug\</ObjectFileName>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WIN64;QT_DEPRECATED_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessToFile>false</PreprocessToFile>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<SuppressStartupBanner>true</SuppressStartupBanner>
<TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
<WarningLevel>Level3</WarningLevel>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions)</AdditionalOptions>
<DataExecutionPrevention>true</DataExecutionPrevention>
<GenerateDebugInformation>true</GenerateDebugInformation>
<IgnoreImportLibrary>true</IgnoreImportLibrary>
<OutputFile>$(OutDir)\qdomyos-zwift.exe</OutputFile>
<RandomizedBaseAddress>true</RandomizedBaseAddress>
<SubSystem>Console</SubSystem>
<SuppressStartupBanner>true</SuppressStartupBanner>
</Link>
<Midl>
<DefaultCharType>Unsigned</DefaultCharType>
<EnableErrorChecks>None</EnableErrorChecks>
<WarningLevel>0</WarningLevel>
</Midl>
<ResourceCompile>
<PreprocessorDefinitions>_CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WIN64;QT_DEPRECATED_WARNINGS;QT_CHARTS_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_BLUETOOTH_LIB;QT_XML_LIB;QT_POSITIONING_LIB;QT_CORE_LIB;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
<QtMoc>
<CompilerFlavor>msvc</CompilerFlavor>
<Include>./$(Configuration)/moc_predefs.h</Include>
<ExecutionDescription>Moc'ing %(Identity)...</ExecutionDescription>
<DynamicSource>output</DynamicSource>
<QtMocDir>$(Configuration)</QtMocDir>
<QtMocFileName>moc_%(Filename).cpp</QtMocFileName>
</QtMoc>
<QtRcc>
<InitFuncName>icons</InitFuncName>
<Compression>default</Compression>
<ExecutionDescription>Rcc'ing %(Identity)...</ExecutionDescription>
<QtRccDir>$(Configuration)</QtRccDir>
<QtRccFileName>qrc_%(Filename).cpp</QtRccFileName>
</QtRcc>
<QtUic>
<ExecutionDescription>Uic'ing %(Identity)...</ExecutionDescription>
<QtUicDir>$(ProjectDir)</QtUicDir>
<QtUicFileName>ui_%(Filename).h</QtUicFileName>
</QtUic>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>GeneratedFiles\$(ConfigurationName);GeneratedFiles;.;release;/include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>-Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
<AssemblerListingLocation>release\</AssemblerListingLocation>
<BrowseInformation>false</BrowseInformation>
<DebugInformationFormat>None</DebugInformationFormat>
<DisableSpecificWarnings>4577;4467;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<ExceptionHandling>Sync</ExceptionHandling>
<ObjectFileName>release\</ObjectFileName>
<Optimization>MaxSpeed</Optimization>
<PreprocessorDefinitions>_CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WIN64;QT_DEPRECATED_WARNINGS;NDEBUG;QT_NO_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessToFile>false</PreprocessToFile>
<ProgramDataBaseFileName>
</ProgramDataBaseFileName>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<SuppressStartupBanner>true</SuppressStartupBanner>
<TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
<WarningLevel>Level3</WarningLevel>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions)</AdditionalOptions>
<DataExecutionPrevention>true</DataExecutionPrevention>
<GenerateDebugInformation>false</GenerateDebugInformation>
<IgnoreImportLibrary>true</IgnoreImportLibrary>
<LinkIncremental>false</LinkIncremental>
<OptimizeReferences>true</OptimizeReferences>
<OutputFile>$(OutDir)\qdomyos-zwift.exe</OutputFile>
<RandomizedBaseAddress>true</RandomizedBaseAddress>
<SubSystem>Console</SubSystem>
<SuppressStartupBanner>true</SuppressStartupBanner>
</Link>
<Midl>
<DefaultCharType>Unsigned</DefaultCharType>
<EnableErrorChecks>None</EnableErrorChecks>
<WarningLevel>0</WarningLevel>
</Midl>
<ResourceCompile>
<PreprocessorDefinitions>_CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WIN64;QT_DEPRECATED_WARNINGS;NDEBUG;QT_NO_DEBUG;QT_CHARTS_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_BLUETOOTH_LIB;QT_XML_LIB;QT_POSITIONING_LIB;QT_CORE_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
<QtMoc>
<CompilerFlavor>msvc</CompilerFlavor>
<Include>./$(Configuration)/moc_predefs.h</Include>
<ExecutionDescription>Moc'ing %(Identity)...</ExecutionDescription>
<DynamicSource>output</DynamicSource>
<QtMocDir>$(Configuration)</QtMocDir>
<QtMocFileName>moc_%(Filename).cpp</QtMocFileName>
</QtMoc>
<QtRcc>
<InitFuncName>icons</InitFuncName>
<Compression>default</Compression>
<ExecutionDescription>Rcc'ing %(Identity)...</ExecutionDescription>
<QtRccDir>$(Configuration)</QtRccDir>
<QtRccFileName>qrc_%(Filename).cpp</QtRccFileName>
</QtRcc>
<QtUic>
<ExecutionDescription>Uic'ing %(Identity)...</ExecutionDescription>
<QtUicDir>$(ProjectDir)</QtUicDir>
<QtUicFileName>ui_%(Filename).h</QtUicFileName>
</QtUic>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="bike.cpp" />
<ClCompile Include="bluetooth.cpp" />
<ClCompile Include="bluetoothdevice.cpp" />
<ClCompile Include="charts.cpp" />
<ClCompile Include="domyosbike.cpp" />
<ClCompile Include="domyostreadmill.cpp" />
<ClCompile Include="gpx.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="mainwindow.cpp" />
<ClCompile Include="toorxtreadmill.cpp" />
<ClCompile Include="trainprogram.cpp" />
<ClCompile Include="treadmill.cpp" />
<ClCompile Include="trxappgateusbtreadmill.cpp" />
<ClCompile Include="virtualbike.cpp" />
<ClCompile Include="virtualtreadmill.cpp" />
</ItemGroup>
<ItemGroup>
<QtMoc Include="bike.h">
</QtMoc>
<QtMoc Include="bluetooth.h">
</QtMoc>
<QtMoc Include="bluetoothdevice.h">
</QtMoc>
<QtMoc Include="charts.h">
</QtMoc>
<QtMoc Include="domyosbike.h">
</QtMoc>
<QtMoc Include="domyostreadmill.h">
</QtMoc>
<QtMoc Include="gpx.h">
</QtMoc>
<QtMoc Include="mainwindow.h">
</QtMoc>
<QtMoc Include="toorxtreadmill.h">
</QtMoc>
<QtMoc Include="trainprogram.h">
</QtMoc>
<QtMoc Include="treadmill.h">
</QtMoc>
<QtMoc Include="trxappgateusbtreadmill.h">
</QtMoc>
<QtMoc Include="virtualbike.h">
</QtMoc>
<QtMoc Include="virtualtreadmill.h">
</QtMoc>
</ItemGroup>
<ItemGroup>
<CustomBuild Include="debug\moc_predefs.h.cbt">
<FileType>Document</FileType>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs)</AdditionalInputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zi -MDd -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2&gt;NUL &gt;debug\moc_predefs.h</Command>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Generate moc_predefs.h</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">debug\moc_predefs.h;%(Outputs)</Outputs>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</CustomBuild>
<CustomBuild Include="release\moc_predefs.h.cbt">
<FileType>Document</FileType>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs)</AdditionalInputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -O2 -MD -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2&gt;NUL &gt;release\moc_predefs.h</Command>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Generate moc_predefs.h</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">release\moc_predefs.h;%(Outputs)</Outputs>
</CustomBuild>
</ItemGroup>
<ItemGroup>
<QtUic Include="charts.ui">
</QtUic>
<QtUic Include="mainwindow.ui">
</QtUic>
</ItemGroup>
<ItemGroup>
<None Include="icons\bluetooth-icon.png" />
<None Include="icons\cadence.png" />
<None Include="icons\elevationgain.png" />
<None Include="icons\fan.png" />
<None Include="icons\heart_red.png" />
<QtRcc Include="icons.qrc">
</QtRcc>
<None Include="icons\inclination.png" />
<None Include="icons\kcal.png" />
<None Include="icons\odometer.png" />
<None Include="icons\resistance.png" />
<None Include="icons\speed.png" />
<None Include="icons\start.png" />
<None Include="icons\stop.png" />
<None Include="icons\watt.png" />
<None Include="icons\weight.png" />
<None Include="icons\zwift-on.png" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

10
src/qml.qrc Normal file
View File

@@ -0,0 +1,10 @@
<RCC>
<qresource prefix="/">
<file>HomeForm.ui.qml</file>
<file>main.qml</file>
<file>Page1Form.ui.qml</file>
<file>Page2Form.ui.qml</file>
<file>qtquickcontrols2.conf</file>
<file>Home.qml</file>
</qresource>
</RCC>

14
src/qtquickcontrols2.conf Normal file
View File

@@ -0,0 +1,14 @@
; This file can be edited to change the style of the application
; Read "Qt Quick Controls 2 Configuration File" for details:
; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html
[Controls]
Style=Material
[Universal]
Theme=System
Accent=Red
[Material]
Theme=Dark
Primary=BlueGrey

15
src/sessionline.cpp Normal file
View File

@@ -0,0 +1,15 @@
#include "sessionline.h"
SessionLine::SessionLine(double speed, int8_t inclination, double distance, uint8_t watt, int8_t resistance, uint8_t heart, double pace, QTime time)
{
this->speed = speed;
this->inclination = inclination;
this->distance = distance;
this->watt = watt;
this->resistance = resistance;
this->heart = heart;
this->pace = pace;
this->time = time;
}
SessionLine::SessionLine() {}

22
src/sessionline.h Normal file
View File

@@ -0,0 +1,22 @@
#ifndef SESSIONLINE_H
#define SESSIONLINE_H
#include <QTimer>
#include <QTime>
class SessionLine
{
public:
double speed;
int8_t inclination;
double distance;
uint8_t watt;
int8_t resistance;
uint8_t heart;
double pace;
QTime time;
SessionLine();
SessionLine(double speed, int8_t inclination, double distance, uint8_t watt, int8_t resistance, uint8_t heart, double pace, QTime time = QTime::currentTime());
};
#endif // SESSIONLINE_H

73
src/test/test-bike/.gitignore vendored Normal file
View File

@@ -0,0 +1,73 @@
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe

View File

@@ -0,0 +1,8 @@
#include <QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
return a.exec();
}

View File

@@ -0,0 +1,23 @@
QT -= gui
CONFIG += c++11 console
CONFIG -= app_bundle
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

137
src/toorxtreadmill.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include "toorxtreadmill.h"
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
toorxtreadmill::toorxtreadmill()
{
refresh = new QTimer(this);
initDone = false;
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
refresh->start(200);
}
void toorxtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("TRX ROUTE KEY"))
{
bttreadmill = device;
// Create a discovery agent and connect to its signals
discoveryAgent = new QBluetoothServiceDiscoveryAgent(this);
connect(discoveryAgent, SIGNAL(serviceDiscovered(QBluetoothServiceInfo)),
this, SLOT(serviceDiscovered(QBluetoothServiceInfo)));
// Start a discovery
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
return;
}
}
// In your local slot, read information about the found devices
void toorxtreadmill::serviceDiscovered(const QBluetoothServiceInfo &service)
{
if(service.device().address() == bttreadmill.address())
{
debug("Found new service: " + service.serviceName()
+ '(' + service.serviceUuid().toString() + ')');
if(service.serviceName().contains("SerialPort"))
{
debug("Serial port service found");
discoveryAgent->stop();
serialPortService = service;
socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);
connect(socket, &QBluetoothSocket::readyRead, this, &toorxtreadmill::readSocket);
connect(socket, &QBluetoothSocket::connected, this, QOverload<>::of(&toorxtreadmill::rfCommConnected));
connect(socket, &QBluetoothSocket::disconnected, this, &toorxtreadmill::disconnected);
connect(socket, QOverload<QBluetoothSocket::SocketError>::of(&QBluetoothSocket::error),
this, &toorxtreadmill::onSocketErrorOccurred);
debug("Create socket");
socket->connectToService(serialPortService);
debug("ConnectToService done");
}
}
}
void toorxtreadmill::update()
{
if(initDone)
{
const char poll[] = {0x55, 0x17, 0x01, 0x01, 0x53};
socket->write(poll, sizeof(poll));
debug("write poll");
}
}
void toorxtreadmill::rfCommConnected()
{
debug("connected " + socket->peerName());
initDone = true;
}
void toorxtreadmill::readSocket()
{
if (!socket)
return;
while (socket->canReadLine()) {
QByteArray line = socket->readLine();
debug(socket->peerName() +
QString::fromUtf8(line.constData(), line.length()));
if(line.length() == 17)
{
elapsed = GetElapsedTimeFromPacket(line);
Distance = GetDistanceFromPacket(line);
KCal = GetCaloriesFromPacket(line);
Speed = GetSpeedFromPacket(line);
Inclination = GetInclinationFromPacket(line);
Heart = GetHeartRateFromPacket(line);
}
}
}
uint8_t toorxtreadmill::GetHeartRateFromPacket(QByteArray packet)
{
return packet.at(16);
}
uint8_t toorxtreadmill::GetInclinationFromPacket(QByteArray packet)
{
return packet.at(15);
}
double toorxtreadmill::GetSpeedFromPacket(QByteArray packet)
{
double convertedData = (double)(packet.at(13) << 8) + ((double)packet.at(14) / 100.0);
return convertedData;
}
uint16_t toorxtreadmill::GetCaloriesFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(11) << 8) | packet.at(12);
return convertedData;
}
uint16_t toorxtreadmill::GetDistanceFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(9) << 8) | packet.at(10);
return convertedData;
}
uint16_t toorxtreadmill::GetElapsedTimeFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(7) << 8) | packet.at(8);
return convertedData;
}
void toorxtreadmill::onSocketErrorOccurred(QBluetoothSocket::SocketError error)
{
debug("onSocketErrorOccurred " + QString::number(error));
}

70
src/toorxtreadmill.h Normal file
View File

@@ -0,0 +1,70 @@
#ifndef TOORX_H
#define TOORX_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 <QBluetoothServiceDiscoveryAgent>
#include <QBluetoothSocket>
#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 "virtualtreadmill.h"
#include "treadmill.h"
class toorxtreadmill : public treadmill
{
Q_OBJECT
public:
explicit toorxtreadmill();
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
private slots:
void serviceDiscovered(const QBluetoothServiceInfo &service);
void readSocket();
void rfCommConnected();
void onSocketErrorOccurred(QBluetoothSocket::SocketError);
void update();
private:
QBluetoothDeviceInfo bttreadmill;
QBluetoothServiceDiscoveryAgent *discoveryAgent;
QBluetoothServiceInfo serialPortService;
QBluetoothSocket *socket = nullptr;
QTimer* refresh;
bool initDone = false;
uint16_t GetElapsedTimeFromPacket(QByteArray packet);
uint16_t GetDistanceFromPacket(QByteArray packet);
uint16_t GetCaloriesFromPacket(QByteArray packet);
double GetSpeedFromPacket(QByteArray packet);
uint8_t GetInclinationFromPacket(QByteArray packet);
uint8_t GetHeartRateFromPacket(QByteArray packet);
signals:
void disconnected();
void debug(QString string);
};
#endif // TOORX_H

177
src/trainprogram.cpp Normal file
View File

@@ -0,0 +1,177 @@
#include "trainprogram.h"
#include <QFile>
#include <QtXml/QtXml>
trainprogram::trainprogram(QList<trainrow> rows, bluetooth* b)
{
this->bluetoothManager = b;
this->rows = rows;
this->loadedRows = rows;
connect(&timer, SIGNAL(timeout()), this, SLOT(scheduler()));
timer.setInterval(1000);
timer.start();
}
void trainprogram::scheduler()
{
if(
rows.count() == 0 ||
started == false ||
enabled == false ||
bluetoothManager->device() == nullptr ||
bluetoothManager->device()->currentSpeed() <= 0
)
{
return;
}
ticks++;
elapsed = ticks;
ticksCurrentRow++;
elapsedCurrentRow = ticksCurrentRow;
// entry point
if(ticks == 1 && currentStep == 0)
{
if(rows[0].forcespeed && rows[0].speed)
{
qDebug() << "trainprogram change speed" + QString::number(rows[0].speed);
emit changeSpeedAndInclination(rows[0].speed, rows[0].inclination);
}
else
{
qDebug() << "trainprogram change inclination" + QString::number(rows[0].inclination);
emit changeInclination(rows[0].inclination);
}
}
uint32_t currentRowLen = rows[currentStep].duration.second() +
(rows[currentStep].duration.minute() * 60) +
(rows[currentStep].duration.hour() * 3600);
uint32_t nextRowLen = 0;
if(rows.count() > currentStep + 1)
nextRowLen = rows[currentStep + 1].duration.second() +
(rows[currentStep + 1].duration.minute() * 60) +
(rows[currentStep + 1].duration.hour() * 3600);
qDebug() << "trainprogram elapsed current row" + QString::number(elapsedCurrentRow) + "current row len" + QString::number(currentRowLen);
if(elapsedCurrentRow >= currentRowLen && currentRowLen)
{
if(nextRowLen)
{
currentStep++;
ticksCurrentRow = 0;
elapsedCurrentRow = 0;
if(rows[currentStep].forcespeed && rows[currentStep].speed)
{
qDebug() << "trainprogram change speed" + QString::number(rows[currentStep].speed);
emit changeSpeedAndInclination(rows[currentStep].speed, rows[currentStep].inclination);
}
qDebug() << "trainprogram change inclination" + QString::number(rows[currentStep].inclination);
emit changeInclination(rows[currentStep].inclination);
}
else
{
qDebug() << "trainprogram ends!";
started = false;
emit stop();
}
}
}
void trainprogram::onTapeStarted()
{
started = true;
}
void trainprogram::restart()
{
ticks = 0;
ticksCurrentRow = 0;
elapsed = 0;
elapsedCurrentRow = 0;
currentStep = 0;
started = true;
}
void trainprogram::save(QString filename)
{
QFile output(filename);
output.open(QIODevice::WriteOnly);
QXmlStreamWriter stream(&output);
stream.setAutoFormatting(true);
stream.writeStartDocument();
stream.writeStartElement("rows");
foreach (trainrow row, rows) {
stream.writeStartElement("row");
stream.writeAttribute("duration", row.duration.toString());
stream.writeAttribute("speed", QString::number(row.speed));
stream.writeAttribute("inclination", QString::number(row.inclination));
stream.writeAttribute("forcespeed", row.forcespeed?"1":"0");
stream.writeEndElement();
}
stream.writeEndElement();
stream.writeEndDocument();
}
trainprogram* trainprogram::load(QString filename, bluetooth* b)
{
QList<trainrow> list;
QFile input(filename);
input.open(QIODevice::ReadOnly);
QXmlStreamReader stream(&input);
while(!stream.atEnd())
{
stream.readNext();
trainrow row;
QXmlStreamAttributes atts = stream.attributes();
if(atts.length())
{
row.duration = QTime::fromString(atts.value("duration").toString(), "hh:mm:ss");
row.speed = atts.value("speed").toDouble();
row.inclination = atts.value("inclination").toDouble();
row.forcespeed = atts.value("forcespeed").toInt()?true:false ;
list.append(row);
}
}
trainprogram *tr = new trainprogram(list, b);
return tr;
}
QTime trainprogram::totalElapsedTime()
{
return QTime(0,0,elapsed);
}
QTime trainprogram::currentRowElapsedTime()
{
return QTime(0,0,elapsedCurrentRow);
}
QTime trainprogram::duration()
{
QTime total(0,0,0,0);
foreach (trainrow row, rows) {
total = total.addSecs((row.duration.hour() * 3600) + (row.duration.minute() * 60) + row.duration.second());
}
return total;
}
double trainprogram::totalDistance()
{
double distance = 0;
foreach (trainrow row, rows) {
if(row.duration.hour() || row.duration.minute() || row.duration.second())
{
if(!row.forcespeed)
{
return -1;
}
distance += ((row.duration.hour() * 3600) + (row.duration.minute() * 60) + row.duration.second()) * (row.speed / 3600);
}
}
return distance;
}

59
src/trainprogram.h Normal file
View File

@@ -0,0 +1,59 @@
#ifndef TRAINPROGRAM_H
#define TRAINPROGRAM_H
#include <QTime>
#include <QTimer>
#include <QObject>
#include "bluetooth.h"
class trainrow
{
public:
QTime duration;
double speed;
double inclination;
bool forcespeed;
};
class trainprogram: public QObject
{
Q_OBJECT
public:
trainprogram(QList<trainrow>, bluetooth* b);
void save(QString filename);
static trainprogram* load(QString filename, bluetooth* b);
QTime totalElapsedTime();
QTime currentRowElapsedTime();
QTime duration();
double totalDistance();
QList<trainrow> rows;
QList<trainrow> loadedRows; // rows as loaded
uint32_t elapsed = 0;
bool enabled = true;
void restart();
void scheduler(int tick);
public slots:
void onTapeStarted();
void scheduler();
signals:
void start();
void stop();
void changeSpeed(double speed);
void changeInclination(double inclination);
void changeSpeedAndInclination(double speed, double inclination);
private:
bluetooth* bluetoothManager;
bool started = false;
uint32_t ticks = 0;
uint16_t currentStep = 0;
uint32_t ticksCurrentRow = 0;
uint32_t elapsedCurrentRow = 0;
QTimer timer;
};
#endif // TRAINPROGRAM_H

32
src/treadmill.cpp Normal file
View File

@@ -0,0 +1,32 @@
#include "treadmill.h"
treadmill::treadmill()
{
}
void treadmill::changeSpeed(double speed){ requestSpeed = speed;}
void treadmill::changeInclination(double inclination){ requestInclination = inclination; }
void treadmill::changeSpeedAndInclination(double speed, double inclination){ requestSpeed = speed; requestInclination = inclination;}
double treadmill::currentInclination(){ return Inclination; }
double treadmill::elevationGain(){ return elevationAcc; }
uint8_t treadmill::fanSpeed() { return FanSpeed; };
bool treadmill::connected() { return false; }
bluetoothdevice::BLUETOOTH_TYPE treadmill::deviceType() { return bluetoothdevice::TREADMILL; }
uint16_t treadmill::watts(double weight)
{
// calc Watts ref. https://alancouzens.com/blog/Run_Power.html
uint16_t watts=0;
if(currentSpeed() > 0)
{
double pace=60/currentSpeed();
double VO2R=210.0/pace;
double VO2A=(VO2R*weight)/1000.0;
double hwatts=75*VO2A;
double vwatts=((9.8*weight) * (currentInclination()/100));
watts=hwatts+vwatts;
}
return watts;
}

34
src/treadmill.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef TREADMILL_H
#define TREADMILL_H
#include <QObject>
#include "bluetoothdevice.h"
class treadmill:public bluetoothdevice
{
Q_OBJECT
public:
treadmill();
virtual double currentInclination();
virtual double elevationGain();
virtual uint8_t fanSpeed();
virtual bool connected();
uint16_t watts(double weight=75.0);
bluetoothdevice::BLUETOOTH_TYPE deviceType();
public slots:
virtual void changeSpeed(double speed);
virtual void changeInclination(double inclination);
virtual void changeSpeedAndInclination(double speed, double inclination);
signals:
void tapeStarted();
protected:
double elevationAcc = 0;
double Inclination = 0;
double requestSpeed = -1;
double requestInclination = -1;
};
#endif // TREADMILL_H

View File

@@ -0,0 +1,398 @@
#include "trxappgateusbtreadmill.h"
#include "virtualtreadmill.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
trxappgateusbtreadmill::trxappgateusbtreadmill()
{
refresh = new QTimer(this);
initDone = false;
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
refresh->start(200);
}
void trxappgateusbtreadmill::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log)
{
QEventLoop loop;
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)data, data_len));
if(!disable_log)
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
loop.exec();
}
void trxappgateusbtreadmill::forceSpeedOrIncline(double requestSpeed, double requestIncline)
{
Q_UNUSED(requestSpeed);
Q_UNUSED(requestIncline);
}
bool trxappgateusbtreadmill::changeFanSpeed(uint8_t speed)
{
Q_UNUSED(speed);
return false;
}
void trxappgateusbtreadmill::update()
{
static uint8_t sec1 = 0;
static QTime lastTime;
static bool first = true;
//qDebug() << treadmill.isValid() << m_control->state() << gattCommunicationChannelService << gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
if(m_control->state() == QLowEnergyController::UnconnectedState)
{
emit disconnected();
return;
}
if(initRequest)
{
initRequest = false;
btinit(false);
}
else if(bttreadmill.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotifyCharacteristic.isValid() &&
initDone)
{
if(currentSpeed() > 0.0 && !first)
elapsed += ((double)lastTime.msecsTo(QTime::currentTime()) / 1000.0);
// updating the treadmill console every second
if(sec1++ == (1000 / refresh->interval()))
{
sec1 = 0;
//updateDisplay(elapsed);
}
const uint8_t noOpData[] = { 0xf0, 0xa2, 0x01, 0xd3, 0x66 };
writeCharacteristic((uint8_t*)noOpData, sizeof(noOpData), "noOp", true);
if(requestSpeed != -1)
{
if(requestSpeed != currentSpeed())
{
debug("writing speed " + QString::number(requestSpeed));
double inc = Inclination;
if(requestInclination != -1)
{
inc = requestInclination;
requestInclination = -1;
}
forceSpeedOrIncline(requestSpeed, inc);
}
requestSpeed = -1;
}
if(requestInclination != -1)
{
if(requestInclination != currentInclination())
{
debug("writing incline " + QString::number(requestInclination));
double speed = currentSpeed();
if(requestSpeed != -1)
{
speed = requestSpeed;
requestSpeed = -1;
}
forceSpeedOrIncline(speed, requestInclination);
}
requestInclination = -1;
}
if(requestStart != -1)
{
debug("starting...");
btinit(true);
requestStart = -1;
emit tapeStarted();
}
if(requestStop != -1)
{
debug("stopping...");
//writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
if(requestIncreaseFan != -1)
{
debug("increasing fan speed...");
changeFanSpeed(FanSpeed + 1);
requestIncreaseFan = -1;
}
else if(requestDecreaseFan != -1)
{
debug("decreasing fan speed...");
changeFanSpeed(FanSpeed - 1);
requestDecreaseFan = -1;
}
elevationAcc += (currentSpeed() / 3600.0) * 1000 * (currentInclination() / 100) * (refresh->interval() / 1000);
}
lastTime = QTime::currentTime();
first = false;
}
void trxappgateusbtreadmill::serviceDiscovered(const QBluetoothUuid &gatt)
{
debug("serviceDiscovered " + gatt.toString());
}
static QByteArray lastPacket;
void trxappgateusbtreadmill::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
Q_UNUSED(characteristic);
static QTime lastTime;
static bool first = true;
debug(" << " + newValue.toHex(' '));
if (lastPacket.length() && lastPacket == newValue)
return;
lastPacket = newValue;
if (newValue.length() != 19)
return;
double speed = GetSpeedFromPacket(newValue);
double incline = GetInclinationFromPacket(newValue);
double kcal = GetKcalFromPacket(newValue);
double distance = GetDistanceFromPacket(newValue);
Heart = 0;
FanSpeed = 0;
if(!first)
DistanceCalculated += ((speed / 3600.0) / ( 1000.0 / (lastTime.msecsTo(QTime::currentTime()))));
debug("Current speed: " + QString::number(speed));
debug("Current incline: " + QString::number(incline));
debug("Current heart: " + QString::number(Heart));
debug("Current KCal: " + QString::number(kcal));
debug("Current Distance: " + QString::number(distance));
debug("Current Elapsed from the treadmill (not used): " + QString::number(GetElapsedFromPacket(newValue)));
debug("Current Distance Calculated: " + QString::number(DistanceCalculated));
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
Speed = speed;
Inclination = incline;
KCal = kcal;
Distance = distance;
lastTime = QTime::currentTime();
first = false;
}
uint16_t trxappgateusbtreadmill::GetElapsedFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(4) - 1);
convertedData += ((packet.at(5) - 1) * 60);
return convertedData;
}
double trxappgateusbtreadmill::GetSpeedFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(13) - 1) + ((packet.at(12) - 1) * 100);
double data = (double)(convertedData) / 10.0f;
return data;
}
double trxappgateusbtreadmill::GetKcalFromPacket(QByteArray packet)
{
uint16_t convertedData = ((packet.at(8) - 1) << 8) | (packet.at(9) - 1);
return (double)(convertedData);
}
double trxappgateusbtreadmill::GetDistanceFromPacket(QByteArray packet)
{
uint16_t convertedData = ((packet.at(6) - 1) << 8) | (packet.at(7) - 1);
double data = ((double)(convertedData)) / 100.0f;
return data;
}
double trxappgateusbtreadmill::GetInclinationFromPacket(QByteArray packet)
{
uint16_t convertedData = packet.at(14);
double data = (convertedData - 1);
if (data < 0) return 0;
return data;
}
void trxappgateusbtreadmill::btinit(bool startTape)
{
Q_UNUSED(startTape);
const uint8_t initData1[] = { 0xf0, 0xa0, 0x01, 0x01, 0x92 };
const uint8_t initData2[] = { 0xf0, 0xa5, 0x01, 0xd3, 0x04, 0x6d };
const uint8_t initData3[] = { 0xf0, 0xa0, 0x01, 0xd3, 0x64 };
const uint8_t initData4[] = { 0xf0, 0xa1, 0x01, 0xd3, 0x65 };
const uint8_t initData5[] = { 0xf0, 0xa3, 0x01, 0xd3, 0x01, 0x15, 0x01, 0x02, 0x51, 0x01, 0x51, 0x23 };
const uint8_t initData6[] = { 0xf0, 0xa4, 0x01, 0xd3, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x73 };
const uint8_t initData7[] = { 0xf0, 0xaf, 0x01, 0xd3, 0x02, 0x75 };
writeCharacteristic((uint8_t*)initData1, sizeof(initData1), "init");
writeCharacteristic((uint8_t*)initData2, sizeof(initData2), "init");
writeCharacteristic((uint8_t*)initData3, sizeof(initData3), "init");
writeCharacteristic((uint8_t*)initData4, sizeof(initData4), "init");
writeCharacteristic((uint8_t*)initData3, sizeof(initData3), "init");
writeCharacteristic((uint8_t*)initData5, sizeof(initData5), "init");
writeCharacteristic((uint8_t*)initData6, sizeof(initData6), "init");
writeCharacteristic((uint8_t*)initData7, sizeof(initData7), "init");
initDone = true;
}
void trxappgateusbtreadmill::stateChanged(QLowEnergyService::ServiceState state)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
debug("BTLE stateChanged " + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
if(state == QLowEnergyService::ServiceDiscovered)
{
foreach(QLowEnergyCharacteristic c,gattCommunicationChannelService->characteristics())
{
debug("characteristic " + c.uuid().toString());
}
QBluetoothUuid _gattWriteCharacteristicId((QString)"0000fff2-0000-1000-8000-00805f9b34fb");
QBluetoothUuid _gattNotifyCharacteristicId((QString)"0000fff1-0000-1000-8000-00805f9b34fb");
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotifyCharacteristic.isValid());
// establish hook into notifications
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
this, SLOT(characteristicChanged(QLowEnergyCharacteristic,QByteArray)));
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)),
this, SLOT(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)));
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)),
this, SLOT(errorService(QLowEnergyService::ServiceError)));
connect(gattCommunicationChannelService, SIGNAL(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)), this,
SLOT(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)));
// ******************************************* virtual treadmill init *************************************
static uint8_t first = 0;
if(!first)
{
debug("creating virtual treadmill interface...");
virtualTreadMill = new virtualtreadmill(this, false);
connect(virtualTreadMill,&virtualtreadmill::debug ,this,&trxappgateusbtreadmill::debug);
}
first = 1;
// ********************************************************************************************************
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattCommunicationChannelService->writeDescriptor(gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
}
}
void trxappgateusbtreadmill::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
{
debug("descriptorWritten " + descriptor.name() + " " + newValue.toHex(' '));
initRequest = true;
}
void trxappgateusbtreadmill::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
Q_UNUSED(characteristic);
debug("characteristicWritten " + newValue.toHex(' '));
}
void trxappgateusbtreadmill::serviceScanDone(void)
{
debug("serviceScanDone");
QBluetoothUuid _gattCommunicationChannelServiceId((QString)"0000fff0-0000-1000-8000-00805f9b34fb");
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
connect(gattCommunicationChannelService, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(stateChanged(QLowEnergyService::ServiceState)));
gattCommunicationChannelService->discoverDetails();
}
void trxappgateusbtreadmill::errorService(QLowEnergyService::ServiceError err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
debug("trxappgateusbtreadmill::errorService" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
}
void trxappgateusbtreadmill::error(QLowEnergyController::Error err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
debug("trxappgateusbtreadmill::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
m_control->disconnect();
}
void trxappgateusbtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("TOORX"))
{
bttreadmill = device;
m_control = QLowEnergyController::createCentral(bttreadmill, 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, 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 trxappgateusbtreadmill::connected()
{
if(!m_control)
return false;
return m_control->state() == QLowEnergyController::DiscoveredState;
}
void* trxappgateusbtreadmill::VirtualTreadMill()
{
return virtualTreadMill;
}
void* trxappgateusbtreadmill::VirtualDevice()
{
return VirtualTreadMill();
}
double trxappgateusbtreadmill::odometer()
{
return DistanceCalculated;
}

View File

@@ -0,0 +1,89 @@
#ifndef TRXAPPGATEUSBTREADMILL_H
#define TRXAPPGATEUSBTREADMILL_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 "virtualtreadmill.h"
#include "treadmill.h"
class trxappgateusbtreadmill : public treadmill
{
Q_OBJECT
public:
trxappgateusbtreadmill();
bool connected();
bool changeFanSpeed(uint8_t speed);
double odometer();
void* VirtualTreadMill();
void* VirtualDevice();
private:
double GetSpeedFromPacket(QByteArray packet);
double GetInclinationFromPacket(QByteArray packet);
double GetKcalFromPacket(QByteArray packet);
double GetDistanceFromPacket(QByteArray packet);
uint16_t GetElapsedFromPacket(QByteArray packet);
void forceSpeedOrIncline(double requestSpeed, double requestIncline);
void updateDisplay(uint16_t elapsed);
void btinit(bool startTape);
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false);
void startDiscover();
double DistanceCalculated = 0;
QTimer* refresh;
virtualtreadmill* virtualTreadMill = 0;
QBluetoothDeviceInfo bttreadmill;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotifyCharacteristic;
bool initDone = false;
bool initRequest = false;
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 stateChanged(QLowEnergyService::ServiceState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};
#endif // TRXAPPGATEUSBTREADMILL_H

311
src/virtualbike.cpp Normal file
View File

@@ -0,0 +1,311 @@
#include "virtualbike.h"
#include <QtMath>
#include <QMetaEnum>
#include <QDataStream>
enum FtmsControlPointCommand {
FTMS_REQUEST_CONTROL = 0x00,
FTMS_RESET,
FTMS_SET_TARGET_SPEED,
FTMS_SET_TARGET_INCLINATION,
FTMS_SET_TARGET_RESISTANCE_LEVEL,
FTMS_SET_TARGET_POWER,
FTMS_SET_TARGET_HEARTRATE,
FTMS_START_RESUME,
FTMS_STOP_PAUSE,
FTMS_SET_TARGETED_EXP_ENERGY,
FTMS_SET_TARGETED_STEPS,
FTMS_SET_TARGETED_STRIDES,
FTMS_SET_TARGETED_DISTANCE,
FTMS_SET_TARGETED_TIME,
FTMS_SET_TARGETED_TIME_TWO_HR_ZONES,
FTMS_SET_TARGETED_TIME_THREE_HR_ZONES,
FTMS_SET_TARGETED_TIME_FIVE_HR_ZONES,
FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS,
FTMS_SET_WHEEL_CIRCUMFERENCE,
FTMS_SPIN_DOWN_CONTROL,
FTMS_SET_TARGETED_CADENCE,
FTMS_RESPONSE_CODE = 0x80
};
enum FtmsResultCode {
FTMS_SUCCESS = 0x01,
FTMS_NOT_SUPPORTED,
FTMS_INVALID_PARAMETER,
FTMS_OPERATION_FAILED,
FTMS_CONTROL_NOT_PERMITTED
};
virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService)
{
Bike = t;
this->noHeartService = noHeartService;
Q_UNUSED(noWriteResistance)
//! [Advertising Data]
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
advertisingData.setLocalName("DomyosBridge");
QList<QBluetoothUuid> services;
services << ((QBluetoothUuid::ServiceClassUuid)0x1826); //FitnessMachineServiceUuid
if(!this->noHeartService)
services << QBluetoothUuid::HeartRate;
advertisingData.setServices(services);
//! [Advertising Data]
serviceDataFIT.setType(QLowEnergyServiceData::ServiceTypePrimary);
QLowEnergyCharacteristicData charDataFIT;
charDataFIT.setUuid((QBluetoothUuid::CharacteristicType)0x2ACC); //FitnessMachineFeatureCharacteristicUuid
QByteArray valueFIT;
valueFIT.append((char)0x80); // resistance level supported
valueFIT.append((char)0x14); // heart rate and elapsed time
valueFIT.append((char)0x00);
valueFIT.append((char)0x00);
valueFIT.append((char)0x00);
valueFIT.append((char)0x00);
valueFIT.append((char)0x00);
valueFIT.append((char)0x00);
charDataFIT.setValue(valueFIT);
charDataFIT.setProperties(QLowEnergyCharacteristic::Read);
QLowEnergyCharacteristicData charDataFIT2;
charDataFIT2.setUuid((QBluetoothUuid::CharacteristicType)0x2AD6); //supported_resistance_level_rangeCharacteristicUuid
charDataFIT2.setProperties(QLowEnergyCharacteristic::Read);
QByteArray valueFIT2;
valueFIT2.append((char)0x0A); // min resistance value
valueFIT2.append((char)0x00); // min resistance value
valueFIT2.append((char)0x96); // max resistance value
valueFIT2.append((char)0x00); // max resistance value
valueFIT2.append((char)0x0A); // step resistance
valueFIT2.append((char)0x00); // step resistance
charDataFIT2.setValue(valueFIT2);
QLowEnergyCharacteristicData charDataFIT3;
charDataFIT3.setUuid((QBluetoothUuid::CharacteristicType)0x2AD9); //Fitness Machine Control Point
charDataFIT3.setProperties(QLowEnergyCharacteristic::Write | QLowEnergyCharacteristic::Indicate);
const QLowEnergyDescriptorData cpClientConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charDataFIT3.addDescriptor(cpClientConfig);
QLowEnergyCharacteristicData charDataFIT4;
charDataFIT4.setUuid((QBluetoothUuid::CharacteristicType)0x2AD2); //indoor bike
charDataFIT4.setProperties(QLowEnergyCharacteristic::Notify | QLowEnergyCharacteristic::Read);
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
const QLowEnergyDescriptorData clientConfig4(QBluetoothUuid::ClientCharacteristicConfiguration,
descriptor);
charDataFIT4.addDescriptor(clientConfig4);
serviceDataFIT.setUuid((QBluetoothUuid::ServiceClassUuid)0x1826); //FitnessMachineServiceUuid
serviceDataFIT.addCharacteristic(charDataFIT);
serviceDataFIT.addCharacteristic(charDataFIT2);
serviceDataFIT.addCharacteristic(charDataFIT3);
serviceDataFIT.addCharacteristic(charDataFIT4);
if(!this->noHeartService)
{
QLowEnergyCharacteristicData charDataHR;
charDataHR.setUuid(QBluetoothUuid::HeartRateMeasurement);
charDataHR.setValue(QByteArray(2, 0));
charDataHR.setProperties(QLowEnergyCharacteristic::Notify);
const QLowEnergyDescriptorData clientConfigHR(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charDataHR.addDescriptor(clientConfigHR);
serviceDataHR.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceDataHR.setUuid(QBluetoothUuid::HeartRate);
serviceDataHR.addCharacteristic(charDataHR);
}
//! [Start Advertising]
leController = QLowEnergyController::createPeripheral();
Q_ASSERT(leController);
if(!this->noHeartService)
serviceHR = leController->addService(serviceDataHR);
serviceFIT = leController->addService(serviceDataFIT);
QObject::connect(serviceFIT, SIGNAL(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)), this, SLOT(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)));
QLowEnergyAdvertisingParameters pars;
pars.setInterval(100, 100);
leController->startAdvertising(pars, advertisingData,
advertisingData);
//! [Start Advertising]
//! [Provide Heartbeat]
QObject::connect(&bikeTimer, SIGNAL(timeout()), this, SLOT(bikeProvider()));
bikeTimer.start(1000);
//! [Provide Heartbeat]
QObject::connect(leController, SIGNAL(disconnected()), this, SLOT(reconnect()));
QObject::connect(leController, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(error(QLowEnergyController::Error)));
}
void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
QByteArray reply;
emit debug("characteristicChanged " + QString::number(characteristic.uuid().toUInt16()) + " " + newValue.toHex(' '));
switch(characteristic.uuid().toUInt16())
{
case 0x2AD9: // Fitness Machine Control Point
if((char)newValue.at(0) == FTMS_SET_TARGET_RESISTANCE_LEVEL)
{
// Set Target Resistance
uint8_t uresistance = newValue.at(1);
uresistance = uresistance / 10;
Bike->changeResistance(uresistance);
emit debug("new requested resistance " + QString::number(uresistance));
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_SET_TARGET_RESISTANCE_LEVEL);
reply.append((quint8)FTMS_SUCCESS);
}
else if((char)newValue.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS) // simulation parameter
{
emit debug("indoor bike simulation parameters");
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS);
reply.append((quint8)FTMS_SUCCESS);
int16_t iresistance = (newValue.at(3) + (newValue.at(4) << 8));
double resistance = ((double)iresistance) / 100.0;
Bike->changeResistance((uint8_t)resistance + 1); // resistance start from 1
}
else if((char)newValue.at(0) == FTMS_START_RESUME)
{
emit debug("start simulation!");
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_START_RESUME);
reply.append((quint8)FTMS_SUCCESS);
}
else if((char)newValue.at(0) == FTMS_REQUEST_CONTROL)
{
emit debug("control requested");
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_REQUEST_CONTROL);
reply.append((quint8)FTMS_SUCCESS);
}
else
{
emit debug("not supported");
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)newValue.at(0));
reply.append((quint8)FTMS_NOT_SUPPORTED);
}
QLowEnergyCharacteristic characteristic
= serviceFIT->characteristic((QBluetoothUuid::CharacteristicType)0x2AD9);
Q_ASSERT(characteristic.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
return;
}
writeCharacteristic(serviceFIT, characteristic, reply);
break;
}
}
void virtualbike::writeCharacteristic(QLowEnergyService* service, QLowEnergyCharacteristic characteristic, QByteArray value)
{
try {
emit debug("virtualbike::writeCharacteristic " + service->serviceName() + " " + characteristic.name() + " " + value.toHex(' '));
service->writeCharacteristic(characteristic, value); // Potentially causes notification.
} catch (...) {
emit debug("virtual bike error!");
}
}
void virtualbike::reconnect()
{
emit debug("virtualbike::reconnect");
leController->disconnectFromDevice();
if(!this->noHeartService)
serviceHR = leController->addService(serviceDataHR);
serviceFIT = leController->addService(serviceDataFIT);
if (serviceFIT)
leController->startAdvertising(QLowEnergyAdvertisingParameters(),
advertisingData, advertisingData);
}
void virtualbike::bikeProvider()
{
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
return;
}
else
{
emit debug("virtual bike connected");
}
QByteArray value;
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed() * 100);
value.append((char)(normalizeSpeed & 0xFF)); // speed
value.append((char)(normalizeSpeed >> 8) & 0xFF); // speed
value.append((char)(Bike->currentCadence() * 2)); // cadence
value.append((char)(0)); // cadence
value.append((char)Bike->currentResistance()); // resistance
value.append((char)(0)); // resistance
value.append((char)(Bike->watts() & 0xFF)); // watts
value.append((char)(Bike->watts() >> 8) & 0xFF); // watts
value.append(char(Bike->currentHeart())); // Actual value.
QLowEnergyCharacteristic characteristic
= serviceFIT->characteristic((QBluetoothUuid::CharacteristicType)0x2AD2);
Q_ASSERT(characteristic.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
return;
}
writeCharacteristic(serviceFIT, characteristic, value);
//characteristic
// = service->characteristic((QBluetoothUuid::CharacteristicType)0x2AD9); // Fitness Machine Control Point
//Q_ASSERT(characteristic.isValid());
//service->readCharacteristic(characteristic);
if(!this->noHeartService)
{
QByteArray valueHR;
valueHR.append(char(0)); // Flags that specify the format of the value.
valueHR.append(char(Bike->currentHeart())); // Actual value.
QLowEnergyCharacteristic characteristicHR
= serviceHR->characteristic(QBluetoothUuid::HeartRateMeasurement);
Q_ASSERT(characteristicHR.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
return;
}
writeCharacteristic(serviceHR, characteristicHR, valueHR);
}
}
bool virtualbike::connected()
{
if(!leController)
return false;
return leController->state() == QLowEnergyController::ConnectedState;
}
void virtualbike::error(QLowEnergyController::Error newError)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
debug("virtualbike::controller:ERROR " + QString::fromLocal8Bit(metaEnum.valueToKey(newError)));
if(newError != QLowEnergyController::RemoteHostClosedError)
reconnect();
}

58
src/virtualbike.h Normal file
View File

@@ -0,0 +1,58 @@
#ifndef VIRTUALBIKE_H
#define VIRTUALBIKE_H
#include <QObject>
#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 <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include "bike.h"
class virtualbike: public QObject
{
Q_OBJECT
public:
virtualbike(bike* t, bool noWriteResistance = false, bool noHeartService = false);
bool connected();
private:
QLowEnergyController* leController;
QLowEnergyService* serviceHR;
QLowEnergyService* serviceFIT;
QLowEnergyAdvertisingData advertisingData;
QLowEnergyServiceData serviceDataHR;
QLowEnergyServiceData serviceDataFIT;
QTimer bikeTimer;
bike* Bike;
bool noHeartService = false;
void writeCharacteristic(QLowEnergyService* service, QLowEnergyCharacteristic characteristic, QByteArray value);
signals:
void debug(QString string);
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void bikeProvider();
void reconnect();
void error(QLowEnergyController::Error newError);
};
#endif // VIRTUALBIKE_H

View File

@@ -1,16 +1,11 @@
#include "virtualtreadmill.h"
#include <QtMath>
volatile double currentSpeed = 0;
volatile double currentIncline = 0;
volatile uint8_t currentHeart = 0;
volatile double requestSpeed = -1;
volatile double requestIncline = -1;
volatile int8_t requestStart = -1;
volatile int8_t requestStop = -1;
virtualtreadmill::virtualtreadmill()
virtualtreadmill::virtualtreadmill(treadmill* t, bool noHeartService)
{
treadMill = t;
this->noHeartService = noHeartService;
//! [Advertising Data]
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
@@ -60,27 +55,33 @@ virtualtreadmill::virtualtreadmill()
serviceData.addCharacteristic(charData3);
//! [Service Data]
QLowEnergyCharacteristicData charDataHR;
charDataHR.setUuid(QBluetoothUuid::HeartRateMeasurement);
charDataHR.setValue(QByteArray(2, 0));
charDataHR.setProperties(QLowEnergyCharacteristic::Notify);
const QLowEnergyDescriptorData clientConfigHR(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charDataHR.addDescriptor(clientConfigHR);
if(noHeartService == false)
{
QLowEnergyCharacteristicData charDataHR;
charDataHR.setUuid(QBluetoothUuid::HeartRateMeasurement);
charDataHR.setValue(QByteArray(2, 0));
charDataHR.setProperties(QLowEnergyCharacteristic::Notify);
const QLowEnergyDescriptorData clientConfigHR(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charDataHR.addDescriptor(clientConfigHR);
QLowEnergyServiceData serviceDataHR;
serviceDataHR.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceDataHR.setUuid(QBluetoothUuid::HeartRate);
serviceDataHR.addCharacteristic(charDataHR);
serviceDataHR.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceDataHR.setUuid(QBluetoothUuid::HeartRate);
serviceDataHR.addCharacteristic(charDataHR);
}
//! [Start Advertising]
leController = QLowEnergyController::createPeripheral();
Q_ASSERT(leController);
service = leController->addService(serviceData);
serviceHR = leController->addService(serviceDataHR);
if(noHeartService == false)
serviceHR = leController->addService(serviceDataHR);
QObject::connect(service, SIGNAL(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)), this, SLOT(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)));
leController->startAdvertising(QLowEnergyAdvertisingParameters(), advertisingData,
QLowEnergyAdvertisingParameters pars;
pars.setInterval(100, 100);
leController->startAdvertising(pars, advertisingData,
advertisingData);
//! [Start Advertising]
@@ -93,7 +94,7 @@ virtualtreadmill::virtualtreadmill()
void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
qDebug() << "characteristicChanged" << characteristic.uuid().toUInt16() << newValue;
emit debug("characteristicChanged " + QString::number(characteristic.uuid().toUInt16()) + " " + newValue);
char a;
char b;
@@ -108,8 +109,9 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
b = newValue.at(2);
uint16_t uspeed = a + (((uint16_t)b) << 8);
requestSpeed = (double)uspeed / 100.0;
qDebug() << "new requested speed" << requestSpeed;
double requestSpeed = (double)uspeed / 100.0;
treadMill->changeSpeed(requestSpeed);
emit debug("new requested speed " + QString::number(requestSpeed));
}
else if ((char)newValue.at(0)== 0x03) // Set Target Inclination
{
@@ -117,20 +119,21 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
b = newValue.at(2);
int16_t sincline = a + (((int16_t)b) << 8);
requestIncline = (double)sincline / 10.0;
double requestIncline = (double)sincline / 10.0;
if(requestIncline < 0)
requestIncline = 0;
qDebug() << "new requested incline" << requestIncline;
treadMill->changeInclination(requestIncline);
emit debug("new requested incline " +QString::number(requestIncline));
}
else if ((char)newValue.at(0)== 0x07) // Start request
{
requestStart = 1;
qDebug() << "request to start";
treadMill->start();
emit debug("request to start");
}
else if ((char)newValue.at(0)== 0x08) // Stop request
{
requestStop = 1;
qDebug() << "request to stop";
treadMill->stop();
emit debug("request to stop");
}
break;
}
@@ -138,7 +141,12 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
void virtualtreadmill::reconnect()
{
emit debug("virtualtreadmill reconnect");
service = leController->addService(serviceData);
if(noHeartService == false)
serviceHR = leController->addService(serviceDataHR);
if (service)
leController->startAdvertising(QLowEnergyAdvertisingParameters(),
advertisingData, advertisingData);
@@ -146,23 +154,29 @@ void virtualtreadmill::reconnect()
void virtualtreadmill::treadmillProvider()
{
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtualtreadmill connection error");
return;
}
QByteArray value;
value.append(0x08); // Inclination avaiable
value.append((char)0x00);
value.append((char)0x01); // heart rate avaiable
uint16_t normalizeSpeed = (uint16_t)qRound(currentSpeed * 100);
uint16_t normalizeSpeed = (uint16_t)qRound(treadMill->currentSpeed() * 100);
char a = (normalizeSpeed >> 8) & 0XFF;
char b = normalizeSpeed & 0XFF;
QByteArray speedBytes;
speedBytes.append(b);
speedBytes.append(a);
uint16_t normalizeIncline = (uint32_t)qRound(currentIncline * 10);
uint16_t normalizeIncline = (uint32_t)qRound(treadMill->currentInclination() * 10);
a = (normalizeIncline >> 8) & 0XFF;
b = normalizeIncline & 0XFF;
QByteArray inclineBytes;
inclineBytes.append(b);
inclineBytes.append(a);
double ramp = qRadiansToDegrees(qAtan(currentIncline/100));
double ramp = qRadiansToDegrees(qAtan(treadMill->currentInclination()/100));
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
a = (normalizeRamp >> 8) & 0XFF;
b = normalizeRamp & 0XFF;
@@ -176,63 +190,51 @@ void virtualtreadmill::treadmillProvider()
value.append(rampBytes); //ramp angle
value.append(treadMill->currentHeart()); // current heart rate
QLowEnergyCharacteristic characteristic
= service->characteristic((QBluetoothUuid::CharacteristicType)0x2ACD); //TreadmillDataCharacteristicUuid
Q_ASSERT(characteristic.isValid());
service->writeCharacteristic(characteristic, value); // Potentially causes notification.
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtualtreadmill connection error");
return;
}
try {
service->writeCharacteristic(characteristic, value); // Potentially causes notification.
} catch (...) {
emit debug("virtualtreadmill error!");
}
//characteristic
// = service->characteristic((QBluetoothUuid::CharacteristicType)0x2AD9); // Fitness Machine Control Point
//Q_ASSERT(characteristic.isValid());
//service->readCharacteristic(characteristic);
QByteArray valueHR;
valueHR.append(char(0)); // Flags that specify the format of the value.
valueHR.append(char(currentHeart)); // Actual value.
QLowEnergyCharacteristic characteristicHR
= serviceHR->characteristic(QBluetoothUuid::HeartRateMeasurement);
Q_ASSERT(characteristicHR.isValid());
serviceHR->writeCharacteristic(characteristicHR, valueHR); // Potentially causes notification.
QFile* log;
QDomDocument docStatus;
QDomElement docRoot;
QDomElement docTreadmill;
QDomElement docHeart;
log = new QFile("status.xml");
if(!log->open(QIODevice::WriteOnly | QIODevice::Text))
if(noHeartService == false)
{
qDebug() << "Open status.xml for writing failed";
QByteArray valueHR;
valueHR.append(char(0)); // Flags that specify the format of the value.
valueHR.append(char(treadMill->currentHeart())); // Actual value.
QLowEnergyCharacteristic characteristicHR
= serviceHR->characteristic(QBluetoothUuid::HeartRateMeasurement);
Q_ASSERT(characteristicHR.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtualtreadmill connection error");
return;
}
try {
serviceHR->writeCharacteristic(characteristicHR, valueHR); // Potentially causes notification.
} catch (...) {
emit debug("virtualtreadmill error!");
}
}
docRoot = docStatus.createElement("Gym");
docStatus.appendChild(docRoot);
docTreadmill = docStatus.createElement("Treadmill");
docTreadmill.setAttribute("Speed", QString::number(currentSpeed));
docTreadmill.setAttribute("Incline", QString::number(currentIncline));
docRoot.appendChild(docTreadmill);
docHeart = docStatus.createElement("Heart");
docHeart.setAttribute("Rate", QString::number(currentHeart));
docRoot.appendChild(docHeart);
docRoot.setAttribute("Updated", QDateTime::currentDateTime().toString());
QTextStream stream(log);
stream << docStatus.toString();
log->close();
}
uint16_t virtualtreadmill::watts()
bool virtualtreadmill::connected()
{
// calc Watts ref. https://alancouzens.com/blog/Run_Power.html
uint16_t watts=0;
if(currentSpeed > 0)
{
double weight=75.0; // TODO: config need
double pace=60/currentSpeed;
double VO2R=210.0/pace;
double VO2A=(VO2R*weight)/1000.0;
double hwatts=75*VO2A;
double vwatts=((9.8*weight) * (currentIncline/100));
watts=hwatts+vwatts;
}
return watts;
if(!leController)
return false;
return leController->state() == QLowEnergyController::ConnectedState;
}

View File

@@ -22,13 +22,14 @@
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QtXml>
#include "treadmill.h"
class virtualtreadmill: QObject
class virtualtreadmill: public QObject
{
Q_OBJECT
public:
virtualtreadmill();
virtualtreadmill(treadmill* t, bool noHeartService);
bool connected();
private:
QLowEnergyController* leController;
@@ -36,8 +37,14 @@ private:
QLowEnergyService* serviceHR;
QLowEnergyAdvertisingData advertisingData;
QLowEnergyServiceData serviceData;
QTimer treadmillTimer;
uint16_t watts();
QLowEnergyServiceData serviceDataHR;
QTimer treadmillTimer;
treadmill* treadMill;
bool noHeartService = false;
signals:
void debug(QString string);
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<rows>
<row duration="00:02:00" speed="5" inclination="0" forcespeed="1"/>
<row duration="00:03:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:05:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7.7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7.7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7.7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:06:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:01:00" speed="5" inclination="0" forcespeed="1"/>
</rows>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<rows>
<row duration="00:01:00" speed="9.85447" inclination="-1.82658" forcespeed="1"/>
<row duration="00:01:00" speed="10.2993" inclination="-2.91281" forcespeed="1"/>
<row duration="00:01:00" speed="10.2958" inclination="-2.15623" forcespeed="1"/>
<row duration="00:01:00" speed="10.2988" inclination="-0.174779" forcespeed="1"/>
<row duration="00:01:00" speed="10.3375" inclination="-2.49576" forcespeed="1"/>
<row duration="00:01:00" speed="11.0169" inclination="-1.41601" forcespeed="1"/>
<row duration="00:01:00" speed="11.8215" inclination="-2.0302" forcespeed="1"/>
<row duration="00:01:00" speed="11.0147" inclination="-1.19839" forcespeed="1"/>
<row duration="00:01:00" speed="3.66404" inclination="3.11131" forcespeed="1"/>
<row duration="00:01:00" speed="9.80634" inclination="5.75139" forcespeed="1"/>
<row duration="00:01:00" speed="8.71754" inclination="9.91104" forcespeed="1"/>
<row duration="00:01:00" speed="8.99724" inclination="9.40288" forcespeed="1"/>
<row duration="00:01:00" speed="10.11" inclination="4.51039" forcespeed="1"/>
<row duration="00:01:00" speed="10.1726" inclination="4.01078" forcespeed="1"/>
<row duration="00:01:00" speed="9.96776" inclination="6.01941" forcespeed="1"/>
<row duration="00:01:00" speed="9.21979" inclination="9.24099" forcespeed="1"/>
<row duration="00:01:00" speed="9.00303" inclination="9.39684" forcespeed="1"/>
<row duration="00:01:00" speed="7.59236" inclination="7.6656" forcespeed="1"/>
<row duration="00:01:00" speed="9.24165" inclination="5.12895" forcespeed="1"/>
<row duration="00:01:00" speed="6.95364" inclination="9.23258" forcespeed="1"/>
<row duration="00:01:00" speed="10.4115" inclination="6.45437" forcespeed="1"/>
<row duration="00:01:00" speed="9.95344" inclination="5.9678" forcespeed="1"/>
<row duration="00:01:00" speed="10.3448" inclination="2.72599" forcespeed="1"/>
<row duration="00:01:00" speed="10.485" inclination="0.286123" forcespeed="1"/>
<row duration="00:01:00" speed="9.42228" inclination="-5.4127" forcespeed="1"/>
<row duration="00:01:00" speed="11.5566" inclination="-6.5417" forcespeed="1"/>
<row duration="00:01:00" speed="10.3381" inclination="-3.48228" forcespeed="1"/>
<row duration="00:01:00" speed="12.2822" inclination="-2.34486" forcespeed="1"/>
<row duration="00:01:00" speed="10.4139" inclination="4.72445" forcespeed="1"/>
<row duration="00:01:00" speed="8.69461" inclination="-5.38266" forcespeed="1"/>
<row duration="00:01:00" speed="10.1889" inclination="-3.06215" forcespeed="1"/>
<row duration="00:01:00" speed="9.98059" inclination="2.46477" forcespeed="1"/>
<row duration="00:01:00" speed="10.9802" inclination="3.87972" forcespeed="1"/>
<row duration="00:01:00" speed="9.05151" inclination="-0.0662914" forcespeed="1"/>
<row duration="00:01:00" speed="3.99165" inclination="-19.9917" forcespeed="1"/>
<row duration="00:01:00" speed="8.7631" inclination="-12.0505" forcespeed="1"/>
<row duration="00:01:00" speed="6.64707" inclination="-16.2478" forcespeed="1"/>
<row duration="00:01:00" speed="6.51384" inclination="-14.8299" forcespeed="1"/>
<row duration="00:01:00" speed="8.57592" inclination="-10.8443" forcespeed="1"/>
<row duration="00:01:00" speed="8.401" inclination="-8.92751" forcespeed="1"/>
<row duration="00:01:00" speed="11.2526" inclination="-2.45276" forcespeed="1"/>
<row duration="00:01:00" speed="11.6882" inclination="-1.12934" forcespeed="1"/>
<row duration="00:01:00" speed="13.9318" inclination="-0.387599" forcespeed="1"/>
<row duration="00:01:00" speed="10.6752" inclination="-0.112417" forcespeed="1"/>
<row duration="00:01:00" speed="10.2653" inclination="-2.33797" forcespeed="1"/>
<row duration="00:01:00" speed="10.7095" inclination="-2.18497" forcespeed="1"/>
<row duration="00:01:00" speed="9.3889" inclination="2.10888" forcespeed="1"/>
<row duration="00:01:00" speed="11.1298" inclination="-3.01893" forcespeed="1"/>
<row duration="00:01:00" speed="11.9722" inclination="-1.90441" forcespeed="1"/>
<row duration="00:01:00" speed="12.0696" inclination="-1.73991" forcespeed="1"/>
<row duration="00:01:00" speed="12.9631" inclination="-1.80512" forcespeed="1"/>
<row duration="00:01:00" speed="12.336" inclination="-1.89688" forcespeed="1"/>
<row duration="00:01:00" speed="11.5617" inclination="-0.467066" forcespeed="1"/>
<row duration="00:01:00" speed="11.3423" inclination="-1.63987" forcespeed="1"/>
<row duration="00:01:00" speed="11.5684" inclination="0" forcespeed="1"/>
<row duration="00:01:00" speed="10.971" inclination="0.656273" forcespeed="1"/>
<row duration="00:01:00" speed="11.2002" inclination="-0.642841" forcespeed="1"/>
<row duration="00:01:00" speed="11.1976" inclination="-0.910918" forcespeed="1"/>
<row duration="00:01:00" speed="10.4373" inclination="-0.977264" forcespeed="1"/>
<row duration="00:01:00" speed="10.2949" inclination="-0.582811" forcespeed="1"/>
<row duration="00:01:00" speed="10.9175" inclination="0.0549612" forcespeed="1"/>
<row duration="00:01:00" speed="9.11082" inclination="2.30495" forcespeed="1"/>
<row duration="00:01:00" speed="10.4257" inclination="1.20855" forcespeed="1"/>
<row duration="00:01:00" speed="10.9692" inclination="1.42217" forcespeed="1"/>
<row duration="00:01:00" speed="10.5789" inclination="1.24777" forcespeed="1"/>
<row duration="00:01:00" speed="10.5046" inclination="0.971002" forcespeed="1"/>
<row duration="00:01:00" speed="10.5973" inclination="0.339712" forcespeed="1"/>
<row duration="00:01:00" speed="10.4905" inclination="1.14389" forcespeed="1"/>
<row duration="00:01:00" speed="11.7968" inclination="1.5767" forcespeed="1"/>
<row duration="00:01:00" speed="10.9239" inclination="1.70268" forcespeed="1"/>
<row duration="00:01:00" speed="12.6751" inclination="2.46152" forcespeed="1"/>
</rows>