Compare commits

...

188 Commits

Author SHA1 Message Date
Roberto Viola
42b711beb4 writing characteristic without confirmation for T900C
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-05 14:39:48 +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
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
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
4712d5780a adding ability to save and load training program (not finished yet) 2020-10-08 14:19:09 +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
cagnulein
5774e725a7 started adding a gui 2020-10-02 16:38:07 +02:00
73 changed files with 49797 additions and 219 deletions

View File

@@ -1,25 +1,69 @@
# qdomyos-zwift
Zwift bridge for Domyos treadmills
Zwift bridge for Treadmills!
<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
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
### Installation from source
$ sudo apt upgrade && sudo apt update # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ sudo apt install libqt5bluetooth5
$ sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default
$ sudo hciconfig hci0 leadv 0
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd src
$ qmake
$ make -j4
$ sudo ./qdomyos-zwift
### MacOs installation
You will need to (at a minimum) to install the xcode Command Line Tools (CLI) thanks to @richardwait
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
### No gui version
run as
$ sudo ./qdomyos-zwift -no-gui
### Reference

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

123
src/bike.cpp Normal file
View File

@@ -0,0 +1,123 @@
#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
if(currentSpeed() > 0)
{
const uint16_t watt_cad40_min = 15;
const uint16_t watt_cad40_max = 30;
const uint16_t watt_cad45_min = 20;
const uint16_t watt_cad45_max = 35;
const uint16_t watt_cad50_min = 25;
const uint16_t watt_cad50_max = 45;
const uint16_t watt_cad55_min = 30;
const uint16_t watt_cad55_max = 55;
const uint16_t watt_cad60_min = 35;
const uint16_t watt_cad60_max = 70;
const uint16_t watt_cad65_min = 40;
const uint16_t watt_cad65_max = 90;
const uint16_t watt_cad70_min = 50;
const uint16_t watt_cad70_max = 110;
const uint16_t watt_cad75_min = 55;
const uint16_t watt_cad75_max = 135;
const uint16_t watt_cad80_min = 65;
const uint16_t watt_cad80_max = 160;
const uint16_t watt_cad85_min = 75;
const uint16_t watt_cad85_max = 190;
const uint16_t watt_cad90_min = 85;
const uint16_t watt_cad90_max = 225;
const uint16_t watt_cad95_min = 100;
const uint16_t watt_cad95_max = 260;
const uint16_t watt_cad100_min = 110;
const uint16_t watt_cad100_max = 300;
const uint16_t watt_cad105_min = 125;
const uint16_t watt_cad105_max = 340;
const uint16_t watt_cad110_min = 140;
const uint16_t watt_cad110_max = 385;
const uint16_t watt_cad115_min = 155;
const uint16_t watt_cad115_max = 435;
const uint16_t watt_cad120_min = 170;
const uint16_t watt_cad120_max = 485;
const uint16_t watt_cad125_min = 190;
const uint16_t watt_cad125_max = 540;
const uint16_t watt_cad130_min = 210;
const uint16_t watt_cad130_max = 595;
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

138
src/bluetooth.cpp Normal file
View File

@@ -0,0 +1,138 @@
#include "bluetooth.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
bluetooth::bluetooth(bool logs, QString deviceName, bool noWriteResistance, bool noHeartService) : QObject(nullptr)
{
QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
filterDevice = deviceName;
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
if(logs)
{
debugCommsLog = new QFile("debug-" + QDateTime::currentDateTime().toString() + ".log");
debugCommsLog->open(QIODevice::WriteOnly | QIODevice::Unbuffered);
}
#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 + '\n';
if(debugCommsLog)
{
debugCommsLog->write(debug.toLocal8Bit());
debugCommsLog->flush();
qDebug() << debug;
}
}
void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
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);
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();
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;
}

57
src/bluetooth.h Normal file
View File

@@ -0,0 +1,57 @@
#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);
bluetoothdevice* device();
private:
QFile* debugCommsLog = 0;
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
domyostreadmill* domyos = 0;
domyosbike* domyosBike = 0;
toorxtreadmill* toorx = 0;
trxappgateusbtreadmill* trxappgateusb = 0;
QString filterDevice = "";
bool noWriteResistance = false;
bool noHeartService = false;
signals:
void deviceConnected();
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>

503
src/domyosbike.cpp Normal file
View File

