Compare commits
188 Commits
startup_se
...
t900c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b711beb4 | ||
|
|
37c3703283 | ||
|
|
1260e1efc4 | ||
|
|
62a76f5f13 | ||
|
|
147ca95be1 | ||
|
|
53fde43c3c | ||
|
|
09defca49b | ||
|
|
541b150763 | ||
|
|
ac77d22eef | ||
|
|
aeb2d5d1bd | ||
|
|
60b00b978e | ||
|
|
e924694fea | ||
|
|
d510e61234 | ||
|
|
7f58393b5b | ||
|
|
4e7b73ab8d | ||
|
|
7b2a81e2f5 | ||
|
|
0fa8873e19 | ||
|
|
eb0dd0c618 | ||
|
|
b7fdbbed90 | ||
|
|
13f341b6a9 | ||
|
|
90523d388c | ||
|
|
32b4ba21de | ||
|
|
fbe4571734 | ||
|
|
8a248f7451 | ||
|
|
bee124bdcf | ||
|
|
d015149365 | ||
|
|
27f207b022 | ||
|
|
54c7acf263 | ||
|
|
53e25c8592 | ||
|
|
24600b0a01 | ||
|
|
850ea9144a | ||
|
|
bde4c5f5cc | ||
|
|
66f6f6ca97 | ||
|
|
209da708c9 | ||
|
|
8243c936b5 | ||
|
|
d35b1fa1ca | ||
|
|
9a6f4617b6 | ||
|
|
fee67bb812 | ||
|
|
895d2c31a0 | ||
|
|
7d37e333f1 | ||
|
|
70cf5040ee | ||
|
|
53b2bd4516 | ||
|
|
d77fba6734 | ||
|
|
a7dcac02df | ||
|
|
bb0a9cde92 | ||
|
|
f279c3689d | ||
|
|
5b36ad9e4f | ||
|
|
d5ddc4cabd | ||
|
|
ea62337b16 | ||
|
|
15f24b8d0c | ||
|
|
dacbb475c8 | ||
|
|
6f2b19b4aa | ||
|
|
b71ca62110 | ||
|
|
24da757c5e | ||
|
|
fe1ec40f6c | ||
|
|
9cbd054ab4 | ||
|
|
62ae1f5c6d | ||
|
|
d57f265315 | ||
|
|
ce56464046 | ||
|
|
a162e0dcfb | ||
|
|
f0074697d6 | ||
|
|
e25b32bba4 | ||
|
|
ddbd41e79f | ||
|
|
cecd624d13 | ||
|
|
074f5d60f9 | ||
|
|
8795e5ec3c | ||
|
|
b23183e308 | ||
|
|
663ed67a02 | ||
|
|
586ba40527 | ||
|
|
f5d85eb1ae | ||
|
|
1af0246490 | ||
|
|
40336b70c8 | ||
|
|
0460de0fe7 | ||
|
|
61fde67eaf | ||
|
|
e4ee05d2a7 | ||
|
|
f49d1e69ff | ||
|
|
4118ed914b | ||
|
|
5564720b56 | ||
|
|
6d88cef84c | ||
|
|
671c262288 | ||
|
|
d76ec7e32e | ||
|
|
5d3b9b3645 | ||
|
|
f3e6fa9d61 | ||
|
|
4da9566713 | ||
|
|
a43b7b393c | ||
|
|
a6c369cb0b | ||
|
|
60a8f7b93f | ||
|
|
9f49d0eee9 | ||
|
|
7b4a3d3aca | ||
|
|
6aeb3c475f | ||
|
|
04c8cb8be5 | ||
|
|
34e69f55ae | ||
|
|
19dcb2e600 | ||
|
|
45d118b90e | ||
|
|
384deeda16 | ||
|
|
dc5290ced9 | ||
|
|
900f364cfe | ||
|
|
fd819219da | ||
|
|
91db440047 | ||
|
|
98e5e50017 | ||
|
|
a93408aecd | ||
|
|
20af3107b7 | ||
|
|
23309060a4 | ||
|
|
26a64bce34 | ||
|
|
117fce8f7c | ||
|
|
ebc842060c | ||
|
|
d172350a13 | ||
|
|
8426f4640b | ||
|
|
e31c8b6ff9 | ||
|
|
bb2909750f | ||
|
|
11d560d14b | ||
|
|
e774fbf575 | ||
|
|
4c2d82abf2 | ||
|
|
b5cd1d5915 | ||
|
|
c5dd48219a | ||
|
|
2320ef4124 | ||
|
|
d3d54b19dc | ||
|
|
30a6008c82 | ||
|
|
0ca04756f3 | ||
|
|
8a1ee08d50 | ||
|
|
9fae936586 | ||
|
|
63c36bbafc | ||
|
|
2a59e75e57 | ||
|
|
15fdaec3c6 | ||
|
|
29a2173e35 | ||
|
|
88dfacb0c3 | ||
|
|
06cb2d9586 | ||
|
|
b28effc4c0 | ||
|
|
0ddd953b2e | ||
|
|
d5e2e98429 | ||
|
|
1e1c977332 | ||
|
|
df16d94438 | ||
|
|
9425e751f2 | ||
|
|
eb8d354111 | ||
|
|
78c9c86227 | ||
|
|
7cc40ffe30 | ||
|
|
9e6a5ca4af | ||
|
|
37ec561409 | ||
|
|
ba8df2c2eb | ||
|
|
4b86f77a22 | ||
|
|
e43276e52b | ||
|
|
33fdd4c979 | ||
|
|
87f8887ef3 | ||
|
|
ed7e4c6bf2 | ||
|
|
12992df557 | ||
|
|
8d42d530cc | ||
|
|
7b9bd00ff4 | ||
|
|
5e7a8d938b | ||
|
|
a0c1e1b645 | ||
|
|
aa53956a35 | ||
|
|
cd1c10a090 | ||
|
|
c21e337bdd | ||
|
|
1d23ac4b81 | ||
|
|
bbeaa5ec95 | ||
|
|
9f6a4de4ac | ||
|
|
ee1c3e0118 | ||
|
|
81ac8909c8 | ||
|
|
3a45935617 | ||
|
|
c89c381177 | ||
|
|
ea57069f33 | ||
|
|
bf40c460a5 | ||
|
|
338b19f664 | ||
|
|
9f9000427f | ||
|
|
92cd9baea3 | ||
|
|
e21ad70ea9 | ||
|
|
d7ac459a3d | ||
|
|
d58db4100f | ||
|
|
3c7cb254e6 | ||
|
|
259b53e8e0 | ||
|
|
b73092bd8f | ||
|
|
787b9aa2c2 | ||
|
|
73a0bd7c65 | ||
|
|
d068526e55 | ||
|
|
8945063f30 | ||
|
|
4625bccad3 | ||
|
|
5c723375d7 | ||
|
|
1212bc83f8 | ||
|
|
efc9788c89 | ||
|
|
42c43158e6 | ||
|
|
1d6c46a32b | ||
|
|
3ba0219ce4 | ||
|
|
c2c5b7746f | ||
|
|
4944f6d48d | ||
|
|
33a478b1ae | ||
|
|
4712d5780a | ||
|
|
76d3139d79 | ||
|
|
8fb8ed6f44 | ||
|
|
5774e725a7 |
58
README.md
@@ -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 on Linux
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
### 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
BIN
docs/CPS_v1.1.pdf
Normal file
BIN
docs/CSCP_SPEC_V10.pdf
Normal file
BIN
docs/CSCS_SPEC_V10.pdf
Normal 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>
|
||||
151
docs/org.bluetooth.characteristic.cycling_power_feature.xml
Normal 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>
|
||||
304
docs/org.bluetooth.characteristic.cycling_power_measurement.xml
Normal 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>
|
||||
128
docs/org.bluetooth.characteristic.cycling_power_vector.xml
Normal 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>
|
||||
219
docs/org.bluetooth.characteristic.indoor_bike_data.xml
Normal 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>
|
||||
@@ -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
|
After Width: | Height: | Size: 230 KiB |
BIN
docs/treadmill-bridge-schema.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/ui-mac.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
docs/ui.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
123
src/bike.cpp
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
BIN
src/icons/bluetooth-icon.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
src/icons/cadence.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
src/icons/chart.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/icons/elevationgain.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/icons/fan.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src/icons/heart_red.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/icons/inclination.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/icons/kcal.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/icons/odometer.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
src/icons/pace.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
src/icons/resistance.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/icons/speed.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/icons/start.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/icons/stop.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/icons/watt.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
src/icons/weight.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/icons/zwift-on.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
99
src/main.cpp
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>NUL >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>NUL >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
@@ -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
|
||||
|
||||
8
src/test/test-bike/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include <QCoreApplication>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
23
src/test/test-bike/test-bike.pro
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
391
src/trxappgateusbtreadmill.cpp
Normal 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;
|
||||
}
|
||||
89
src/trxappgateusbtreadmill.h
Normal 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
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
23
train-programs-examples/calorie-barbara.xml
Normal 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>
|
||||
43062
train-programs-examples/fogliano.gpx
Normal file
74
train-programs-examples/fogliano.xml
Normal 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>
|
||||