@@ -0,0 +1,503 @@
#include "domyosbike.h"
#include "virtualbike.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService)
{
refresh = new QTimer(this);
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)
{
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 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) );
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};
display[3] = ((((uint16_t)odometer())) >> 8) & 0xFF;
display[4] = (((uint16_t)odometer())) & 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()
{
uint8_t noOpData[] = { 0xf0, 0xac, 0x9c };
// stop tape
uint8_t initDataF0C800B8[] = { 0xf0, 0xc8, 0x00, 0xb8 };
static uint8_t sec1 = 0;
//qDebug() << treadmill.isValid() << m_control->state() << gattCommunicationChannelService << gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
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)
{
if(currentSpeed() > 0.0)
elapsed += ((double)refresh->interval() / 1000.0);
// updating the treadmill console every second
if(sec1++ == (1000 / refresh->interval()))
{
sec1 = 0;
updateDisplay(elapsed);
}
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true);
if(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;
}
}
}
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 QTime lastRefresh = QTime::currentTime();
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);
Heart = newValue.at(18);
CrankRevs += ((double)(lastRefresh.msecsTo(QTime::currentTime())) * ((double)Cadence / 60000.0) );
LastCrankEventTime += (uint16_t)((lastRefresh.msecsTo(QTime::currentTime())) * 1.024);
lastRefresh = QTime::currentTime();
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));
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
Speed = speed;
KCal = kcal;
Distance = distance;
}
double domyosbike::GetSpeedFromPacket(QByteArray packet)
{
uint8_t convertedData = (uint8_t)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");
writeCharacteristic(initData2, sizeof(initData2), "init");
writeCharacteristic(initDataStart, sizeof(initDataStart), "init");
writeCharacteristic(initDataStart2, sizeof(initDataStart2), "init");
writeCharacteristic(initDataStart3, sizeof(initDataStart3), "init");
writeCharacteristic(initDataStart4, sizeof(initDataStart4), "init");
writeCharacteristic(initDataStart5, sizeof(initDataStart5), "init");
writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init");
writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init");
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init");
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init");
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init");
if(startTape)
{
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init");
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init");
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init");
}
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 };
// 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,
0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff
};
uint8_t initDataStart7[] = { 0xff, 0xff, 0x8d }; // power on bt icon
uint8_t initDataStart8[] =
{
0xf0, 0xcb, 0x02, 0x00, 0x08, 0xff, 0x01, 0x00, 0x00, 0x01,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00
};
uint8_t initDataStart9[] = { 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xcb }; // power on bt word
uint8_t initDataStart10[] =
{
0xf0, 0xca, 0x00, 0xba
};
writeCharacteristic(initData1, sizeof(initData1), "init");
writeCharacteristic(initData2, sizeof(initData2), "init");
writeCharacteristic(initDataStart, sizeof(initDataStart), "init");
writeCharacteristic(initDataStart2, sizeof(initDataStart2), "init");
writeCharacteristic(initDataStart3, sizeof(initDataStart3), "init");
writeCharacteristic(initDataStart4, sizeof(initDataStart4), "init");
writeCharacteristic(initDataStart5, sizeof(initDataStart5), "init");
writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init");
writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init");
writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init");
writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init");
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init");
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init");
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init");
writeCharacteristic(initData1, sizeof(initData1), "init");
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init");
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());
}
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();
}

94
src/domyosbike.h Normal file
View File

@@ -0,0 +1,94 @@
#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 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);
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;
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

@@ -3,6 +3,7 @@
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
// set speed and incline to 0
uint8_t initData1[] = { 0xf0, 0xc8, 0x01, 0xb9 };
@@ -13,39 +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, 0xc8, 0x01, 0xb9 };
uint8_t initDataStart6[] =
{
0xf0, 0xad, 0xff, 0xff, 0x00, 0x0a, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart7[] = { 0xff, 0xff, 0x95 };
uint8_t initDataStart8[] =
{
0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart9[] = { 0xff, 0xff, 0x8b };
uint8_t initDataStart10[] =
{
0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x03, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart11[] = { 0xff, 0xff, 0x8a };
uint8_t initDataStart12[] =
{
0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x04, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart13[] = { 0xff, 0xff, 0x9e };
#endif
// main startup sequence
uint8_t initDataStart[] = { 0xf0, 0xa3, 0x93 };
uint8_t initDataStart2[] = { 0xf0, 0xa4, 0x94 };
@@ -81,96 +49,101 @@ 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;
QFile* debugCommsLog;
domyostreadmill::domyostreadmill()
{
QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
QTimer* refresh = new QTimer(this);
debugCommsLog = new QFile("debug-" + QDateTime::currentDateTime().toString() + ".log");
debugCommsLog->open(QIODevice::WriteOnly | QIODevice::Unbuffered);
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->setTimerType(Qt::PreciseTimer);
refresh->start(200);
}
void domyostreadmill::debug(QString text)
{
QString debug = QDateTime::currentDateTime().toString() + text + '\n';
debugCommsLog->write(debug.toLocal8Bit());
qDebug() << debug;
}
void domyostreadmill::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log)
{
QEventLoop loop;
/* QEventLoop loop;
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)data, data_len));
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)),
&loop, SLOT(quit()));
*/
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)data, data_len), QLowEnergyService::WriteWithoutResponse);
if(!disable_log)
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
/*
loop.exec();
if(gattCommunicationChannelService->error() != QLowEnergyService::NoError)
{
debug("domyostreadmill::writeCharacteristic ERROR " + QString::number(gattCommunicationChannelService->error()));
emit disconnect();
}*/
}
void domyostreadmill::updateDisplay(uint16_t elapsed)
{
uint8_t writeIncline[] = {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};
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};
writeIncline[3] = (elapsed >> 8) & 0xFF; // high byte for elapsed time (in seconds)
writeIncline[4] = (elapsed & 0xFF); // low byte for elasped time (in seconds)
display[3] = (elapsed / 60) & 0xFF; // high byte for elapsed time (in seconds)
display[4] = (elapsed % 60 & 0xFF); // low byte for elasped time (in seconds)
writeIncline[12] = currentHeart;
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
}
for(uint8_t i=0; i<sizeof(writeIncline)-1; i++)
display[12] = currentHeart();
//display[13] = ((((uint8_t)currentInclination()) * 10) >> 8) & 0xFF;
//display[14] = (((uint8_t)currentInclination()) * 10) & 0xFF;
//display[13] = ((((uint8_t)calories())) >> 8) & 0xFF;
//display[14] = (((uint8_t)calories())) & 0xFF;
//display[20] = (uint8_t)currentSpeed();
for(uint8_t i=0; i<sizeof(display)-1; i++)
{
//qDebug() << QString::number(writeIncline[i], 16);
writeIncline[26] += writeIncline[i]; // the last byte is a sort of a checksum
display[26] += display[i]; // the last byte is a sort of a checksum
}
//qDebug() << "writeIncline crc" << QString::number(writeIncline[26], 16);
writeCharacteristic(writeIncline, 20, "updateDisplay speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline) + " elapsed=" + QString::number(elapsed) );
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "updateDisplay speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline) + " elapsed=" + QString::number(elapsed) );
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, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00};
writeIncline[4] = ((uint16_t)(requestSpeed*10) >> 8) & 0xFF;
@@ -192,58 +165,157 @@ void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestInc
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 uint32_t counter = 0;
{
static uint8_t sec1 = 0;
static QTime lastTime;
static bool first = true;
static uint8_t lastIncompletePacket = 0;
//qDebug() << treadmill.isValid() << m_control->state() << gattCommunicationChannelService << gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
if(lastIncompletePacket > 3 && incompletePackets)
{
lastIncompletePacket = 0;
incompletePackets = false;
debug("incompletePackets timeout");
}
if(incompletePackets)
{
debug("incompletePackets add " + QString::number(lastIncompletePacket));
lastIncompletePacket++;
}
else
lastIncompletePacket = 0;
if(m_control->state() == QLowEnergyController::UnconnectedState)
{
emit disconnected();
return;
}
if(initRequest)
{
initRequest = false;
btinit();
btinit(false);
}
else if(treadmill.isValid() &&
else if(bttreadmill.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotifyCharacteristic.isValid() &&
initDone)
{
if(currentSpeed > 0.0)
counter++;
QTime current = QTime::currentTime();
if(currentSpeed() > 0.0 && !first)
elapsed += (((double)lastTime.msecsTo(current)) / ((double)1000.0));
lastTime = current;
elevationAcc += (currentSpeed() / 3600.0) * 1000 * (currentInclination() / 100) * (refresh->interval() / 1000);
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true);
// updating the treadmill console every second
if(sec1++ >= (1000 / refresh->interval()))
{
if(incompletePackets == false)
{
sec1 = 0;
updateDisplay(elapsed);
return;
}
}
if(incompletePackets == false)
writeCharacteristic(noOpData, sizeof(noOpData), "noOp");
else
debug("polling blocked by other packet");
// byte 3 - 4 = elapsed time
// byte 17 = inclination
if(requestSpeed != -1)
if(incompletePackets == false)
{
debug("writing speed " + QString::number(requestSpeed));
forceSpeedOrIncline(requestSpeed, currentIncline);
requestSpeed = -1;
}
if(requestIncline != -1)
{
debug("writing incline " + QString::number(requestIncline));
forceSpeedOrIncline(currentSpeed, requestIncline);
requestIncline = -1;
}
if(requestStart != -1)
{
debug("starting...");
btinit();
requestStart = -1;
}
if(requestStop != -1)
{
debug("stopping...");
writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
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;
}
}
}
else
{
debug("domyostreadmill::update nothing to do " + QString::number(bttreadmill.isValid()) + " " +
QString::number(m_control->state() == QLowEnergyController::DiscoveredState) + " " +
gattWriteCharacteristic.isValid() + " " +
gattNotifyCharacteristic.isValid() + " " +
initDone);
}
first = false;
}
void domyostreadmill::serviceDiscovered(const QBluetoothUuid &gatt)
@@ -256,44 +328,111 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
{
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
Q_UNUSED(characteristic);
static QTime lastTime;
static bool first = true;
QByteArray value = newValue;
debug(" << " + newValue.toHex(' '));
debug(" << " + QString::number(value.length()) + " " + value.toHex(' '));
if (lastPacket.length() && lastPacket == newValue)
if (lastPacket.length() && lastPacket == value)
{
if(incompletePackets)
debug("packet corrupted");
incompletePackets = false;
return;
}
lastPacket = newValue;
if (newValue.length() != 26)
QByteArray startBytes;
startBytes.append(0xf0);
startBytes.append(0xbc);
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))
{
incompletePackets = false;
debug("...final bytes received");
lastPacket.append(value);
value = lastPacket;
}
lastPacket = value;
if (value.length() != 26)
{
// semaphore for any writing packets (for example, update display)
if(value.length() == 20 && (value.startsWith(startBytes) || value.startsWith(startBytes2)))
{
debug("waiting for other bytes...");
incompletePackets = true;
}
else
{
if(incompletePackets)
debug("packet corrupted");
incompletePackets = false;
}
debug("packet ignored");
return;
}
if (newValue.at(22) == 0x06)
if (value.at(22) == 0x06)
{
debug("start button pressed!");
requestStart = 1;
}
else if (newValue.at(22) == 0x07)
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)newValue.at(1) != 0xbc && newValue.at(2) != 0x04) // intense run, these are the bytes for the inclination and speed status
/*if ((uint8_t)value.at(1) != 0xbc && value.at(2) != 0x04) // intense run, these are the bytes for the inclination and speed status
return;*/
double speed = GetSpeedFromPacket(newValue);
double incline = GetInclinationFromPacket(newValue);
double speed = GetSpeedFromPacket(value);
double incline = GetInclinationFromPacket(value);
double kcal = GetKcalFromPacket(value);
double distance = GetDistanceFromPacket(value);
currentHeart = newValue.at(18);
Heart = value.at(18);
FanSpeed = value.at(23);
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(currentHeart));
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));
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
currentSpeed = speed;
currentIncline = incline;
Speed = speed;
Inclination = incline;
KCal = kcal;
Distance = distance;
lastTime = QTime::currentTime();
first = false;
}
double domyostreadmill::GetSpeedFromPacket(QByteArray packet)
@@ -303,6 +442,19 @@ 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);
@@ -311,7 +463,7 @@ double domyostreadmill::GetInclinationFromPacket(QByteArray packet)
return data;
}
void domyostreadmill::btinit()
void domyostreadmill::btinit(bool startTape)
{
writeCharacteristic(initData1, sizeof(initData1), "init");
writeCharacteristic(initData2, sizeof(initData2), "init");
@@ -325,9 +477,12 @@ void domyostreadmill::btinit()
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init");
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init");
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init");
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init");
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init");
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init");
if(startTape)
{
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init");
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init");
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init");
}
initDone = true;
}
@@ -336,7 +491,6 @@ void domyostreadmill::stateChanged(QLowEnergyService::ServiceState state)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
debug("BTLE stateChanged " + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
if(state == QLowEnergyService::ServiceDiscovered)
{
//qDebug() << gattCommunicationChannelService->characteristics();
@@ -358,12 +512,11 @@ void domyostreadmill::stateChanged(QLowEnergyService::ServiceState state)
// ******************************************* virtual treadmill init *************************************
static uint8_t first = 0;
static virtualtreadmill* v;
Q_UNUSED(v);
if(!first)
{
debug("creating virtual treadmill interface...");
v = new virtualtreadmill();
virtualTreadMill = new virtualtreadmill(this);
connect(virtualTreadMill,&virtualtreadmill::debug ,this,&domyostreadmill::debug);
}
first = 1;
// ********************************************************************************************************
@@ -400,23 +553,23 @@ void domyostreadmill::serviceScanDone(void)
void domyostreadmill::errorService(QLowEnergyService::ServiceError err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
debug("domyostreadmill::errorService" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
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());
debug("domyostreadmill::error " + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
m_control->disconnect();
}
void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("Domyos"))
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()),
@@ -429,7 +582,7 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
Q_UNUSED(error);
Q_UNUSED(this);
debug("Cannot connect to remote device.");
exit(1);
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
@@ -439,7 +592,7 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
debug("LowEnergy controller disconnected");
exit(2);
emit disconnected();
});
// Connect
@@ -447,3 +600,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,27 +18,49 @@
#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();
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);
void forceSpeedOrIncline(double requestSpeed, double requestIncline);
void updateDisplay(uint16_t elapsed);
void btinit();
void btinit(bool startTape);
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false);
void debug(QString text);
void startDiscover();
double DistanceCalculated = 0;
volatile bool incompletePackets = false;
QTimer* refresh;
virtualtreadmill* virtualTreadMill = 0;
signals:
void disconnected();
void debug(QString string);
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
private slots:
@@ -48,8 +70,7 @@ private slots:
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);

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

21
src/icons.qrc Normal file
View File

@@ -0,0 +1,21 @@
<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>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
src/icons/cadence.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 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: 24 KiB

BIN
src/icons/inclination.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
src/icons/kcal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
src/icons/odometer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
src/icons/pace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
src/icons/resistance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/icons/speed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 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: 84 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,98 @@
#include <QtCore/qcoreapplication.h>
#include <QApplication>
#include <QStyleFactory>
#include "virtualtreadmill.h"
#include "domyostreadmill.h"
#include "bluetooth.h"
#include "mainwindow.h"
bool nologs = false;
bool noWriteResistance = false;
bool noHeartService = false;
QString trainProgram;
QString deviceName = "";
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-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], "-train"))
{
trainProgram = argv[++i];
}
if (!qstrcmp(argv[i], "-name"))
{
deviceName = 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;
}
}
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);
#endif
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
//virtualtreadmill* V = new virtualtreadmill();
domyostreadmill* D = new domyostreadmill();
bluetooth* bl = new bluetooth(!nologs, deviceName, noWriteResistance, noHeartService);
//Q_UNUSED(V);
Q_UNUSED(D);
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();
return app->exec();
}

541
src/mainwindow.cpp Normal file
View File

@@ -0,0 +1,541 @@
#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);
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*)((treadmill*)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->groupBox_2->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_groupBox_2_clicked()
{
if(!trainProgram)
createTrainProgram(QList<trainrow>());
trainProgram->enabled = ui->groupBox_2->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) + "%");
}
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() {}
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()
{
}

78
src/mainwindow.h Normal file
View File

@@ -0,0 +1,78 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QDialog>
#include <QTimer>
#include <QTime>
#include <QDebug>
#include <QTableWidgetItem>
#include "trainprogram.h"
#include "domyostreadmill.h"
namespace Ui {
class MainWindow;
}
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());
};
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_groupBox_2_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

1177
src/mainwindow.ui Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,6 @@
QT -= gui
QT += bluetooth
QT += bluetooth widgets xml positioning charts
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 +14,21 @@ 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 \
main.cpp \
virtualtreadmill.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 +36,24 @@ 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
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

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>

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,391 @@
#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(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);
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);
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());
}
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

285
src/virtualbike.cpp Normal file
View File

@@ -0,0 +1,285 @@
#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);
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;
QDataStream replyDs(&reply, QIODevice::ReadWrite);
replyDs.setByteOrder(QDataStream::LittleEndian);
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));
replyDs << (quint8)FTMS_RESPONSE_CODE << (quint8)FTMS_SET_TARGET_RESISTANCE_LEVEL << (quint8)FTMS_SUCCESS ;
}
else if((char)newValue.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS) // simulation parameter
{
emit debug("indoor bike simulation parameters");
replyDs << (quint8)FTMS_RESPONSE_CODE << (quint8)FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS << (quint8)FTMS_SUCCESS ;
}
else if((char)newValue.at(0) == FTMS_START_RESUME)
{
emit debug("start simulation!");
replyDs << (quint8)FTMS_RESPONSE_CODE << (quint8)FTMS_START_RESUME << (quint8)FTMS_SUCCESS ;
}
QLowEnergyCharacteristic characteristic
= serviceFIT->characteristic((QBluetoothUuid::CharacteristicType)0x2AD9);
Q_ASSERT(characteristic.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
return;
}
try {
serviceFIT->writeCharacteristic(characteristic, reply);
} catch (...) {
emit debug("virtual bike error!");
}
break;
}
}
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;
}
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.
value.append(char(0)); // 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;
}
try {
serviceFIT->writeCharacteristic(characteristic, value); // Potentially causes notification.
} catch (...) {
emit debug("virtual bike error!");
}
//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;
}
try {
serviceHR->writeCharacteristic(characteristicHR, valueHR); // Potentially causes notification.
} catch (...) {
emit debug("virtual bike error!");
}
}
}
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)));
reconnect();
}

56
src/virtualbike.h Normal file
View File

@@ -0,0 +1,56 @@
#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;
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,10 @@
#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)
{
treadMill = t;
//! [Advertising Data]
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
@@ -68,7 +62,6 @@ virtualtreadmill::virtualtreadmill()
QByteArray(2, 0));
charDataHR.addDescriptor(clientConfigHR);
QLowEnergyServiceData serviceDataHR;
serviceDataHR.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceDataHR.setUuid(QBluetoothUuid::HeartRate);
serviceDataHR.addCharacteristic(charDataHR);
@@ -81,7 +74,9 @@ virtualtreadmill::virtualtreadmill()
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]
@@ -94,7 +89,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;
@@ -109,8 +104,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
{
@@ -118,20 +114,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;
}
@@ -139,7 +136,9 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
void virtualtreadmill::reconnect()
{
emit debug("virtualtreadmill reconnect");
service = leController->addService(serviceData);
serviceHR = leController->addService(serviceDataHR);
if (service)
leController->startAdvertising(QLowEnergyAdvertisingParameters(),
advertisingData, advertisingData);
@@ -147,23 +146,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);
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;
@@ -180,7 +185,16 @@ void virtualtreadmill::treadmillProvider()
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
@@ -189,27 +203,25 @@ void virtualtreadmill::treadmillProvider()
QByteArray valueHR;
valueHR.append(char(0)); // Flags that specify the format of the value.
valueHR.append(char(currentHeart)); // Actual value.
valueHR.append(char(treadMill->currentHeart())); // Actual value.
QLowEnergyCharacteristic characteristicHR
= serviceHR->characteristic(QBluetoothUuid::HeartRateMeasurement);
Q_ASSERT(characteristicHR.isValid());
serviceHR->writeCharacteristic(characteristicHR, valueHR); // Potentially causes notification.
}
uint16_t virtualtreadmill::watts()
{
// calc Watts ref. https://alancouzens.com/blog/Run_Power.html
uint16_t watts=0;
if(currentSpeed > 0)
if(leController->state() != QLowEnergyController::ConnectedState)
{
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;
emit debug("virtualtreadmill connection error");
return;
}
try {
serviceHR->writeCharacteristic(characteristicHR, valueHR); // Potentially causes notification.
} catch (...) {
emit debug("virtualtreadmill error!");
}
return watts;
}
bool virtualtreadmill::connected()
{
if(!leController)
return false;
return leController->state() == QLowEnergyController::ConnectedState;
}

View File

@@ -22,11 +22,14 @@
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
class virtualtreadmill: QObject
#include "treadmill.h"
class virtualtreadmill: public QObject
{
Q_OBJECT
public:
virtualtreadmill();
virtualtreadmill(treadmill* t);
bool connected();
private:
QLowEnergyController* leController;
@@ -34,8 +37,12 @@ private:
QLowEnergyService* serviceHR;
QLowEnergyAdvertisingData advertisingData;
QLowEnergyServiceData serviceData;
QTimer treadmillTimer;
uint16_t watts();
QLowEnergyServiceData serviceDataHR;
QTimer treadmillTimer;
treadmill* treadMill;
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>