Compare commits

..

67 Commits

Author SHA1 Message Date
Roberto Viola
481e256621 trying to stabilize cadence on echelonconnectsport
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-05 10:44:44 +01:00
Roberto Viola
2e94a16889 restoring correct fitness machine feature bytearray on virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-04 11:13:48 +01:00
Roberto Viola
b4c4b194b2 elevation gain fixed on domyostreadmill #67
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-04 10:39:08 +01:00
Roberto Viola
5da8440086 toggling bluetooth icon on connecting on QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-04 10:35:32 +01:00
Roberto Viola
d5424a38fa fixed adv parameter on future zwift connections. added cadence support
for virtualbike

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-04 09:58:28 +01:00
Roberto Viola
1947473b2c wait the stop of the discovery before starting the virtualtreadmill and
virtualbike

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-04 09:57:39 +01:00
Roberto Viola
1b38c1f400 fixed typo on wakelock on android 2020-12-04 09:55:45 +01:00
Roberto Viola
51f7580d06 setting HR to primary back on virtualbike (windows 10 doesn't see it
with secondary)

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-03 22:32:23 +01:00
Roberto Viola
e6b2a17bee miles added to argument
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-03 17:05:11 +01:00
Roberto Viola
95338bb35e fixed wake lock on android #59
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-03 16:59:34 +01:00
Roberto Viola
f4138820cf speed and distance implemented on echelonconnectsport #62
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-03 16:18:02 +01:00
Roberto Viola
f446dac1db add android 64 bit and minlevelsdk 29 2020-12-03 16:01:00 +01:00
Roberto Viola
2967fc1ab4 compiling for android release 2020-12-03 15:12:26 +01:00
Roberto Viola
ed3ab8b0f1 peloton compatibility added (UNTESTED)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-03 11:43:45 +01:00
Roberto Viola
884b9a9a8c added miles option to QML #66
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-03 11:13:47 +01:00
Roberto Viola
aac169b834 heartrate on virtualbike put as secondaryservice
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-02 18:28:23 +01:00
Roberto Viola
601d9cb20e pace icon changed 2020-12-02 16:15:58 +01:00
Roberto Viola
90034c3746 working on trainProgram on QML, they don't work yet
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-02 15:15:20 +01:00
Roberto Viola
97a2e7d8c9 swap order of heartrate service on virtualbike 2020-12-02 13:50:42 +01:00
Roberto Viola
bccd5522d1 drop shadow on QML and purpling everything :D
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-02 10:37:18 +01:00
Roberto Viola
07c5dc247a watt calculation improved for echelonconnectsport
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-02 10:05:47 +01:00
Roberto Viola
005834588a fixed parameters on main
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-02 08:16:35 +01:00
Roberto Viola
a2fa9a4843 timeout added to writeCharacteristic to domyosbike and domyostreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 22:24:28 +01:00
Roberto Viola
1d8f473e4c removed ugly and useless workaround on virtualtreadmill ( https://
github.com/cagnulein/qdomyos-zwift/issues/60#issuecomment-736587560 )

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 16:37:26 +01:00
Roberto Viola
4dc48fca2b popup on train program loaded on QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 16:15:36 +01:00
Roberto Viola
7eb0176e0c train prorgam and gpx added to QML (untested)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 15:58:37 +01:00
Roberto Viola
974396672a built version 1.2.1
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 14:03:38 +01:00
Roberto Viola
1758255399 fixed heart rate service in QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 12:26:34 +01:00
Roberto Viola
1506839f54 Update README.md 2020-12-01 11:34:29 +01:00
Roberto Viola
636c4c8185 bikeResistanceOffset and bikeResistanceGain added to parameter and QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 11:29:38 +01:00
Roberto Viola
9d0fd94a23 fixed issue on -qml parameter
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 09:29:35 +01:00
Roberto Viola
251c00cefb added -qml parameter to force the qml on Desktop too (useful for debug)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 08:59:49 +01:00
Roberto Viola
4949e8d816 version 1.1.5
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 08:37:11 +01:00
Roberto Viola
e57b0834c6 trying force heart rate service enabled on virtualbike ( https://
github.com/cagnulein/qdomyos-zwift/issues/61#issuecomment-735999248 )

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 08:32:07 +01:00
Roberto Viola
bb3f9d0bb4 indoorbike xml was wrong!
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 08:22:43 +01:00
Roberto Viola
2a8b8d6584 resistance doubled in virtualbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 08:03:55 +01:00
Roberto Viola
ea58b92fed sync write display added to domyosbike
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-12-01 08:02:19 +01:00
Roberto Viola
d6f5ce405e added watt table to echelonconnectsport
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 19:50:40 +01:00
Roberto Viola
34ffafb55f distance fixed on echelonconnectsport
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 19:23:55 +01:00
Roberto Viola
e6d9f5d847 fixed typo on echelonconnectsport resistance parser
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 19:11:01 +01:00
Roberto Viola
c3bfaffcf1 init echelonconnectsport fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 16:10:26 +01:00
Roberto Viola
f5eac6d6a1 linux build fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 15:55:42 +01:00
Roberto Viola
19c3a90bf4 storage permission on android for the logs
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 15:52:19 +01:00
Roberto Viola
705baaa37c odometer on echelonconnectsport fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 15:37:06 +01:00
Roberto Viola
e302e90066 logs on android restored
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 15:36:38 +01:00
Roberto Viola
9d9800d4e6 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2020-11-30 14:04:09 +01:00
Roberto Viola
5c00a959f4 echelonconnectsport service UUID fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 14:03:39 +01:00
Roberto Viola
54501760d3 cache dropped 2020-11-30 13:49:59 +01:00
Roberto Viola
2f4b76014f android compatibility issue fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 11:44:23 +01:00
Roberto Viola
7de4bac932 fixed elevation gain on domyostreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 11:15:10 +01:00
Roberto Viola
d992959792 android build commented 2020-11-30 10:25:14 +01:00
Roberto Viola
1ce77629ff echelonconnectsport added (NOT TESTED)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 10:16:06 +01:00
Roberto Viola
908c1536f6 signal icon added to QML
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 08:09:49 +01:00
Roberto Viola
b8948c6d8f bluetoothdeviceinfo move to bluetoothdevice to get signal strength
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-30 07:32:39 +01:00
Roberto Viola
1ab448f7cc ugly workaround for zwift virtualtreadmill on raspberry 2020-11-29 17:13:11 +01:00
Roberto Viola
38ea3f5c80 fixed cadence on UI 2020-11-29 17:09:30 +01:00
Roberto Viola
a87e818d9a virtual treadmill created when the domyostreadmill has finished its init
Signed-off-by: Roberto Viola <roberto.viola83@gmail.com>
2020-11-28 17:47:17 +01:00
Roberto Viola
d852bd44fe added root check 2020-11-28 17:14:55 +01:00
Roberto Viola
5f7d7e01b8 speed and inclination not synced in the state file fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-27 16:01:52 +01:00
Roberto Viola
548fa9d8d6 android qml: show relevant icons only
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-27 15:56:10 +01:00
Roberto Viola
3a725d71b5 linux compilation fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-25 15:17:28 +01:00
Roberto Viola
a304963dc5 decimal point fixed on the state file
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-25 09:45:22 +01:00
Roberto Viola
bf9fb4537b restore from previous values on domyostreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-25 09:37:15 +01:00
Roberto Viola
838fe8c96e xml state file written for domyostreadmill (untested)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-25 06:08:59 +01:00
Roberto Viola
77b204d9fd SIGINT handled
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-25 05:55:04 +01:00
Roberto Viola
8a6e8e9c9d youtube video added 2020-11-23 14:41:18 +01:00
Roberto Viola
123df9db6b saving speed and inclination for future session on domyostreadmill
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-11-23 10:40:25 +01:00
47 changed files with 3566 additions and 485 deletions

View File

@@ -19,19 +19,19 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Cache Qt Linux Desktop
id: cache-qt-linux-desktop
uses: actions/cache@v1
with:
path: '${{ github.workspace }}/output/linux-desktop/'
key: ${{ runner.os }}-QtCache-Linux-Desktop
# - name: Cache Qt Linux Desktop
# id: cache-qt-linux-desktop
# uses: actions/cache@v1
# with:
# path: '${{ github.workspace }}/output/linux-desktop/'
# key: ${{ runner.os }}-QtCache-Linux-Desktop
- name: Cache Qt Linux Android
id: cache-qt-android
uses: actions/cache@v1
with:
path: '${{ github.workspace }}/output/android/'
key: ${{ runner.os }}-QtCache-Android
# - name: Cache Qt Linux Android
# id: cache-qt-android
# uses: actions/cache@v1
# with:
# path: '${{ github.workspace }}/output/android/'
# key: ${{ runner.os }}-QtCache-Android
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
@@ -45,7 +45,7 @@ jobs:
target: 'desktop'
modules: 'qtcharts debug_info'
dir: '${{ github.workspace }}/output/linux-desktop/'
cached: ${{ steps.cache-qt-linux-desktop.outputs.cache-hit }}
# cached: ${{ steps.cache-qt-linux-desktop.outputs.cache-hit }}
- name: Compile Linux Desktop
run: cd src; qmake; make -j4
@@ -56,39 +56,39 @@ jobs:
name: linux-desktop-binary
path: src/qdomyos-zwift
- uses: actions/checkout@v2
with:
repository: nttld/setup-ndk
path: setup-ndk
# - uses: actions/checkout@v2
# with:
# repository: nttld/setup-ndk
# path: setup-ndk
# The packages.json in nttld/setup-ndk has already been updated,
# https://github.com/nttld/setup-ndk/commit/831db5b02a0f0cab80614619efe461a3dcc140e6
# but `dist/*` has not been rebuilt yet. Build it.
# https://github.com/nttld/setup-ndk/tree/main/dist
- name: Locally rebuilt setup-ndk
run: |
npm -prefix ./setup-ndk install
npm -prefix ./setup-ndk run all
# - name: Locally rebuilt setup-ndk
# run: |
# npm -prefix ./setup-ndk install
# npm -prefix ./setup-ndk run all
# Install using locally rebuilt setup-ndk
- name: Setup Android NDK r21d
uses: ./setup-ndk
# - name: Setup Android NDK r21d
# uses: ./setup-ndk
#- uses: nttld/setup-ndk@v1
with:
ndk-version: r21d
# with:
# ndk-version: r21d
# waiting github.com/jurplel/install-qt-action/issues/63
- name: Install Qt Android
uses: jurplel/install-qt-action@v2
with:
version: '5.12.9'
host: 'linux'
target: 'android'
arch: 'android_armv7'
modules: 'qtcharts debug_info'
dir: '${{ github.workspace }}/output/android/'
cached: ${{ steps.cache-qt-android.outputs.cache-hit }}
# - name: Install Qt Android
# uses: jurplel/install-qt-action@v2
# with:
# version: '5.12.9'
# host: 'linux'
# target: 'android'
# arch: 'android_armv7'
# modules: 'qtcharts debug_info'
# dir: '${{ github.workspace }}/output/android/'
# cached: ${{ steps.cache-qt-android.outputs.cache-hit }}
- name: Compile Android
run: cd src; qmake; make -j4
# - name: Compile Android
# run: cd src; qmake; make -j4
# - name: Install Qt MacOS
# uses: jurplel/install-qt-action@v2

View File

@@ -5,6 +5,8 @@ Zwift bridge for Treadmills and Bike!
![UI](docs/treadmill-bridge-schema.png)
[![Video](https://img.youtube.com/vi/GgG3dMhmo2Y/0.jpg)](https://www.youtube.com/watch?v=GgG3dMhmo2Y)
![UI](docs/ui.png)
![UI](docs/realtime-chart.png)
@@ -17,12 +19,13 @@ 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
2. Toorx TRX Route Key compatible
3. Echelon Connect Sport compatible
4. Zwift compatible
5. Create, load and save train programs
6. Measure distance, elevation gain and watts
7. Gpx import (with difficulty slider)
8. Realtime Charts
![First Success](docs/first_success.jpg)
@@ -59,6 +62,8 @@ Download and install http://download.qt.io/official_releases/qt/5.12/5.12.9/qt-o
- Raspberry 3b+ and Toorx TRX Route Key
- Android Pixel 2 and Echelon Connect Sport
### Your machine is not compatible?

View File

@@ -0,0 +1,69 @@
<?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="CSC Measurement" type="org.bluetooth.characteristic.csc_measurement" uuid="2A5B" last-modified="2012-04-12" approved="Yes">
<InformativeText>
<Summary>
The CSC Measurement characteristic (CSC refers to Cycling Speed and Cadence) is a variable length structure containing a Flags field and, based on the contents of the Flags field, may contain one or more additional fields as shown in the tables below.
</Summary>
</InformativeText>
<Value>
<Field name="Flags">
<InformativeText>These flags define which data fields are present in the Characteristic value.</InformativeText>
<Requirement>Mandatory</Requirement>
<Format>8bit</Format>
<BitField>
<Bit index="0" size="1" name="Wheel Revolution Data Present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C1" />
</Enumerations>
</Bit>
<Bit index="1" size="1" name="Crank Revolution Data Present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C2" />
</Enumerations>
</Bit>
<ReservedForFutureUse index="2" size="6" />
</BitField>
</Field>
<Field name="Cumulative Wheel Revolutions">
<InformativeText>
C1: Field exists if the key of bit 0 of the Flags field is set to 1.
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint32</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
</Field>
<Field name="Last Wheel Event Time">
<InformativeText>
Unit has a resolution of 1/1024s.
<br>C1: Field exists if the key of bit 0 of the Flags field is set to 1.</br>
</InformativeText>
<Requirement>C1</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.time.second</Unit>
<BinaryExponent>-10</BinaryExponent>
</Field>
<Field name="Cumulative Crank Revolutions">
<InformativeText>
C2: Field exists if the key of bit 1 of the Flags field is set to 1.
</InformativeText>
<Requirement>C2</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.unitless</Unit>
</Field>
<Field name="Last Crank Event Time">
<InformativeText>C2: Field exists if the key of bit 1 of the Flags field is set to 1.
<br>Unit has a resolution of 1/1024s.</br>
</InformativeText>
<Requirement>C2</Requirement>
<Format>uint16</Format>
<Unit>org.bluetooth.unit.time.second</Unit>
<BinaryExponent>-10</BinaryExponent>
</Field>
</Value>
<Note>
The fields in the above table are in the order of LSO to MSO. Where LSO = Least Significant Octet and MSO = Most Significant Octet.
</Note>
</Characteristic>

View File

@@ -1,14 +1,8 @@
<?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">
<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>
<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">
@@ -17,203 +11,200 @@ last-modified="2017-02-14" approved="Yes">
<BitField>
<Bit index="0" size="1" name="More Data">
<Enumerations>
<Enumeration key="0" value="False" requires="C1" />
<Enumeration key="0" value="False" requires="C1"/>
<Enumeration key="1" value="True" />
</Enumerations>
</Bit>
<Bit index="1" size="1"
name="Instantaneous Cadence present">
<Bit index="1" size="1" name="Average Speed present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C2" />
<Enumeration key="1" value="True" requires="C2" />
</Enumerations>
</Bit>
<Bit index="2" size="1" name="Average Speed present">
<Bit index="2" size="1" name="Instantaneous Cadence present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C3" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<Enumeration key="1" value="True" requires="C10" />
</Enumerations>
</Bit>
<Bit index="10" size="1"
name="Metabolic Equivalent present">
<Bit index="10" size="1" name="Metabolic Equivalent present">
<Enumerations>
<Enumeration key="0" value="False" />
<Enumeration key="1" value="True" requires="C11" />
<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" />
<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" />
<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>
<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>
<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>
<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>
<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>
<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>
<Unit>org.bluetooth.unit.angular_velocity.revolution_per_minute</Unit>
</Field>
<Field name="Total Distance">
<InformativeText>Meters with a resolution of
1</InformativeText>
<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>
<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>
<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>
<InformativeText>Watts with a resolution of 1</InformativeText>
<Requirement>C8</Requirement>
<Format>sint16</Format>
<Unit>org.bluetooth.unit.power.watt</Unit>
</Field>
</Field>
<Field name="Total Energy">
<InformativeText>Kilo Calorie with a resolution of
1</InformativeText>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<Note>
The fields in the above table, reading from top to bottom, are shown in the order of LSO to MSO, where LSO = Least Significant Octet and MSO = Most Significant Octet.
The Least Significant Octet represents the eight bits numbered 0 to 7.
</Note>
</Characteristic>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2011 Bluetooth SIG, Inc. All rights reserved. -->
<Service xsi:noNamespaceSchemaLocation="http://schemas.bluetooth.org/Documents/service.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Cycling Speed and Cadence" type="org.bluetooth.service.cycling_speed_and_cadence" uuid="1816" last-modified="2012-04-05" approved="Yes">
<InformativeText>
<Abstract>
This service exposes speed-related and cadence-related data from a Cycling Speed and Cadence sensor intended for fitness applications.
</Abstract>
<Summary>
The Cycling Speed and Cadence (CSC) Service exposes speed-related data and/or cadence-related data while using the Cycling Speed and Cadence sensor (Server).
</Summary>
</InformativeText>
<Dependencies>
<Dependency>This service is not dependent upon any other services.</Dependency>
</Dependencies>
<GATTRequirements>
<Requirement subProcedure="Write Characteristic Values">C1</Requirement>
<Requirement subProcedure="Notifications">Mandatory</Requirement>
<Requirement subProcedure="Indications">C1</Requirement>
<Requirement subProcedure="Read Characteristic Descriptors">Mandatory</Requirement>
<Requirement subProcedure="Write Characteristic Descriptors">Mandatory</Requirement>
</GATTRequirements>
<Note>C1: Mandatory if the SC Control Point characteristic is supported, otherwise excluded for this service.</Note>
<Transports>
<Classic>true</Classic>
<LowEnergy>true</LowEnergy>
</Transports>
<ErrorCodes>
<ErrorCode name="Procedure Already in Progress" code="0x80">A SC Control Point request cannot be serviced because a previously triggered SC Control Point operation is still in progress.</ErrorCode>
<ErrorCode name="Client Characteristic Configuration descriptor improperly configured" code="0x81">The Client Characteristic Configuration descriptor is not configured according to the requirements of the service.</ErrorCode>
</ErrorCodes>
<Characteristics>
<Characteristic name="CSC Measurement" type="org.bluetooth.characteristic.csc_measurement">
<InformativeText>
The CSC Measurement characteristic is used to send speed-related data and/or cadence-related data.
</InformativeText>
<Requirement>Mandatory</Requirement>
<Properties>
<Read>Excluded</Read>
<Write>Excluded</Write>
<WriteWithoutResponse>Excluded</WriteWithoutResponse>
<SignedWrite>Excluded</SignedWrite>
<ReliableWrite>Excluded</ReliableWrite>
<Notify>Mandatory</Notify>
<Indicate>Excluded</Indicate>
<WritableAuxiliaries>Excluded</WritableAuxiliaries>
<Broadcast>Excluded</Broadcast>
</Properties>
<Descriptors>
<Descriptor name="Client Characteristic Configuration" type="org.bluetooth.descriptor.gatt.client_characteristic_configuration">
<Requirement>Mandatory</Requirement>
<Properties>
<Read>Mandatory</Read>
<Write>Mandatory</Write>
</Properties>
</Descriptor>
</Descriptors>
</Characteristic>
<Characteristic name="CSC Feature" type="org.bluetooth.characteristic.csc_feature">
<InformativeText>
The CSC Feature characteristic is used to describe the supported features of the Server. Reserved for Future Use (RFU) bits in the CSC Feature characteristic value are set to 0.
</InformativeText>
<Requirement>Mandatory</Requirement>
<Properties>
<Read>Mandatory</Read>
<Write>Excluded</Write>
<WriteWithoutResponse>Excluded</WriteWithoutResponse>
<SignedWrite>Excluded</SignedWrite>
<ReliableWrite>Excluded</ReliableWrite>
<Notify>Excluded</Notify>
<Indicate>Excluded</Indicate>
<WritableAuxiliaries>Excluded</WritableAuxiliaries>
<Broadcast>Excluded</Broadcast>
</Properties>
</Characteristic>
<Characteristic name="Sensor Location" type="org.bluetooth.characteristic.sensor_location">
<InformativeText>
<p>The Sensor Location characteristic of the device is used to describe the physical location of the Server when correctly fitted.</p>
<p><b>C1:</b> Mandatory if the Multiple Sensor Location feature is supported, otherwise optional.</p>
</InformativeText>
<Requirement>C1</Requirement>
<Properties>
<Read>Mandatory</Read>
<Write>Excluded</Write>
<WriteWithoutResponse>Excluded</WriteWithoutResponse>
<SignedWrite>Excluded</SignedWrite>
<ReliableWrite>Excluded</ReliableWrite>
<Notify>Excluded</Notify>
<Indicate>Excluded</Indicate>
<WritableAuxiliaries>Excluded</WritableAuxiliaries>
<Broadcast>Excluded</Broadcast>
</Properties>
</Characteristic>
<Characteristic name="SC Control Point" type="org.bluetooth.characteristic.sc_control_point">
<InformativeText>
<p>If the SC Control Point is supported, profiles utilizing this service are required to ensure that the Client configures the SC Control Point characteristic for indications (i.e. via the Client Characteristic Configuration descriptor) at the first connection.</p>
<p>Support for this characteristic is mandatory if the Server supports Wheel Revolution Data or Multiple Sensor Locations features, otherwise it is excluded.</p>
<p><b>C2:</b> Mandatory if at least one SC Control Point procedure is supported, otherwise excluded.</p>
</InformativeText>
<Requirement>C2</Requirement>
<Properties>
<Read>Excluded</Read>
<Write>Mandatory</Write>
<WriteWithoutResponse>Excluded</WriteWithoutResponse>
<SignedWrite>Excluded</SignedWrite>
<ReliableWrite>Excluded</ReliableWrite>
<Notify>Excluded</Notify>
<Indicate>Mandatory</Indicate>
<WritableAuxiliaries>Excluded</WritableAuxiliaries>
<Broadcast>Excluded</Broadcast>
</Properties>
<Descriptors>
<Descriptor name="Client Characteristic Configuration" type="org.bluetooth.descriptor.gatt.client_characteristic_configuration">
<Requirement>if_characteristic_supported</Requirement>
<Properties>
<Read>Mandatory</Read>
<Write>Mandatory</Write>
</Properties>
</Descriptor>
</Descriptors>
</Characteristic>
</Characteristics>
</Service>

View File

@@ -13,13 +13,13 @@ HomeForm{
start.onClicked: { start_clicked(); }
stop.onClicked: { stop_clicked(); }
Component.onCompleted: { console.log("completed"); }
Component.onCompleted: { console.log("completed"); }
GridView {
anchors.horizontalCenter: parent.horizontalCenter
anchors.fill: parent
cellWidth: 175
cellHeight: 125
cellHeight: 130
focus: true
model: appModel
leftMargin: { (parent.width % 175) / 2; }
@@ -34,17 +34,31 @@ HomeForm{
// }
delegate: Item {
id: id1
width: 175
width: 170
height: 125
visible: visibleItem
Component.onCompleted: console.log("completed " + objectName)
Rectangle {
width: 173
width: 168
height: 123
radius: 3
border.width: 1
border.color: "purple"
color: Material.backgroundColor
id: rect
}
DropShadow {
anchors.fill: rect
cached: true
horizontalOffset: 3
verticalOffset: 3
radius: 8.0
samples: 16
color: Material.color(Material.Purple)
source: rect
}
Image {
@@ -60,7 +74,7 @@ HomeForm{
Text {
objectName: "value"
id: myValue
color: Material.textSelectionColor
color: "white"
y: 0
anchors {
horizontalCenter: parent.horizontalCenter

View File

@@ -28,19 +28,42 @@ Page {
width: 50
height: 100
color: Material.backgroundColor
Image {
Column {
id: column
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
id: treadmill_connection
width: 48
height: 48
source: "icons/icons/bluetooth-icon.png"
enabled: rootItem.device
smooth: true
}
ColorOverlay {
anchors.fill: treadmill_connection
source: treadmill_connection
color: treadmill_connection.enabled ? "#00000000" : "#B0D3d3d3"
width: parent.width
height: 100
spacing: 0
padding: 0
Rectangle {
width: 50
height: 100
color: Material.backgroundColor
Image {
anchors.verticalCenter: parent.verticalCenter
id: treadmill_connection
width: 48
height: 48
source: "icons/icons/bluetooth-icon.png"
enabled: rootItem.device
smooth: true
}
ColorOverlay {
anchors.fill: treadmill_connection
source: treadmill_connection
color: treadmill_connection.enabled ? "#00000000" : "#B0D3d3d3"
}
}
Image {
anchors.horizontalCenter: parent.horizontalCenter
id: treadmill_signal
width: 24
height: 24
source: rootItem.signal
smooth: true
}
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0" android:versionCode="1" android:installLocation="auto">
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.3.1" android:versionCode="12" android:installLocation="auto">
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
Remove the comment if you do not require these default permissions. -->
<!-- %%INSERT_PERMISSIONS -->
@@ -9,7 +9,7 @@
<!-- %%INSERT_FEATURES -->
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:debuggable="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon">
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="qdomyos-zwift" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -73,7 +73,9 @@
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application>
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_CHECKIN_PROPERTIES"/>
</manifest>

View File

@@ -36,7 +36,7 @@ android {
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion '28.0.3'
buildToolsVersion '29.0.2'
sourceSets {
main {
@@ -53,6 +53,7 @@ android {
lintOptions {
abortOnError false
checkReleaseBuilds false
}
// Do not compress Qt binary resources file
@@ -62,7 +63,7 @@ android {
defaultConfig {
resConfig "en"
minSdkVersion = qtMinSdkVersion
targetSdkVersion = qtTargetSdkVersion
minSdkVersion = 29
targetSdkVersion = 29
}
}

View File

@@ -13,110 +13,6 @@ int8_t bike::currentResistance() { return Resistance;}
uint8_t bike::currentCadence() { return Cadence;}
uint8_t bike::fanSpeed() { return FanSpeed; }
bool bike::connected() { return false; }
uint16_t bike::watts() { return 0; }
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }
uint16_t bike::watts()
{
const uint8_t max_resistance = 15;
// ref https://translate.google.com/translate?hl=it&sl=en&u=https://support.wattbike.com/hc/en-us/articles/115001881825-Power-Resistance-and-Cadence-Tables&prev=search&pto=aue
const uint16_t watt_cad40_min = 25;
const uint16_t watt_cad40_max = 55;
const uint16_t watt_cad45_min = 35;
const uint16_t watt_cad45_max = 65;
const uint16_t watt_cad50_min = 40;
const uint16_t watt_cad50_max = 80;
const uint16_t watt_cad55_min = 50;
const uint16_t watt_cad55_max = 105;
const uint16_t watt_cad60_min = 60;
const uint16_t watt_cad60_max = 125;
const uint16_t watt_cad65_min = 70;
const uint16_t watt_cad65_max = 160;
const uint16_t watt_cad70_min = 85;
const uint16_t watt_cad70_max = 190;
const uint16_t watt_cad75_min = 100;
const uint16_t watt_cad75_max = 240;
const uint16_t watt_cad80_min = 115;
const uint16_t watt_cad80_max = 280;
const uint16_t watt_cad85_min = 130;
const uint16_t watt_cad85_max = 340;
const uint16_t watt_cad90_min = 150;
const uint16_t watt_cad90_max = 390;
const uint16_t watt_cad95_min = 175;
const uint16_t watt_cad95_max = 450;
const uint16_t watt_cad100_min = 195;
const uint16_t watt_cad100_max = 520;
const uint16_t watt_cad105_min = 210;
const uint16_t watt_cad105_max = 600;
const uint16_t watt_cad110_min = 245;
const uint16_t watt_cad110_max = 675;
const uint16_t watt_cad115_min = 270;
const uint16_t watt_cad115_max = 760;
const uint16_t watt_cad120_min = 300;
const uint16_t watt_cad120_max = 850;
const uint16_t watt_cad125_min = 330;
const uint16_t watt_cad125_max = 945;
const uint16_t watt_cad130_min = 360;
const uint16_t watt_cad130_max = 1045;
if(currentSpeed() <= 0) return 0;
if(currentCadence() < 41)
return((((watt_cad40_max-watt_cad40_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad40_min);
else if(currentCadence() < 46)
return((((watt_cad45_max-watt_cad45_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad45_min);
else if(currentCadence() < 51)
return((((watt_cad50_max-watt_cad50_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad50_min);
else if(currentCadence() < 56)
return((((watt_cad55_max-watt_cad55_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad55_min);
else if(currentCadence() < 61)
return((((watt_cad60_max-watt_cad60_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad60_min);
else if(currentCadence() < 66)
return((((watt_cad65_max-watt_cad65_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad65_min);
else if(currentCadence() < 71)
return((((watt_cad70_max-watt_cad70_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad70_min);
else if(currentCadence() < 76)
return((((watt_cad75_max-watt_cad75_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad75_min);
else if(currentCadence() < 81)
return((((watt_cad80_max-watt_cad80_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad80_min);
else if(currentCadence() < 86)
return((((watt_cad85_max-watt_cad85_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad85_min);
else if(currentCadence() < 91)
return((((watt_cad90_max-watt_cad90_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad90_min);
else if(currentCadence() < 96)
return((((watt_cad95_max-watt_cad95_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad95_min);
else if(currentCadence() < 101)
return((((watt_cad100_max-watt_cad100_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad100_min);
else if(currentCadence() < 106)
return((((watt_cad105_max-watt_cad105_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad105_min);
else if(currentCadence() < 111)
return((((watt_cad110_max-watt_cad110_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad110_min);
else if(currentCadence() < 116)
return((((watt_cad115_max-watt_cad115_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad115_min);
else if(currentCadence() < 121)
return((((watt_cad120_max-watt_cad120_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad120_min);
else if(currentCadence() < 126)
return((((watt_cad125_max-watt_cad125_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad125_min);
else
return((((watt_cad130_max-watt_cad130_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad130_min);
return 0;
}

View File

@@ -16,7 +16,7 @@ public:
virtual double currentCrankRevolutions();
virtual uint16_t lastCrankEventTime();
virtual bool connected();
uint16_t watts();
virtual uint16_t watts();
bluetoothdevice::BLUETOOTH_TYPE deviceType();
public slots:

View File

@@ -3,8 +3,9 @@
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
#include <QtXml>
bluetooth::bluetooth(bool logs, QString deviceName, bool noWriteResistance, bool noHeartService, uint32_t pollDeviceTime, bool noConsole, bool testResistance) : QObject(nullptr)
bluetooth::bluetooth(bool logs, QString deviceName, bool noWriteResistance, bool noHeartService, uint32_t pollDeviceTime, bool noConsole, bool testResistance, uint8_t bikeResistanceOffset, uint8_t bikeResistanceGain)
{
QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
filterDevice = deviceName;
@@ -14,6 +15,8 @@ bluetooth::bluetooth(bool logs, QString deviceName, bool noWriteResistance, bool
this->pollDeviceTime = pollDeviceTime;
this->noConsole = noConsole;
this->logs = logs;
this->bikeResistanceGain = bikeResistanceGain;
this->bikeResistanceOffset = bikeResistanceOffset;
#ifndef WIN32
if(!QBluetoothLocalDevice::allDevices().count())
@@ -27,12 +30,20 @@ bluetooth::bluetooth(bool logs, QString deviceName, bool noWriteResistance, bool
discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
this, SLOT(deviceDiscovered(QBluetoothDeviceInfo)));
connect(discoveryAgent, SIGNAL(canceled()),
this, SLOT(canceled()));
// Start a discovery
discoveryAgent->start();
}
}
void bluetooth::canceled()
{
debug("BTLE scanning stops");
emit searchingStop();
}
void bluetooth::debug(QString text)
{
QString debug = QDateTime::currentDateTime().toString() + " " + QString::number(QDateTime::currentMSecsSinceEpoch()) + " " + text;
@@ -59,20 +70,41 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device)
if(device.name().startsWith("Domyos-Bike") && !device.name().startsWith("DomyosBridge") && filter)
{
discoveryAgent->stop();
domyosBike = new domyosbike(noWriteResistance, noHeartService, testResistance);
domyosBike = new domyosbike(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset, bikeResistanceGain);
emit(deviceConnected());
connect(domyosBike, SIGNAL(disconnected()), this, SLOT(restart()));
connect(domyosBike, SIGNAL(disconnected()), this, SLOT(restart()));
connect(domyosBike, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
domyosBike->deviceDiscovered(device);
connect(this, SIGNAL(searchingStop()), domyosBike, SLOT(searchingStop()));
if(!discoveryAgent->isActive())
emit searchingStop();
}
else if(device.name().startsWith("Domyos") && !device.name().startsWith("DomyosBridge") && filter)
{
discoveryAgent->stop();
domyos = new domyostreadmill(this->pollDeviceTime, noConsole, noHeartService);
stateFileRead();
emit(deviceConnected());
connect(domyos, SIGNAL(disconnected()), this, SLOT(restart()));
connect(domyos, SIGNAL(disconnected()), this, SLOT(restart()));
connect(domyos, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
connect(domyos, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double)));
connect(domyos, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
domyos->deviceDiscovered(device);
connect(this, SIGNAL(searchingStop()), domyos, SLOT(searchingStop()));
if(!discoveryAgent->isActive())
emit searchingStop();
}
else if(device.name().startsWith("ECH-SPORT") && filter)
{
discoveryAgent->stop();
echelonConnectSport = new echelonconnectsport(noWriteResistance, noHeartService);
//stateFileRead();
emit(deviceConnected());
connect(echelonConnectSport, SIGNAL(disconnected()), this, SLOT(restart()));
connect(echelonConnectSport, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
//connect(echelonConnectSport, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double)));
//connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
echelonConnectSport->deviceDiscovered(device);
}
else if((device.name().startsWith("TRX ROUTE KEY")) && filter)
{
@@ -116,6 +148,11 @@ void bluetooth::restart()
delete trxappgateusb;
trxappgateusb = 0;
}
if(echelonConnectSport)
{
delete echelonConnectSport;
echelonConnectSport = 0;
}
discoveryAgent->start();
}
@@ -129,5 +166,104 @@ bluetoothdevice* bluetooth::device()
return toorx;
else if(trxappgateusb)
return trxappgateusb;
else if(echelonConnectSport)
return echelonConnectSport;
return nullptr;
}
bool bluetooth::handleSignal(int signal)
{
if(signal == SIGNALS::SIG_INT)
{
qDebug() << "SIGINT";
QFile::remove("status.xml");
exit(0);
}
// Let the signal propagate as though we had not been there
return false;
}
void bluetooth::stateFileRead()
{
if(!device()) return;
QFile* log;
QDomDocument xmlBOM;
log = new QFile("status.xml");
if(!log->open(QIODevice::ReadOnly | QIODevice::Text))
{
qDebug() << "Open status.xml for writing failed";
return;
}
xmlBOM.setContent(log);
QDomElement root=xmlBOM.documentElement();
// Get root names and attributes
QString Type=root.tagName();
QString lastUpdated = root.attribute("Updated", QDateTime::currentDateTime().toString());
QDomElement machine=root.firstChild().toElement();
// Loop while there is a child
while(!machine.isNull())
{
// Check if the child tag name is COMPONENT
if (machine.tagName()=="Treadmill")
{
// Read and display the component ID
double speed = machine.attribute("Speed", "0.0").toDouble();
double inclination = machine.attribute("Incline", "0.0").toDouble();
((domyostreadmill*)device())->setLastSpeed(speed);
((domyostreadmill*)device())->setLastInclination(inclination);
}
// Next component
machine = machine.nextSibling().toElement();
}
log->close();
}
void bluetooth::stateFileUpdate()
{
if(!device()) return;
if(device()->deviceType() != bluetoothdevice::TREADMILL) return;
QFile* log;
QDomDocument docStatus;
QDomElement docRoot;
QDomElement docTreadmill;
QDomElement docHeart;
log = new QFile("status.xml");
if(!log->open(QIODevice::WriteOnly | QIODevice::Text))
{
qDebug() << "Open status.xml for writing failed";
return;
}
docRoot = docStatus.createElement("Gym");
docStatus.appendChild(docRoot);
docTreadmill = docStatus.createElement("Treadmill");
docTreadmill.setAttribute("Speed", QString::number(device()->currentSpeed(), 'f', 1));
docTreadmill.setAttribute("Incline", QString::number(((treadmill*)device())->currentInclination(), 'f', 1));
docRoot.appendChild(docTreadmill);
//docHeart = docStatus.createElement("Heart");
//docHeart.setAttribute("Rate", QString::number(currentHeart));
//docRoot.appendChild(docHeart);
docRoot.setAttribute("Updated", QDateTime::currentDateTime().toString());
QTextStream stream(log);
stream << docStatus.toString();
log->flush();
log->close();
}
void bluetooth::speedChanged(double speed)
{
Q_UNUSED(speed);
stateFileUpdate();
}
void bluetooth::inclinationChanged(double inclination)
{
Q_UNUSED(inclination);
stateFileUpdate();
}

View File

@@ -20,13 +20,15 @@
#include "domyosbike.h"
#include "trxappgateusbtreadmill.h"
#include "toorxtreadmill.h"
#include "echelonconnectsport.h"
#include "bluetoothdevice.h"
#include "signalhandler.h"
class bluetooth : public QObject
class bluetooth : public QObject, public SignalHandler
{
Q_OBJECT
public:
explicit bluetooth(bool logs, QString deviceName = "", bool noWriteResistance = false, bool noHeartService = false, uint32_t pollDeviceTime = 200, bool noConsole = false, bool testResistance = false);
explicit bluetooth(bool logs, QString deviceName = "", bool noWriteResistance = false, bool noHeartService = false, uint32_t pollDeviceTime = 200, bool noConsole = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, uint8_t bikeResistanceGain = 1);
bluetoothdevice* device();
private:
@@ -36,6 +38,7 @@ private:
domyosbike* domyosBike = 0;
toorxtreadmill* toorx = 0;
trxappgateusbtreadmill* trxappgateusb = 0;
echelonconnectsport* echelonConnectSport = 0;
QString filterDevice = "";
bool testResistance = false;
bool noWriteResistance = false;
@@ -43,10 +46,17 @@ private:
bool noConsole = false;
bool logs = true;
uint32_t pollDeviceTime = 200;
uint8_t bikeResistanceOffset = 4;
uint8_t bikeResistanceGain = 1;
bool handleSignal(int signal);
void stateFileUpdate();
void stateFileRead();
signals:
void deviceConnected();
void deviceFound(QString name);
void searchingStop();
public slots:
void restart();
@@ -54,6 +64,9 @@ public slots:
private slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
void canceled();
void speedChanged(double);
void inclinationChanged(double);
signals:

View File

@@ -11,7 +11,7 @@ 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 ); }
QTime bluetoothdevice::currentPace(){ if(Speed == 0) return QTime(0,0,0,0); else 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; };

View File

@@ -2,6 +2,7 @@
#define BLUETOOTHDEVICE_H
#include <QObject>
#include <QBluetoothDeviceInfo>
class bluetoothdevice : public QObject
{
@@ -18,6 +19,7 @@ public:
virtual void* VirtualDevice();
uint16_t watts(double weight=75.0);
virtual bool changeFanSpeed(uint8_t speed);
QBluetoothDeviceInfo bluetoothDevice;
enum BLUETOOTH_TYPE {
UNKNOWN = 0,

View File

@@ -5,12 +5,16 @@
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testResistance)
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testResistance, uint8_t bikeResistanceOffset, uint8_t bikeResistanceGain)
{
refresh = new QTimer(this);
this->testResistance = testResistance;
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
this->bikeResistanceGain = bikeResistanceGain;
this->bikeResistanceOffset = bikeResistanceOffset;
initDone = false;
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
refresh->start(200);
@@ -19,10 +23,13 @@ domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testRes
void domyosbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool wait_for_response)
{
QEventLoop loop;
QTimer timeout;
if(wait_for_response)
{
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
timeout.singleShot(300, &loop, SLOT(quit()));
}
else
{
@@ -36,6 +43,9 @@ void domyosbike::writeCharacteristic(uint8_t* data, uint8_t data_len, QString in
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
loop.exec();
if(timeout.isActive() == false)
debug(" exit for timeout");
}
void domyosbike::updateDisplay(uint16_t elapsed)
@@ -65,8 +75,8 @@ void domyosbike::updateDisplay(uint16_t elapsed)
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) );
writeCharacteristic(display, 20, "updateDisplay elapsed=" + QString::number(elapsed), false, false );
writeCharacteristic(&display[20], sizeof (display) - 20, "updateDisplay elapsed=" + QString::number(elapsed), false, true );
//if(bike_type == CHANG_YOW)
{
@@ -82,8 +92,8 @@ void domyosbike::updateDisplay(uint16_t elapsed)
display2[26] += display2[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(display2, 20, "updateDisplay2");
writeCharacteristic(&display2[20], sizeof (display2) - 20, "updateDisplay2");
writeCharacteristic(display2, 20, "updateDisplay2", false, false);
writeCharacteristic(&display2[20], sizeof (display2) - 20, "updateDisplay2", false, true);
}
}
@@ -129,7 +139,7 @@ void domyosbike::update()
//else
// btinit_telink(false);
}
else if(btbike.isValid() &&
else if(bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
@@ -141,6 +151,18 @@ void domyosbike::update()
elapsed += (((double)lastTime.msecsTo(current)) / ((double)1000.0));
lastTime = current;
// ******************************************* virtual bike init *************************************
static uint8_t firstVirtual = 0;
if(!firstVirtual && searchStopped)
{
debug("creating virtual bike interface...");
virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
connect(virtualBike,&virtualbike::debug ,this,&domyosbike::debug);
firstVirtual = 1;
}
// ********************************************************************************************************
// updating the treadmill console every second
if(sec1++ == (500 / refresh->interval()))
{
@@ -394,17 +416,6 @@ void domyosbike::stateChanged(QLowEnergyService::ServiceState state)
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);
@@ -412,6 +423,11 @@ void domyosbike::stateChanged(QLowEnergyService::ServiceState state)
}
}
void domyosbike::searchingStop()
{
searchStopped = true;
}
void domyosbike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
{
debug("descriptorWritten " + descriptor.name() + " " + newValue.toHex(' '));
@@ -455,7 +471,7 @@ 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;
bluetoothDevice = device;
if(device.address().toString().startsWith("57"))
{
@@ -468,7 +484,7 @@ void domyosbike::deviceDiscovered(const QBluetoothDeviceInfo &device)
bike_type = CHANG_YOW;
}
m_control = QLowEnergyController::createCentral(btbike, this);
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, SIGNAL(serviceDiscovered(const QBluetoothUuid &)),
this, SLOT(serviceDiscovered(const QBluetoothUuid &)));
connect(m_control, SIGNAL(discoveryFinished()),
@@ -481,6 +497,7 @@ void domyosbike::deviceDiscovered(const QBluetoothDeviceInfo &device)
Q_UNUSED(error);
Q_UNUSED(this);
debug("Cannot connect to remote device.");
searchStopped = false;
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
@@ -491,6 +508,7 @@ void domyosbike::deviceDiscovered(const QBluetoothDeviceInfo &device)
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
debug("LowEnergy controller disconnected");
searchStopped = false;
emit disconnected();
});
@@ -516,3 +534,108 @@ void* domyosbike::VirtualDevice()
{
return VirtualBike();
}
uint16_t domyosbike::watts()
{
const uint8_t max_resistance = 15;
// ref https://translate.google.com/translate?hl=it&sl=en&u=https://support.wattbike.com/hc/en-us/articles/115001881825-Power-Resistance-and-Cadence-Tables&prev=search&pto=aue
const uint16_t watt_cad40_min = 25;
const uint16_t watt_cad40_max = 55;
const uint16_t watt_cad45_min = 35;
const uint16_t watt_cad45_max = 65;
const uint16_t watt_cad50_min = 40;
const uint16_t watt_cad50_max = 80;
const uint16_t watt_cad55_min = 50;
const uint16_t watt_cad55_max = 105;
const uint16_t watt_cad60_min = 60;
const uint16_t watt_cad60_max = 125;
const uint16_t watt_cad65_min = 70;
const uint16_t watt_cad65_max = 160;
const uint16_t watt_cad70_min = 85;
const uint16_t watt_cad70_max = 190;
const uint16_t watt_cad75_min = 100;
const uint16_t watt_cad75_max = 240;
const uint16_t watt_cad80_min = 115;
const uint16_t watt_cad80_max = 280;
const uint16_t watt_cad85_min = 130;
const uint16_t watt_cad85_max = 340;
const uint16_t watt_cad90_min = 150;
const uint16_t watt_cad90_max = 390;
const uint16_t watt_cad95_min = 175;
const uint16_t watt_cad95_max = 450;
const uint16_t watt_cad100_min = 195;
const uint16_t watt_cad100_max = 520;
const uint16_t watt_cad105_min = 210;
const uint16_t watt_cad105_max = 600;
const uint16_t watt_cad110_min = 245;
const uint16_t watt_cad110_max = 675;
const uint16_t watt_cad115_min = 270;
const uint16_t watt_cad115_max = 760;
const uint16_t watt_cad120_min = 300;
const uint16_t watt_cad120_max = 850;
const uint16_t watt_cad125_min = 330;
const uint16_t watt_cad125_max = 945;
const uint16_t watt_cad130_min = 360;
const uint16_t watt_cad130_max = 1045;
if(currentSpeed() <= 0) return 0;
if(currentCadence() < 41)
return((((watt_cad40_max-watt_cad40_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad40_min);
else if(currentCadence() < 46)
return((((watt_cad45_max-watt_cad45_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad45_min);
else if(currentCadence() < 51)
return((((watt_cad50_max-watt_cad50_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad50_min);
else if(currentCadence() < 56)
return((((watt_cad55_max-watt_cad55_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad55_min);
else if(currentCadence() < 61)
return((((watt_cad60_max-watt_cad60_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad60_min);
else if(currentCadence() < 66)
return((((watt_cad65_max-watt_cad65_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad65_min);
else if(currentCadence() < 71)
return((((watt_cad70_max-watt_cad70_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad70_min);
else if(currentCadence() < 76)
return((((watt_cad75_max-watt_cad75_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad75_min);
else if(currentCadence() < 81)
return((((watt_cad80_max-watt_cad80_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad80_min);
else if(currentCadence() < 86)
return((((watt_cad85_max-watt_cad85_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad85_min);
else if(currentCadence() < 91)
return((((watt_cad90_max-watt_cad90_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad90_min);
else if(currentCadence() < 96)
return((((watt_cad95_max-watt_cad95_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad95_min);
else if(currentCadence() < 101)
return((((watt_cad100_max-watt_cad100_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad100_min);
else if(currentCadence() < 106)
return((((watt_cad105_max-watt_cad105_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad105_min);
else if(currentCadence() < 111)
return((((watt_cad110_max-watt_cad110_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad110_min);
else if(currentCadence() < 116)
return((((watt_cad115_max-watt_cad115_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad115_min);
else if(currentCadence() < 121)
return((((watt_cad120_max-watt_cad120_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad120_min);
else if(currentCadence() < 126)
return((((watt_cad125_max-watt_cad125_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad125_min);
else
return((((watt_cad130_max-watt_cad130_min) / (max_resistance - 1)) * (currentResistance() - 1))+watt_cad130_min);
return 0;
}

View File

@@ -32,7 +32,7 @@ class domyosbike : public bike
{
Q_OBJECT
public:
domyosbike(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false);
domyosbike(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, uint8_t bikeResistanceGain = 1);
bool connected();
void* VirtualBike();
@@ -49,11 +49,11 @@ private:
void btinit_telink(bool startTape);
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false, bool wait_for_response = false);
void startDiscover();
uint16_t watts();
QTimer* refresh;
virtualbike* virtualBike = 0;
QBluetoothDeviceInfo btbike;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
@@ -64,6 +64,9 @@ private:
bool noWriteResistance = false;
bool noHeartService = false;
bool testResistance = false;
uint8_t bikeResistanceOffset = 4;
uint8_t bikeResistanceGain = 1;
bool searchStopped = false;
enum _BIKE_TYPE {
CHANG_YOW,
@@ -77,6 +80,7 @@ signals:
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
void searchingStop();
private slots:

View File

@@ -5,6 +5,9 @@
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
static double lastSpeed = 0.0;
static double lastInclination = 0;
// set speed and incline to 0
uint8_t initData1[] = { 0xf0, 0xc8, 0x01, 0xb9 };
uint8_t initData2[] = { 0xf0, 0xc9, 0xb9 };
@@ -49,7 +52,6 @@ QBluetoothUuid _gattCommunicationChannelServiceId((QString)"49535343-fe7d-4ae5-8
QBluetoothUuid _gattWriteCharacteristicId((QString)"49535343-8841-43f4-a8d4-ecbe34729bb3");
QBluetoothUuid _gattNotifyCharacteristicId((QString)"49535343-1e4d-4bd9-ba61-23c647249616");
QBluetoothDeviceInfo bttreadmill;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
@@ -58,10 +60,17 @@ QLowEnergyCharacteristic gattNotifyCharacteristic;
bool initDone = false;
bool initRequest = false;
domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService)
domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed, double forceInitInclination)
{
this->noConsole = noConsole;
this->noHeartService = noHeartService;
if(forceInitSpeed > 0)
lastSpeed = forceInitSpeed;
if(forceInitInclination > 0)
lastInclination = forceInitInclination;
refresh = new QTimer(this);
initDone = false;
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
@@ -71,10 +80,13 @@ domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool n
void domyostreadmill::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool wait_for_response)
{
QEventLoop loop;
QTimer timeout;
if(wait_for_response)
{
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
timeout.singleShot(300, &loop, SLOT(quit()));
}
else
{
@@ -88,6 +100,9 @@ void domyostreadmill::writeCharacteristic(uint8_t* data, uint8_t data_len, QStri
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
loop.exec();
if(timeout.isActive() == false)
debug(" exit for timeout");
}
void domyostreadmill::updateDisplay(uint16_t elapsed)
@@ -136,8 +151,8 @@ void domyostreadmill::updateDisplay(uint16_t elapsed)
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) );
writeCharacteristic(display, 20, "updateDisplay elapsed=" + QString::number(elapsed), false, false );
writeCharacteristic(&display[20], sizeof (display) - 20, "updateDisplay elapsed=" + QString::number(elapsed), false, true );
}
void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestIncline)
@@ -161,8 +176,8 @@ void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestInc
//qDebug() << "writeIncline crc" << QString::number(writeIncline[26], 16);
writeCharacteristic(writeIncline, 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline));
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline));
writeCharacteristic(writeIncline, 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline), false, false);
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline), false, true);
}
bool domyostreadmill::changeFanSpeed(uint8_t speed)
@@ -178,7 +193,7 @@ bool domyostreadmill::changeFanSpeed(uint8_t speed)
fanSpeed[3] += fanSpeed[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(fanSpeed, 4, "changeFanSpeed speed=" + QString::number(speed));
writeCharacteristic(fanSpeed, 4, "changeFanSpeed speed=" + QString::number(speed), false, true);
return true;
}
@@ -199,18 +214,32 @@ void domyostreadmill::update()
if(initRequest)
{
initRequest = false;
btinit(false);
btinit((lastSpeed > 0 ? true : false));
}
else if(bttreadmill.isValid() &&
else if(bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotifyCharacteristic.isValid() &&
initDone)
{
// ******************************************* virtual treadmill init *************************************
static uint8_t firstInit = 0;
if(!firstInit && searchStopped)
{
debug("creating virtual treadmill interface...");
virtualTreadMill = new virtualtreadmill(this, noHeartService);
connect(virtualTreadMill,&virtualtreadmill::debug ,this,&domyostreadmill::debug);
firstInit = 1;
}
// ********************************************************************************************************
debug("Domyos Treadmill RSSI " + QString::number(bluetoothDevice.rssi()));
QDateTime current = QDateTime::currentDateTime();
double deltaTime = (((double)lastTime.msecsTo(current)) / ((double)1000.0));
if(currentSpeed() > 0.0 && !first)
elapsed += (((double)lastTime.msecsTo(current)) / ((double)1000.0));
elapsed += deltaTime;
lastTime = current;
// updating the treadmill console every second
@@ -263,6 +292,8 @@ void domyostreadmill::update()
if(requestStart != -1)
{
debug("starting...");
if(lastSpeed == 0.0)
lastSpeed = 0.5;
btinit(true);
requestStart = -1;
emit tapeStarted();
@@ -287,7 +318,7 @@ void domyostreadmill::update()
}
}
elevationAcc += (currentSpeed() / 3600.0) * 1000 * (currentInclination() / 100) * (refresh->interval() / 1000);
elevationAcc += (currentSpeed() / 3600.0) * 1000.0 * (currentInclination() / 100.0) * deltaTime;
}
first = false;
@@ -390,11 +421,26 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
Speed = speed;
Inclination = incline;
if(Speed != speed)
{
Speed = speed;
emit speedChanged(speed);
}
if(Inclination != incline)
{
Inclination = incline;
emit inclinationChanged(incline);
}
KCal = kcal;
Distance = distance;
if(speed > 0)
{
lastSpeed = speed;
lastInclination = incline;
}
lastTime = QDateTime::currentDateTime();
first = false;
}
@@ -422,7 +468,12 @@ double domyostreadmill::GetDistanceFromPacket(QByteArray packet)
double domyostreadmill::GetInclinationFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(2) << 8) | packet.at(3);
double data = ((double)convertedData - 1000.0f) / 10.0f;
double data;
if(convertedData > 10000)
data = ((double)convertedData - 65512.0f) / 10.0f;
else
data = ((double)convertedData - 1000.0f) / 10.0f;
if (data < 0) return 0;
return data;
}
@@ -436,16 +487,20 @@ void domyostreadmill::btinit(bool startTape)
writeCharacteristic(initDataStart3, sizeof(initDataStart3), "init", false, true);
writeCharacteristic(initDataStart4, sizeof(initDataStart4), "init", false, true);
writeCharacteristic(initDataStart5, sizeof(initDataStart5), "init", false, true);
writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
//writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
//writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
forceSpeedOrIncline(lastSpeed, lastInclination);
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init", false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init", false, true);
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init", false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init", false, true);
if(startTape)
{
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init", false, false);
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init", false, true);
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init", false, false);
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init", false, true);
forceSpeedOrIncline(lastSpeed, lastInclination);
}
initDone = true;
@@ -474,17 +529,6 @@ void domyostreadmill::stateChanged(QLowEnergyService::ServiceState state)
connect(gattCommunicationChannelService, SIGNAL(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)), this,
SLOT(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)));
// ******************************************* virtual treadmill init *************************************
static uint8_t first = 0;
if(!first)
{
debug("creating virtual treadmill interface...");
virtualTreadMill = new virtualtreadmill(this, noHeartService);
connect(virtualTreadMill,&virtualtreadmill::debug ,this,&domyostreadmill::debug);
}
first = 1;
// ********************************************************************************************************
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
@@ -532,8 +576,8 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("Domyos") && !device.name().startsWith("DomyosBridge"))
{
bttreadmill = device;
m_control = QLowEnergyController::createCentral(bttreadmill, this);
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, SIGNAL(serviceDiscovered(const QBluetoothUuid &)),
this, SLOT(serviceDiscovered(const QBluetoothUuid &)));
connect(m_control, SIGNAL(discoveryFinished()),
@@ -546,6 +590,7 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
Q_UNUSED(error);
Q_UNUSED(this);
debug("Cannot connect to remote device.");
searchStopped = false;
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
@@ -556,6 +601,7 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
debug("LowEnergy controller disconnected");
searchStopped = false;
emit disconnected();
});
@@ -586,3 +632,18 @@ double domyostreadmill::odometer()
{
return DistanceCalculated;
}
void domyostreadmill::setLastSpeed(double speed)
{
lastSpeed = speed;
}
void domyostreadmill::setLastInclination(double inclination)
{
lastInclination = inclination;
}
void domyostreadmill::searchingStop()
{
searchStopped = true;
}

View File

@@ -31,11 +31,14 @@ class domyostreadmill : public treadmill
{
Q_OBJECT
public:
domyostreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false);
domyostreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
bool connected();
bool changeFanSpeed(uint8_t speed);
double odometer();
void setLastSpeed(double speed);
void setLastInclination(double inclination);
void* VirtualTreadMill();
void* VirtualDevice();
@@ -54,6 +57,7 @@ private:
bool noConsole = false;
bool noHeartService = false;
uint32_t pollDeviceTime = 200;
bool searchStopped = false;
QTimer* refresh;
virtualtreadmill* virtualTreadMill = 0;
@@ -61,9 +65,12 @@ private:
signals:
void disconnected();
void debug(QString string);
void speedChanged(double speed);
void inclinationChanged(double inclination);
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
void searchingStop();
private slots:

387
src/echelonconnectsport.cpp Normal file
View File

@@ -0,0 +1,387 @@
#include "echelonconnectsport.h"
#include "virtualbike.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
#include <math.h>
echelonconnectsport::echelonconnectsport(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 echelonconnectsport::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log, bool wait_for_response)
{
QEventLoop loop;
if(wait_for_response)
{
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
}
else
{
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)),
&loop, SLOT(quit()));
}
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)data, data_len));
if(!disable_log)
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
loop.exec();
}
void echelonconnectsport::sendPoll()
{
static uint8_t counter = 1;
uint8_t noOpData[] = { 0xf0, 0xa0, 0x01, 0x00, 0x00 };
noOpData[3] = counter;
for(uint8_t i=0; i<sizeof(noOpData)-1; i++)
{
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true);
counter++;
if(!counter)
counter = 1;
}
void echelonconnectsport::update()
{
static QDateTime lastTime;
static bool first = true;
static uint8_t sec1 = 0;
if(m_control->state() == QLowEnergyController::UnconnectedState)
{
emit disconnected();
return;
}
if(initRequest)
{
initRequest = false;
btinit();
}
else if(bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotify1Characteristic.isValid() &&
gattNotify2Characteristic.isValid() &&
initDone)
{
QDateTime current = QDateTime::currentDateTime();
if(currentSpeed() > 0.0 && !first)
elapsed += (((double)lastTime.msecsTo(current)) / ((double)1000.0));
lastTime = current;
// updating the treadmill console every second
if(sec1++ == (500 / refresh->interval()))
{
sec1 = 0;
//updateDisplay(elapsed);
}
sendPoll();
if(requestResistance != -1)
{
if(requestResistance > 15) requestResistance = 15;
else if(requestResistance == 0) requestResistance = 1;
if(requestResistance != currentResistance())
{
debug("writing resistance " + QString::number(requestResistance));
//forceResistance(requestResistance);
}
requestResistance = -1;
}
if(requestStart != -1)
{
debug("starting...");
btinit();
requestStart = -1;
emit bikeStarted();
}
if(requestStop != -1)
{
debug("stopping...");
//writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
}
first = false;
}
void echelonconnectsport::serviceDiscovered(const QBluetoothUuid &gatt)
{
debug("serviceDiscovered " + gatt.toString());
}
static QByteArray lastPacket;
void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
Q_UNUSED(characteristic);
static QDateTime lastRefresh = QDateTime::currentDateTime();
debug(" << " + newValue.toHex(' '));
if (lastPacket.length() && lastPacket == newValue)
return;
lastPacket = newValue;
// resistance value is in another frame
if(newValue.length() == 5 && newValue.at(0) == 0xf0 && newValue.at(1) == 0xd2)
{
Resistance = newValue.at(3);
debug("Current resistance: " + QString::number(Resistance));
return;
}
if (newValue.length() != 13)
return;
/*if ((uint8_t)(newValue.at(0)) != 0xf0 && (uint8_t)(newValue.at(1)) != 0xd1)
return;*/
double distance = GetDistanceFromPacket(newValue);
Cadence = newValue.at(10);
Speed = 0.37497622 * ((double)Cadence);
KCal = 0;
Distance += ((Speed / 3600000.0) * ((double)lastRefresh.msecsTo(QDateTime::currentDateTime())) );
double cadenceAdd = ((double)(lastRefresh.msecsTo(QDateTime::currentDateTime())) * ((double)Cadence / 60000.0) );
static double crankRevsLocal = 0;
static uint16_t lastCrankEventTimeLocal = 0;
crankRevsLocal += cadenceAdd;
lastCrankEventTimeLocal += (uint16_t)((lastRefresh.msecsTo(QDateTime::currentDateTime())) * 1.024);
lastRefresh = QDateTime::currentDateTime();
// crank rotation done
if(((uint16_t)(crankRevsLocal)) > ((uint16_t)(CrankRevs)))
{
CrankRevs = crankRevsLocal;
LastCrankEventTime = lastCrankEventTimeLocal;
}
debug("Current Local elapsed: " + GetElapsedFromPacket(newValue).toString());
debug("Current Speed: " + QString::number(Speed));
debug("Current Calculate Distance: " + QString::number(Distance));
debug("Current Cadence: " + QString::number(Cadence));
debug("Current Distance: " + QString::number(distance));
debug("Local Current CrankRevs: " + QString::number(crankRevsLocal));
debug("Local Last CrankEventTime: " + QString::number(lastCrankEventTimeLocal));
debug("Current CrankRevs: " + QString::number(CrankRevs));
debug("Last CrankEventTime: " + QString::number(LastCrankEventTime));
debug("Current Watt: " + QString::number(watts()));
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
}
QTime echelonconnectsport::GetElapsedFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(3) << 8) | packet.at(4);
QTime t(0,convertedData / 60, convertedData % 60);
return t;
}
double echelonconnectsport::GetDistanceFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(7) << 8) | packet.at(8);
double data = ((double)convertedData) / 100.0f;
return data;
}
void echelonconnectsport::btinit()
{
uint8_t initData1[] = { 0xf0, 0xa1, 0x00, 0x91 };
uint8_t initData2[] = { 0xf0, 0xa3, 0x00, 0x93 };
uint8_t initData3[] = { 0xf0, 0xb0, 0x01, 0x01, 0xa2 };
// in the snoof log it repeats this frame 4 times, i will have to analyze the response to understand if 4 times are enough
writeCharacteristic(initData1, sizeof(initData1), "init", false, true);
writeCharacteristic(initData1, sizeof(initData1), "init", false, true);
writeCharacteristic(initData1, sizeof(initData1), "init", false, true);
writeCharacteristic(initData1, sizeof(initData1), "init", false, true);
writeCharacteristic(initData2, sizeof(initData2), "init", false, true);
writeCharacteristic(initData1, sizeof(initData1), "init", false, true);
writeCharacteristic(initData3, sizeof(initData3), "init", false, true);
initDone = true;
}
void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state)
{
QBluetoothUuid _gattWriteCharacteristicId((QString)"0bf669f2-45f2-11e7-9598-0800200c9a66");
QBluetoothUuid _gattNotify1CharacteristicId((QString)"0bf669f3-45f2-11e7-9598-0800200c9a66");
QBluetoothUuid _gattNotify2CharacteristicId((QString)"0bf669f4-45f2-11e7-9598-0800200c9a66");
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);
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
gattNotify2Characteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId);
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotify1Characteristic.isValid());
Q_ASSERT(gattNotify2Characteristic.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,&echelonconnectsport::debug);
}
first = 1;
// ********************************************************************************************************
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattCommunicationChannelService->writeDescriptor(gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
gattCommunicationChannelService->writeDescriptor(gattNotify2Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
}
}
void echelonconnectsport::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
{
debug("descriptorWritten " + descriptor.name() + " " + newValue.toHex(' '));
initRequest = true;
}
void echelonconnectsport::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
{
Q_UNUSED(characteristic);
debug("characteristicWritten " + newValue.toHex(' '));
}
void echelonconnectsport::serviceScanDone(void)
{
debug("serviceScanDone");
QBluetoothUuid _gattCommunicationChannelServiceId((QString)"0bf669f1-45f2-11e7-9598-0800200c9a66");
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
connect(gattCommunicationChannelService, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(stateChanged(QLowEnergyService::ServiceState)));
gattCommunicationChannelService->discoverDetails();
}
void echelonconnectsport::errorService(QLowEnergyService::ServiceError err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
debug("echelonconnectsport::errorService" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
}
void echelonconnectsport::error(QLowEnergyController::Error err)
{
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
debug("echelonconnectsport::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
m_control->disconnect();
}
void echelonconnectsport::deviceDiscovered(const QBluetoothDeviceInfo &device)
{
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("ECH-SPORT"))
{
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, SIGNAL(serviceDiscovered(const QBluetoothUuid &)),
this, SLOT(serviceDiscovered(const QBluetoothUuid &)));
connect(m_control, SIGNAL(discoveryFinished()),
this, SLOT(serviceScanDone()));
connect(m_control, SIGNAL(error(QLowEnergyController::Error)),
this, SLOT(error(QLowEnergyController::Error)));
connect(m_control, 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 echelonconnectsport::connected()
{
if(!m_control)
return false;
return m_control->state() == QLowEnergyController::DiscoveredState;
}
void* echelonconnectsport::VirtualBike()
{
return virtualBike;
}
void* echelonconnectsport::VirtualDevice()
{
return VirtualBike();
}
uint16_t echelonconnectsport::watts()
{
if(currentCadence() == 0) return 0;
// https://github.com/cagnulein/qdomyos-zwift/issues/62#issuecomment-736913564
if(currentCadence() < 90)
return (uint16_t)((3.59 * exp(0.0217 * (double)(currentCadence()))) * exp(0.095 * (double)(currentResistance())) );
else
return (uint16_t)((3.59 * exp(0.0217 * (double)(currentCadence()))) * exp(0.088 * (double)(currentResistance())) );
}

87
src/echelonconnectsport.h Normal file
View File

@@ -0,0 +1,87 @@
#ifndef ECHELONCONNECTSPORT_H
#define ECHELONCONNECTSPORT_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 echelonconnectsport : public bike
{
Q_OBJECT
public:
echelonconnectsport(bool noWriteResistance, bool noHeartService);
bool connected();
void* VirtualBike();
void* VirtualDevice();
private:
double GetDistanceFromPacket(QByteArray packet);
QTime GetElapsedFromPacket(QByteArray packet);
void btinit();
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false, bool wait_for_response = false);
void startDiscover();
void sendPoll();
uint16_t watts();
QTimer* refresh;
virtualbike* virtualBike = 0;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotify1Characteristic;
QLowEnergyCharacteristic gattNotify2Characteristic;
bool initDone = false;
bool initRequest = false;
bool noWriteResistance = false;
bool noHeartService = 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 // ECHELONCONNECTSPORT_H

View File

@@ -1,6 +1,9 @@
#include "homeform.h"
#include <QQmlContext>
#include <QTime>
#include <QSettings>
#include <QQmlFile>
#include "gpx.h"
DataObject::DataObject(QString name, QString icon, QString value, bool writable, QString id)
{
@@ -15,47 +18,125 @@ DataObject::DataObject(QString name, QString icon, QString value, bool writable,
}
void DataObject::setValue(QString v) {m_value = v; emit valueChanged(m_value);}
void DataObject::setVisible(bool visible) {m_visible = visible; emit visibleChanged(m_visible);}
homeform::homeform(QQmlApplicationEngine* engine, bluetooth* bl)
{
QSettings settings;
bool miles = settings.value("miles_unit", false).toBool();
QString unit = "km";
if(miles)
unit = "mi";
speed = new DataObject("Speed (" + unit + "/h)", "icons/icons/speed.png", "0.0", true, "speed");
inclination = new DataObject("Inclination (%)", "icons/icons/inclination.png", "0.0", true, "inclination");
cadence = new DataObject("Cadence (bpm)", "icons/icons/cadence.png", "0", false, "cadence");
elevation = new DataObject("Elev. Gain (m)", "icons/icons/elevationgain.png", "0", false, "elevation");
calories = new DataObject("Calories (KCal)", "icons/icons/kcal.png", "0", false, "calories");
odometer = new DataObject("Odometer (" + unit + ")", "icons/icons/odometer.png", "0.0", false, "odometer");
pace = new DataObject("Pace (m/km)", "icons/icons/pace.png", "0:00", false, "pace");
resistance = new DataObject("Resistance (%)", "icons/icons/resistance.png", "0", true, "resistance");
watt = new DataObject("Watt", "icons/icons/watt.png", "0", false, "watt");
heart = new DataObject("Heart (bpm)", "icons/icons/heart_red.png", "0", false, "heart");
fan = new DataObject("Fan Speed", "icons/icons/fan.png", "0", true, "fan");
this->bluetoothManager = bl;
this->engine = engine;
connect(bluetoothManager, SIGNAL(deviceFound(QString)), this, SLOT(deviceFound(QString)));
connect(bluetoothManager, SIGNAL(deviceConnected()), this, SLOT(deviceConnected()));
connect(bluetoothManager, SIGNAL(deviceConnected()), this, SLOT(trainProgramSignals()));
engine->rootContext()->setContextProperty("rootItem", (QObject *)this);
dataList = {
speed,
inclination,
cadence,
elevation,
calories,
odometer,
pace,
resistance,
watt,
heart,
fan
};
engine->rootContext()->setContextProperty("appModel", QVariant::fromValue(dataList));
QObject *rootObject = engine->rootObjects().first();
QObject *home = rootObject->findChild<QObject*>("home");
QObject::connect(home, SIGNAL(start_clicked()),
this, SLOT(Start()));
QObject::connect(home, SIGNAL(stop_clicked()),
this, SLOT(Stop()));
QObject::connect(home, SIGNAL(plus_clicked(QString)),
this, SLOT(Plus(QString)));
QObject::connect(home, SIGNAL(minus_clicked(QString)),
this, SLOT(Minus(QString)));
this->trainProgram = new trainprogram(QList<trainrow>(), bl);
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &homeform::update);
timer->start(1000);
}
void homeform::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 homeform::deviceConnected()
{
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
dataList = {
speed,
inclination,
elevation,
calories,
odometer,
pace,
watt,
heart,
fan
};
}
else if(bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
{
dataList = {
speed,
cadence,
elevation,
calories,
odometer,
resistance,
watt,
heart,
fan
};
}
engine->rootContext()->setContextProperty("appModel", QVariant::fromValue(dataList));
QObject *rootObject = engine->rootObjects().first();
QObject *home = rootObject->findChild<QObject*>("home");
QObject *stack = rootObject;
QObject::connect(home, SIGNAL(start_clicked()),
this, SLOT(Start()));
QObject::connect(home, SIGNAL(stop_clicked()),
this, SLOT(Stop()));
QObject::connect(stack, SIGNAL(trainprogram_open_clicked(QUrl)),
this, SLOT(trainprogram_open_clicked(QUrl)));
QObject::connect(home, SIGNAL(plus_clicked(QString)),
this, SLOT(Plus(QString)));
QObject::connect(home, SIGNAL(minus_clicked(QString)),
this, SLOT(Minus(QString)));
}
void homeform::deviceFound(QString name)
{
if(!name.trimmed().length()) return;
m_info = name + " founded";
emit infoChanged(m_info);
}
@@ -159,6 +240,23 @@ void homeform::Stop()
bluetoothManager->device()->stop();
}
QString homeform::signal()
{
if(!bluetoothManager)
return "icons/icons/signal-1.png";
if(!bluetoothManager->device())
return "icons/icons/signal-1.png";
int16_t rssi = bluetoothManager->device()->bluetoothDevice.rssi();
if(rssi > -40)
return "icons/icons/signal-3.png";
else if(rssi > -60)
return "icons/icons/signal-2.png";
return "icons/icons/signal-1.png";
}
void homeform::update()
{
if(bluetoothManager->device())
@@ -168,16 +266,31 @@ void homeform::update()
double watts = 0;
double pace = 0;
speed->setValue(QString::number(bluetoothManager->device()->currentSpeed(), 'f', 2));
QSettings settings;
bool miles = settings.value("miles_unit", false).toBool();
double unit_conversion = 1.0;
if(miles)
unit_conversion = 0.621371;
emit signalChanged(signal());
speed->setValue(QString::number(bluetoothManager->device()->currentSpeed() * unit_conversion, 'f', 2));
heart->setValue(QString::number(bluetoothManager->device()->currentHeart()));
odometer->setValue(QString::number(bluetoothManager->device()->odometer(), 'f', 2));
odometer->setValue(QString::number(bluetoothManager->device()->odometer() * unit_conversion, 'f', 2));
calories->setValue(QString::number(bluetoothManager->device()->calories(), 'f', 0));
fan->setValue(QString::number(bluetoothManager->device()->fanSpeed()));
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
pace = 10000 / (((treadmill*)bluetoothManager->device())->currentPace().second() + (((treadmill*)bluetoothManager->device())->currentPace().minute() * 60));
if(pace < 0) pace = 0;
if(bluetoothManager->device()->currentSpeed())
{
pace = 10000 / (((treadmill*)bluetoothManager->device())->currentPace().second() + (((treadmill*)bluetoothManager->device())->currentPace().minute() * 60));
if(pace < 0) pace = 0;
}
else
{
pace = 0;
}
watts = ((treadmill*)bluetoothManager->device())->watts(/*weight->text().toFloat()*/); // TODO: add weight to settings
inclination = ((treadmill*)bluetoothManager->device())->currentInclination();
this->pace->setValue(((treadmill*)bluetoothManager->device())->currentPace().toString("m:ss"));
@@ -191,6 +304,7 @@ void homeform::update()
watts = ((bike*)bluetoothManager->device())->watts();
watt->setValue(QString::number(watts));
this->resistance->setValue(QString::number(resistance));
this->cadence->setValue(QString::number(((bike*)bluetoothManager->device())->currentCadence()));
}
/*
if(trainProgram)
@@ -227,8 +341,13 @@ void homeform::update()
bool homeform::getDevice()
{
static bool toggle = false;
if(!this->bluetoothManager->device())
return false;
{
// toggling the bluetooth icon
toggle = !toggle;
return toggle;
}
return this->bluetoothManager->device()->connected();
}
@@ -250,3 +369,43 @@ bool homeform::getZwift()
}
return false;
}
void homeform::trainprogram_open_clicked(QUrl fileName)
{
qDebug() << "trainprogram_open_clicked" << fileName;
QFile file(QQmlFile::urlToLocalFileOrQrc(fileName));
qDebug() << file.fileName();
if(!file.fileName().isEmpty())
{
if(file.fileName().endsWith("xml"))
{
if(trainProgram)
delete trainProgram;
trainProgram = trainprogram::load(file.fileName(), bluetoothManager);
}
else if(file.fileName().endsWith("gpx"))
{
if(trainProgram)
delete trainProgram;
gpx g;
QList<trainrow> list;
foreach(gpx_altitude_point_for_treadmill p, g.open(file.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;
}
trainProgramSignals();
}
}

View File

@@ -5,6 +5,7 @@
#include <QQmlApplicationEngine>
#include "bluetooth.h"
#include "sessionline.h"
#include "trainprogram.h"
class DataObject : public QObject
{
@@ -14,16 +15,19 @@ class DataObject : public QObject
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged)
Q_PROPERTY(QString value READ value WRITE setValue NOTIFY valueChanged)
Q_PROPERTY(bool writable READ writable NOTIFY writableChanged)
Q_PROPERTY(bool visibleItem READ visibleItem NOTIFY visibleChanged)
Q_PROPERTY(QString plusName READ plusName NOTIFY plusNameChanged)
Q_PROPERTY(QString minusName READ minusName NOTIFY minusNameChanged)
public:
DataObject(QString name, QString icon, QString value, bool writable, QString id);
void setValue(QString value);
void setVisible(bool visible);
QString name() {return m_name;}
QString icon() {return m_icon;}
QString value() {return m_value;}
bool writable() {return m_writable;}
bool visibleItem() {return m_visible;}
QString plusName() {return m_id + "_plus";}
QString minusName() {return m_id + "_minus";}
@@ -32,12 +36,14 @@ public:
QString m_icon;
QString m_value;
bool m_writable;
bool m_visible = true;
signals:
void valueChanged(QString value);
void nameChanged(QString value);
void iconChanged(QString value);
void writableChanged(bool value);
void visibleChanged(bool value);
void plusNameChanged(QString value);
void minusNameChanged(QString value);
};
@@ -48,29 +54,33 @@ class homeform: public QObject
Q_PROPERTY( bool device READ getDevice NOTIFY changeOfdevice)
Q_PROPERTY( bool zwift READ getZwift NOTIFY changeOfzwift)
Q_PROPERTY(QString info READ info NOTIFY infoChanged)
Q_PROPERTY(QString signal READ signal NOTIFY signalChanged)
public:
homeform(QQmlApplicationEngine* engine, bluetooth* bl);
QString info() {return m_info;}
QString signal();
private:
QList<QObject *> dataList;
QList<SessionLine> Session;
bluetooth* bluetoothManager;
bluetooth* bluetoothManager = 0;
QQmlApplicationEngine* engine;
trainprogram* trainProgram = 0;
QString m_info = "Connecting...";
DataObject* speed = new DataObject("Speed (km/h)", "icons/icons/speed.png", "0.0", true, "speed");
DataObject* inclination = new DataObject("Inclination (%)", "icons/icons/inclination.png", "0.0", true, "inclination");
DataObject* cadence = new DataObject("Cadence (bpm)", "icons/icons/cadence.png", "0", false, "cadence");
DataObject* elevation = new DataObject("Elev. Gain (m)", "icons/icons/elevationgain.png", "0", false, "elevation");
DataObject* calories = new DataObject("Calories (KCal)", "icons/icons/kcal.png", "0", false, "calories");
DataObject* odometer = new DataObject("Odometer (km)", "icons/icons/odometer.png", "0.0", false, "odometer");
DataObject* pace = new DataObject("Pace (m/km)", "icons/icons/pace.png", "0:00", false, "pace");
DataObject* resistance = new DataObject("Resistance (%)", "icons/icons/resistance.png", "0", true, "resistance");
DataObject* watt = new DataObject("Watt", "icons/icons/watt.png", "0", false, "watt");
DataObject* heart = new DataObject("Heart (bpm)", "icons/icons/heart_red.png", "0", false, "heart");
DataObject* fan = new DataObject("Fan Speed", "icons/icons/fan.png", "0", true, "fan");
DataObject* speed;
DataObject* inclination;
DataObject* cadence;
DataObject* elevation;
DataObject* calories;
DataObject* odometer;
DataObject* pace;
DataObject* resistance;
DataObject* watt;
DataObject* heart;
DataObject* fan;
QTimer* timer;
@@ -84,10 +94,14 @@ private slots:
void Minus(QString);
void Plus(QString);
void deviceFound(QString name);
void deviceConnected();
void trainprogram_open_clicked(QUrl fileName);
void trainProgramSignals();
signals:
void changeOfdevice();
void changeOfzwift();
void signalChanged(QString value);
void infoChanged(QString value);
};

View File

@@ -18,5 +18,9 @@
<file>icons/pace.png</file>
<file>icons/chart.png</file>
<file>icons/icon.png</file>
<file>icons/signal-0.png</file>
<file>icons/signal-1.png</file>
<file>icons/signal-2.png</file>
<file>icons/signal-3.png</file>
</qresource>
</RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/icons/signal-0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

BIN
src/icons/signal-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

BIN
src/icons/signal-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

BIN
src/icons/signal-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

47
src/keepawakehelper.cpp Normal file
View File

@@ -0,0 +1,47 @@
#include <QDebug>
#ifdef Q_OS_ANDROID
#include <QAndroidJniObject>
#include "keepawakehelper.h"
#include "jni.h"
KeepAwakeHelper::KeepAwakeHelper()
{
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;");
if ( activity.isValid() )
{
QAndroidJniObject serviceName = QAndroidJniObject::getStaticObjectField<jstring>("android/content/Context","POWER_SERVICE");
if ( serviceName.isValid() )
{
QAndroidJniObject powerMgr = activity.callObjectMethod("getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;",serviceName.object<jobject>());
if ( powerMgr.isValid() )
{
jint levelAndFlags = QAndroidJniObject::getStaticField<jint>("android/os/PowerManager","SCREEN_DIM_WAKE_LOCK");
QAndroidJniObject tag = QAndroidJniObject::fromString( "My Tag" );
m_wakeLock = powerMgr.callObjectMethod("newWakeLock", "(ILjava/lang/String;)Landroid/os/PowerManager$WakeLock;", levelAndFlags,tag.object<jstring>());
}
}
}
if ( m_wakeLock.isValid() )
{
m_wakeLock.callMethod<void>("acquire", "()V");
qDebug() << "Locked device, can't go to standby anymore";
}
else
{
assert( false );
}
}
KeepAwakeHelper::~KeepAwakeHelper()
{
if ( m_wakeLock.isValid() )
{
m_wakeLock.callMethod<void>("release", "()V");
qDebug() << "Unlocked device, can now go to standby";
}
}
#endif

18
src/keepawakehelper.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef KEEPAWAKEHELPER_H
#define KEEPAWAKEHELPER_H
#ifdef Q_OS_ANDROID
#include <QAndroidJniObject>
class KeepAwakeHelper
{
public:
KeepAwakeHelper();
virtual ~KeepAwakeHelper();
private:
QAndroidJniObject m_wakeLock;
};
#endif
#endif // KEEPAWAKEHELPER_H

View File

@@ -2,15 +2,22 @@
#include <QStyleFactory>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // getuid
#include <QStandardPaths>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QSettings>
#include "virtualtreadmill.h"
#include "domyostreadmill.h"
#include "bluetooth.h"
#include "mainwindow.h"
#include "homeform.h"
#ifdef Q_OS_ANDROID
#include <QtAndroid>
#include "keepawakehelper.h"
#endif
bool nologs = false;
bool noWriteResistance = false;
bool noHeartService = true;
@@ -18,9 +25,13 @@ bool noConsole = false;
bool onlyVirtualBike = false;
bool onlyVirtualTreadmill = false;
bool testResistance = false;
bool forceQml = false;
bool miles = false;
QString trainProgram;
QString deviceName = "";
uint32_t pollDeviceTime = 200;
uint8_t bikeResistanceOffset = 4;
uint8_t bikeResistanceGain = 1;
static QString logfilename = "debug-" + QDateTime::currentDateTime().toString().replace(":", "_") + ".log";
QCoreApplication* createApplication(int &argc, char *argv[])
@@ -30,6 +41,10 @@ QCoreApplication* createApplication(int &argc, char *argv[])
for (int i = 1; i < argc; ++i) {
if (!qstrcmp(argv[i], "-no-gui"))
nogui = true;
if (!qstrcmp(argv[i], "-qml"))
forceQml = true;
if (!qstrcmp(argv[i], "-miles"))
miles = true;
if (!qstrcmp(argv[i], "-no-console"))
noConsole = true;
if (!qstrcmp(argv[i], "-test-resistance"))
@@ -58,10 +73,20 @@ QCoreApplication* createApplication(int &argc, char *argv[])
{
pollDeviceTime = atol(argv[++i]);
}
if (!qstrcmp(argv[i], "-bike-resistance-gain"))
{
bikeResistanceGain = atoi(argv[++i]);
}
if (!qstrcmp(argv[i], "-bike-resistance-offset"))
{
bikeResistanceOffset = atoi(argv[++i]);
}
}
if(nogui)
return new QCoreApplication(argc, argv);
else if(forceQml)
return new QGuiApplication(argc, argv);
else
{
QApplication* a = new QApplication(argc, argv);
@@ -143,38 +168,99 @@ void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QS
int main(int argc, char *argv[])
{
#ifdef Q_OS_LINUX
#ifndef Q_OS_ANDROID
if (getuid())
{
printf("Runme as root!\n");
return -1;
}
else printf("%s", "OK, you are root.\n");
#endif
#endif
#ifndef Q_OS_ANDROID
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
qInstallMessageHandler(myMessageOutput);
#else
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QScopedPointer<QGuiApplication> app(new QGuiApplication(argc, argv));
#endif
if(onlyVirtualBike)
app->setOrganizationName("Roberto Viola");
app->setOrganizationDomain("robertoviola.cloud");
app->setApplicationName("qDomyos-Zwift");
QSettings settings;
#ifdef Q_OS_ANDROID
noHeartService = settings.value("bike_heartrate_service", !noHeartService).toBool();
bikeResistanceOffset = settings.value("bike_resistance_offset", bikeResistanceOffset).toInt();
bikeResistanceGain = settings.value("bike_resistance_gain", bikeResistanceGain).toInt();
#else
settings.setValue("miles_unit", miles);
#endif
qInstallMessageHandler(myMessageOutput);
qDebug() << "version 1.3.1";
#ifndef Q_OS_ANDROID
if(!forceQml)
{
virtualbike* V = new virtualbike(new bike(), noWriteResistance, noHeartService);
return app->exec();
}
else if(onlyVirtualTreadmill)
{
virtualtreadmill* V = new virtualtreadmill(new treadmill(), noHeartService);
return app->exec();
if(onlyVirtualBike)
{
virtualbike* V = new virtualbike(new bike(), noWriteResistance, noHeartService);
Q_UNUSED(V)
return app->exec();
}
else if(onlyVirtualTreadmill)
{
virtualtreadmill* V = new virtualtreadmill(new treadmill(), noHeartService);
Q_UNUSED(V)
return app->exec();
}
}
#endif
bluetooth* bl = new bluetooth(!nologs, deviceName, noWriteResistance, noHeartService, pollDeviceTime, testResistance);
bluetooth* bl = new bluetooth(!nologs, deviceName, noWriteResistance, noHeartService, pollDeviceTime, noConsole, testResistance, bikeResistanceOffset, bikeResistanceGain);
#ifndef Q_OS_ANDROID
if(forceQml)
#endif
{
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
qobject_cast<QGuiApplication *>(app.data()), [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
#ifdef Q_OS_ANDROID
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
new homeform(&engine, bl);
auto result = QtAndroid::checkPermission(QString("android.permission.WRITE_EXTERNAL_STORAGE"));
if(result == QtAndroid::PermissionResult::Denied){
QtAndroid::PermissionResultMap resultHash = QtAndroid::requestPermissionsSync(QStringList({"android.permission.WRITE_EXTERNAL_STORAGE"}));
if(resultHash["android.permission.WRITE_EXTERNAL_STORAGE"] == QtAndroid::PermissionResult::Denied)
qDebug() << "log unwritable!";
}
result = QtAndroid::checkPermission(QString("android.permission.READ_EXTERNAL_STORAGE"));
if(result == QtAndroid::PermissionResult::Denied){
QtAndroid::PermissionResultMap resultHash = QtAndroid::requestPermissionsSync(QStringList({"android.permission.READ_EXTERNAL_STORAGE"}));
if(resultHash["android.permission.READ_EXTERNAL_STORAGE"] == QtAndroid::PermissionResult::Denied)
qDebug() << "log unwritable!";
}
#endif
engine.load(url);
new homeform(&engine, bl);
return app.exec();
#else
{
#ifdef Q_OS_ANDROID
KeepAwakeHelper helper;
#endif
// screen and CPU will stay awake during this section
// lock will be released when helper object goes out of scope
return app->exec();
}
}
#ifndef Q_OS_ANDROID
if (qobject_cast<QApplication *>(app.data())) {
// start GUI version...
MainWindow* W = 0;

View File

@@ -1,16 +1,50 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtQuick.Dialogs 1.0
ApplicationWindow {
id: window
width: 640
height: 480
visible: true
objectName: "stack"
title: qsTr("Stack")
signal trainprogram_open_clicked(url name)
Popup {
id: popup
parent: Overlay.overlay
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
width: 380
height: 50
modal: true
focus: true
palette.text: "white"
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition
{
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
}
exit: Transition
{
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Program has been loaded correctly. Press start to begin!")
}
}
}
header: ToolBar {
contentHeight: toolButton.implicitHeight
Material.primary: Material.Purple
ToolButton {
id: toolButton
@@ -40,22 +74,55 @@ ApplicationWindow {
Column {
anchors.fill: parent
ItemDelegate {
text: qsTr("Settings")
width: parent.width
onClicked: {
stackView.push("settings.qml")
drawer.close()
}
}
ItemDelegate {
id: gpx_open
text: qsTr("Open GPX")
width: parent.width
onClicked: {
fileDialog.visible = true
drawer.close()
}
}
ItemDelegate {
id: trainprogram_open
text: qsTr("Open Train Program")
width: parent.width
onClicked: {
fileDialog.visible = true
drawer.close()
}
}
ItemDelegate {
text: qsTr("by Roberto Viola")
width: parent.width
/* onClicked: {
stackView.push("Page1Form.ui.qml")
stackView.push("Page2Form.ui.qml")
drawer.close()
}*/
}
/* ItemDelegate {
text: qsTr("Page 2")
width: parent.width
onClicked: {
stackView.push("Page2Form.ui.qml")
drawer.close()
FileDialog {
id: fileDialog
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialog.fileUrl)
trainprogram_open_clicked(fileDialog.fileUrl)
fileDialog.close()
popup.open()
}
}*/
onRejected: {
console.log("Canceled")
fileDialog.close()
}
}
}
}

View File

@@ -65,8 +65,15 @@ void MainWindow::update()
if(bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL)
{
pace = 10000 / (((treadmill*)bluetoothManager->device())->currentPace().second() + (((treadmill*)bluetoothManager->device())->currentPace().minute() * 60));
if(pace < 0) pace = 0;
if(bluetoothManager->device()->currentSpeed())
{
pace = 10000 / (((treadmill*)bluetoothManager->device())->currentPace().second() + (((treadmill*)bluetoothManager->device())->currentPace().minute() * 60));
if(pace < 0) pace = 0;
}
else
{
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"));
@@ -80,6 +87,7 @@ void MainWindow::update()
watts = ((bike*)bluetoothManager->device())->watts();
ui->watt->setText(QString::number(watts));
ui->resistance->setText(QString::number(resistance));
ui->cadence->setText(QString::number(((bike*)bluetoothManager->device())->currentCadence()));
}
if(trainProgram)

View File

@@ -20,10 +20,13 @@ SOURCES += \
bluetoothdevice.cpp \
charts.cpp \
domyostreadmill.cpp \
echelonconnectsport.cpp \
gpx.cpp \
homeform.cpp \
keepawakehelper.cpp \
main.cpp \
sessionline.cpp \
signalhandler.cpp \
toorxtreadmill.cpp \
treadmill.cpp \
mainwindow.cpp \
@@ -44,8 +47,11 @@ HEADERS += \
bluetoothdevice.h \
charts.h \
domyostreadmill.h \
echelonconnectsport.h \
homeform.h \
keepawakehelper.h \
sessionline.h \
signalhandler.h \
toorxtreadmill.h \
gpx.h \
treadmill.h \

File diff suppressed because it is too large Load Diff

View File

@@ -6,5 +6,6 @@
<file>Page2Form.ui.qml</file>
<file>qtquickcontrols2.conf</file>
<file>Home.qml</file>
<file>settings.qml</file>
</qresource>
</RCC>

144
src/settings.qml Normal file
View File

@@ -0,0 +1,144 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.0
import QtQuick.Controls.Material 2.0
import Qt.labs.settings 1.0
Pane {
id: settingsPane
width: parent.width
height: parent.height
Settings {
id: settings
property bool bike_heartrate_service: false
property int bike_resistance_offset: 4
property int bike_resistance_gain: 1
property bool miles_unit: false
property bool bike_cadence_sensor: false
}
ColumnLayout {
id: column
spacing: 0
anchors.fill: parent
Label {
id: rebootLabel
text: qsTr("Reboot the app in order to apply the settings")
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
color: Material.color(Material.Red)
}
Label {
id: generalLabel
text: qsTr("General Options")
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
color: Material.color(Material.Grey)
}
SwitchDelegate {
id: unitDelegate
text: qsTr("Use Miles unit in UI")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
anchors.left: parent.left
anchors.right: parent.right
clip: false
checked: settings.miles_unit
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.miles_unit = checked
}
Label {
id: bikeBridgeLabel
text: qsTr("Bike Bridge Options")
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
color: Material.color(Material.Grey)
}
SwitchDelegate {
id: cadenceSensorDelegate
text: qsTr("Cadence Sensor (Peloton compatibility)")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
anchors.left: parent.left
anchors.right: parent.right
clip: false
checked: settings.bike_cadence_sensor
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.bike_cadence_sensor = checked
}
SwitchDelegate {
id: switchDelegate
text: qsTr("Heart Rate service outside FTMS")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
anchors.left: parent.left
anchors.right: parent.right
clip: false
checked: settings.bike_heartrate_service
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.bike_heartrate_service = checked
}
RowLayout {
spacing: 10
Label {
id: labelBikeResistanceOffset
text: qsTr("Zwift Resistance Offset:")
Layout.preferredWidth: 150
Layout.fillWidth: true
}
TextField {
Layout.preferredWidth: 60
id: bikeResistanceOffsetTextField
text: settings.bike_resistance_offset
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
inputMethodHints: Qt.ImhDigitsOnly
onAccepted: settings.bike_resistance_offset = text
}
}
RowLayout {
spacing: 10
Label {
id: labelBikeResistanceGain
text: qsTr("Zwift Resistance Gain:")
Layout.preferredWidth: 150
Layout.fillWidth: true
}
TextField {
Layout.preferredWidth: 60
id: bikeResistanceGainTextField
text: settings.bike_resistance_gain
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
inputMethodHints: Qt.ImhDigitsOnly
onAccepted: settings.bike_resistance_gain = text
}
}
}
}

169
src/signalhandler.cpp Normal file
View File

@@ -0,0 +1,169 @@
#include "signalhandler.h"
#include <assert.h>
#include <cstddef>
#if 1
#include <signal.h>
#else
#include <windows.h>
#endif //!__MINGW32_MAJOR_VERSION
// There can be only ONE SignalHandler per process
SignalHandler* g_handler(NULL);
#if 0
BOOL WINAPI WIN32_handleFunc(DWORD);
int WIN32_physicalToLogical(DWORD);
DWORD WIN32_logicalToPhysical(int);
std::set<int> g_registry;
#else //__MINGW32_MAJOR_VERSION
void POSIX_handleFunc(int);
int POSIX_physicalToLogical(int);
int POSIX_logicalToPhysical(int);
#endif //__MINGW32_MAJOR_VERSION
SignalHandler::SignalHandler(int mask) : _mask(mask)
{
assert(g_handler == NULL);
g_handler = this;
#if 0
SetConsoleCtrlHandler(WIN32_handleFunc, TRUE);
#endif //__MINGW32_MAJOR_VERSION
for (int i=0;i<numSignals;i++)
{
int logical = 0x1 << i;
if (_mask & logical)
{
#if 0
g_registry.insert(logical);
#else
int sig = POSIX_logicalToPhysical(logical);
bool failed = signal(sig, POSIX_handleFunc) == SIG_ERR;
assert(!failed);
(void)failed; // Silence the warning in non _DEBUG; TODO: something better
#endif //__MINGW32_MAJOR_VERSION
}
}
}
SignalHandler::~SignalHandler()
{
#if 0
SetConsoleCtrlHandler(WIN32_handleFunc, FALSE);
#else
for (int i=0;i<numSignals;i++)
{
int logical = 0x1 << i;
if (_mask & logical)
{
signal(POSIX_logicalToPhysical(logical), SIG_DFL);
}
}
#endif //__MINGW32_MAJOR_VERSION
}
#if 0
DWORD WIN32_logicalToPhysical(int signal)
{
switch (signal)
{
case SignalHandler::SIG_INT: return CTRL_C_EVENT;
case SignalHandler::SIG_TERM: return CTRL_BREAK_EVENT;
case SignalHandler::SIG_CLOSE: return CTRL_CLOSE_EVENT;
default:
return ~(unsigned int)0; // SIG_ERR = -1
}
}
#else
int POSIX_logicalToPhysical(int signal)
{
switch (signal)
{
case SignalHandler::SIG_INT: return SIGINT;
case SignalHandler::SIG_TERM: return SIGTERM;
// In case the client asks for a SIG_CLOSE handler, accept and
// bind it to a SIGTERM. Anyway the signal will never be raised
case SignalHandler::SIG_CLOSE: return SIGTERM;
//case SignalHandler::SIG_RELOAD: return SIGHUP;
default:
return -1; // SIG_ERR = -1
}
}
#endif //__MINGW32_MAJOR_VERSION
#if 0
int WIN32_physicalToLogical(DWORD signal)
{
switch (signal)
{
case CTRL_C_EVENT: return SignalHandler::SIG_INT;
case CTRL_BREAK_EVENT: return SignalHandler::SIG_TERM;
case CTRL_CLOSE_EVENT: return SignalHandler::SIG_CLOSE;
default:
return SignalHandler::SIG_UNHANDLED;
}
}
#else
int POSIX_physicalToLogical(int signal)
{
switch (signal)
{
case SIGINT: return SignalHandler::SIG_INT;
case SIGTERM: return SignalHandler::SIG_TERM;
//case SIGHUP: return SignalHandler::SIG_RELOAD;
default:
return SignalHandler::SIG_UNHANDLED;
}
}
#endif //__MINGW32_MAJOR_VERSION
#if 0
BOOL WINAPI WIN32_handleFunc(DWORD signal)
{
if (g_handler)
{
int signo = WIN32_physicalToLogical(signal);
// The std::set is thread-safe in const reading access and we never
// write to it after the program has started so we don't need to
// protect this search by a mutex
std::set<int>::const_iterator found = g_registry.find(signo);
if (signo != -1 && found != g_registry.end())
{
return g_handler->handleSignal(signo) ? TRUE : FALSE;
}
else
{
return FALSE;
}
}
else
{
return FALSE;
}
}
#else
void POSIX_handleFunc(int signal)
{
if (g_handler)
{
int signo = POSIX_physicalToLogical(signal);
g_handler->handleSignal(signo);
}
}
#endif //__MINGW32_MAJOR_VERSION

29
src/signalhandler.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef SIGNALHANDLER_H
#define SIGNALHANDLER_H
class SignalHandler
{
public:
SignalHandler(int mask = DEFAULT_SIGNALS);
virtual ~SignalHandler();
enum SIGNALS
{
SIG_UNHANDLED = 0, // Physical signal not supported by this class
SIG_NOOP = 1, // The application is requested to do a no-op (only a target that platform-specific signals map to when they can't be raised anyway)
SIG_INT = 2, // Control+C (should terminate but consider that it's a normal way to do so; can delay a bit)
SIG_TERM = 4, // Control+Break (should terminate now without regarding the consquences)
SIG_CLOSE = 8, // Container window closed (should perform normal termination, like Ctrl^C) [Windows only; on Linux it maps to SIG_TERM]
SIG_RELOAD = 16, // Reload the configuration [Linux only, physical signal is SIGHUP; on Windows it maps to SIG_NOOP]
DEFAULT_SIGNALS = SIG_INT | SIG_TERM | SIG_CLOSE,
};
static const int numSignals = 6;
virtual bool handleSignal(int signal) = 0;
private:
int _mask;
};
#endif // SIGNALHANDLER_H

View File

@@ -15,7 +15,7 @@ void toorxtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
if(device.name().startsWith("TRX ROUTE KEY"))
{
bttreadmill = device;
bluetoothDevice = device;
// Create a discovery agent and connect to its signals
discoveryAgent = new QBluetoothServiceDiscoveryAgent(this);
@@ -31,7 +31,7 @@ void toorxtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
// In your local slot, read information about the found devices
void toorxtreadmill::serviceDiscovered(const QBluetoothServiceInfo &service)
{
if(service.device().address() == bttreadmill.address())
if(service.device().address() == bluetoothDevice.address())
{
debug("Found new service: " + service.serviceName()
+ '(' + service.serviceUuid().toString() + ')');

View File

@@ -46,7 +46,6 @@ private slots:
void update();
private:
QBluetoothDeviceInfo bttreadmill;
QBluetoothServiceDiscoveryAgent *discoveryAgent;
QBluetoothServiceInfo serialPortService;
QBluetoothSocket *socket = nullptr;

View File

@@ -58,7 +58,7 @@ void trxappgateusbtreadmill::update()
initRequest = false;
btinit(false);
}
else if(bttreadmill.isValid() &&
else if(bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
@@ -342,8 +342,8 @@ 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);
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, SIGNAL(serviceDiscovered(const QBluetoothUuid &)),
this, SLOT(serviceDiscovered(const QBluetoothUuid &)));
connect(m_control, SIGNAL(discoveryFinished()),

View File

@@ -55,7 +55,6 @@ private:
QTimer* refresh;
virtualtreadmill* virtualTreadMill = 0;
QBluetoothDeviceInfo bttreadmill;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;

View File

@@ -2,6 +2,7 @@
#include <QtMath>
#include <QMetaEnum>
#include <QDataStream>
#include <QSettings>
enum FtmsControlPointCommand {
FTMS_REQUEST_CONTROL = 0x00,
@@ -36,72 +37,131 @@ enum FtmsResultCode {
FTMS_CONTROL_NOT_PERMITTED
};
virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService)
virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, uint8_t bikeResistanceGain)
{
Bike = t;
this->noHeartService = noHeartService;
this->bikeResistanceGain = bikeResistanceGain;
this->bikeResistanceOffset = bikeResistanceOffset;
QSettings settings;
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
Q_UNUSED(noWriteResistance)
//! [Advertising Data]
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
advertisingData.setLocalName("DomyosBridge");
advertisingData.setLocalName("DomyosBridge"); // save chars for service
QList<QBluetoothUuid> services;
services << ((QBluetoothUuid::ServiceClassUuid)0x1826); //FitnessMachineServiceUuid
if(!cadence)
services << ((QBluetoothUuid::ServiceClassUuid)0x1826); //FitnessMachineServiceUuid
else
services << (QBluetoothUuid::ServiceClassUuid::CyclingSpeedAndCadence);
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);
if(!cadence)
{
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 charDataFIT2;
charDataFIT2.setUuid((QBluetoothUuid::CharacteristicType)0x2AD6); //supported_resistance_level_rangeCharacteristicUuid
charDataFIT2.setProperties(QLowEnergyCharacteristic::Read);
QByteArray valueFIT2;
valueFIT2.append((char)0x0A); // min resistance value
valueFIT2.append((char)0x00); // min resistance value
valueFIT2.append((char)0x96); // max resistance value
valueFIT2.append((char)0x00); // max resistance value
valueFIT2.append((char)0x0A); // step resistance
valueFIT2.append((char)0x00); // step resistance
charDataFIT2.setValue(valueFIT2);
QLowEnergyCharacteristicData charDataFIT3;
charDataFIT3.setUuid((QBluetoothUuid::CharacteristicType)0x2AD9); //Fitness Machine Control Point
charDataFIT3.setProperties(QLowEnergyCharacteristic::Write | QLowEnergyCharacteristic::Indicate);
const QLowEnergyDescriptorData cpClientConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charDataFIT3.addDescriptor(cpClientConfig);
QLowEnergyCharacteristicData charDataFIT3;
charDataFIT3.setUuid((QBluetoothUuid::CharacteristicType)0x2AD9); //Fitness Machine Control Point
charDataFIT3.setProperties(QLowEnergyCharacteristic::Write | QLowEnergyCharacteristic::Indicate);
const QLowEnergyDescriptorData cpClientConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charDataFIT3.addDescriptor(cpClientConfig);
QLowEnergyCharacteristicData charDataFIT4;
charDataFIT4.setUuid((QBluetoothUuid::CharacteristicType)0x2AD2); //indoor bike
charDataFIT4.setProperties(QLowEnergyCharacteristic::Notify | QLowEnergyCharacteristic::Read);
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
const QLowEnergyDescriptorData clientConfig4(QBluetoothUuid::ClientCharacteristicConfiguration,
descriptor);
charDataFIT4.addDescriptor(clientConfig4);
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);
serviceDataFIT.setUuid((QBluetoothUuid::ServiceClassUuid)0x1826); //FitnessMachineServiceUuid
serviceDataFIT.addCharacteristic(charDataFIT);
serviceDataFIT.addCharacteristic(charDataFIT2);
serviceDataFIT.addCharacteristic(charDataFIT3);
serviceDataFIT.addCharacteristic(charDataFIT4);
}
else
{
QLowEnergyCharacteristicData charData;
charData.setUuid(QBluetoothUuid::CharacteristicType::CSCFeature);
charData.setProperties(QLowEnergyCharacteristic::Read);
QByteArray value;
value.append((char)0x02); // crank supported
value.append((char)0x00);
charData.setValue(value);
QLowEnergyCharacteristicData charData2;
charData2.setUuid(QBluetoothUuid::CharacteristicType::SensorLocation);
charData2.setProperties(QLowEnergyCharacteristic::Read);
QByteArray valueLocaltion;
valueLocaltion.append((char)13); // rear hub
charData2.setValue(valueLocaltion);
/*const QLowEnergyDescriptorData clientConfig2(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charData2.addDescriptor(clientConfig2);*/
QLowEnergyCharacteristicData charData3;
charData3.setUuid(QBluetoothUuid::CharacteristicType::CSCMeasurement);
charData3.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Notify);
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
const QLowEnergyDescriptorData clientConfig4(QBluetoothUuid::ClientCharacteristicConfiguration,
descriptor);
charData3.addDescriptor(clientConfig4);
QLowEnergyCharacteristicData charData4;
charData4.setUuid(QBluetoothUuid::CharacteristicType::SCControlPoint);
charData4.setProperties(QLowEnergyCharacteristic::Write | QLowEnergyCharacteristic::Indicate);
const QLowEnergyDescriptorData cpClientConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charData4.addDescriptor(cpClientConfig);
serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::CyclingSpeedAndCadence);
serviceData.addCharacteristic(charData);
serviceData.addCharacteristic(charData3);
serviceData.addCharacteristic(charData2);
serviceData.addCharacteristic(charData4);
}
if(!this->noHeartService)
{
@@ -121,11 +181,19 @@ virtualbike::virtualbike(bike* t, bool noWriteResistance, bool noHeartService)
//! [Start Advertising]
leController = QLowEnergyController::createPeripheral();
Q_ASSERT(leController);
if(!cadence)
serviceFIT = leController->addService(serviceDataFIT);
else
service = leController->addService(serviceData);
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)));
if(!cadence)
QObject::connect(serviceFIT, SIGNAL(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)), this, SLOT(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)));
else
QObject::connect(service, SIGNAL(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)), this, SLOT(characteristicChanged(const QLowEnergyCharacteristic, const QByteArray)));
QLowEnergyAdvertisingParameters pars;
pars.setInterval(100, 100);
@@ -169,7 +237,7 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte
int16_t iresistance = (newValue.at(3) + (newValue.at(4) << 8));
double resistance = ((double)iresistance) / 100.0;
Bike->changeResistance((uint8_t)resistance + 1); // resistance start from 1
Bike->changeResistance((uint8_t)(resistance * bikeResistanceGain) + bikeResistanceOffset + 1); // resistance start from 1
}
else if((char)newValue.at(0) == FTMS_START_RESUME)
{
@@ -218,20 +286,31 @@ void virtualbike::writeCharacteristic(QLowEnergyService* service, QLowEnergyChar
void virtualbike::reconnect()
{
QSettings settings;
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
emit debug("virtualbike::reconnect");
leController->disconnectFromDevice();
if(!cadence)
serviceFIT = leController->addService(serviceDataFIT);
else
service = leController->addService(serviceData);
if(!this->noHeartService)
serviceHR = leController->addService(serviceDataHR);
serviceFIT = leController->addService(serviceDataFIT);
if (serviceFIT)
leController->startAdvertising(QLowEnergyAdvertisingParameters(),
advertisingData, advertisingData);
QLowEnergyAdvertisingParameters pars;
pars.setInterval(100, 100);
leController->startAdvertising(pars,
advertisingData, advertisingData);
}
void virtualbike::bikeProvider()
{
QSettings settings;
bool cadence = settings.value("bike_cadence_sensor", false).toBool();
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
@@ -244,34 +323,54 @@ void virtualbike::bikeProvider()
QByteArray value;
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed() * 100);
value.append((char)(normalizeSpeed & 0xFF)); // speed
value.append((char)(normalizeSpeed >> 8) & 0xFF); // speed
value.append((char)(Bike->currentCadence() * 2)); // cadence
value.append((char)(0)); // cadence
value.append((char)Bike->currentResistance()); // resistance
value.append((char)(0)); // resistance
value.append((char)(Bike->watts() & 0xFF)); // watts
value.append((char)(Bike->watts() >> 8) & 0xFF); // watts
value.append(char(Bike->currentHeart())); // Actual value.
QLowEnergyCharacteristic characteristic
= serviceFIT->characteristic((QBluetoothUuid::CharacteristicType)0x2AD2);
Q_ASSERT(characteristic.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
if(!cadence)
{
emit debug("virtual bike not connected");
return;
}
writeCharacteristic(serviceFIT, characteristic, value);
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed() * 100);
value.append((char)(normalizeSpeed & 0xFF)); // speed
value.append((char)(normalizeSpeed >> 8) & 0xFF); // speed
value.append((char)(Bike->currentCadence() * 2)); // cadence
value.append((char)(0)); // cadence
value.append((char)Bike->currentResistance()); // resistance
value.append((char)(0)); // resistance
value.append((char)(Bike->watts() & 0xFF)); // watts
value.append((char)(Bike->watts() >> 8) & 0xFF); // watts
value.append(char(Bike->currentHeart())); // Actual value.
QLowEnergyCharacteristic characteristic
= serviceFIT->characteristic((QBluetoothUuid::CharacteristicType)0x2AD2);
Q_ASSERT(characteristic.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
return;
}
writeCharacteristic(serviceFIT, characteristic, value);
}
else
{
value.append((char)0x02); // crank data present
value.append((char)(((uint16_t)Bike->currentCrankRevolutions()) & 0xFF)); // revs count
value.append((char)(((uint16_t)Bike->currentCrankRevolutions()) >> 8) & 0xFF); // revs count
value.append((char)(Bike->lastCrankEventTime() & 0xff)); // eventtime
value.append((char)(Bike->lastCrankEventTime() >> 8) & 0xFF); // eventtime
QLowEnergyCharacteristic characteristic
= service->characteristic(QBluetoothUuid::CharacteristicType::CSCMeasurement);
Q_ASSERT(characteristic.isValid());
if(leController->state() != QLowEnergyController::ConnectedState)
{
emit debug("virtual bike not connected");
return;
}
writeCharacteristic(service, characteristic, value);
}
//characteristic
// = service->characteristic((QBluetoothUuid::CharacteristicType)0x2AD9); // Fitness Machine Control Point
//Q_ASSERT(characteristic.isValid());

View File

@@ -28,20 +28,24 @@ class virtualbike: public QObject
{
Q_OBJECT
public:
virtualbike(bike* t, bool noWriteResistance = false, bool noHeartService = false);
virtualbike(bike* t, bool noWriteResistance = false, bool noHeartService = false, uint8_t bikeResistanceOffset = 4, uint8_t bikeResistanceGain = 1);
bool connected();
private:
QLowEnergyController* leController;
QLowEnergyService* serviceHR;
QLowEnergyService* serviceFIT;
QLowEnergyService* service;
QLowEnergyAdvertisingData advertisingData;
QLowEnergyServiceData serviceDataHR;
QLowEnergyServiceData serviceDataFIT;
QLowEnergyServiceData serviceData;
QTimer bikeTimer;
bike* Bike;
bool noHeartService = false;
uint8_t bikeResistanceOffset = 4;
uint8_t bikeResistanceGain = 1;
void writeCharacteristic(QLowEnergyService* service, QLowEnergyCharacteristic characteristic, QByteArray value);

View File

@@ -147,6 +147,9 @@ void virtualtreadmill::reconnect()
if(noHeartService == false)
serviceHR = leController->addService(serviceDataHR);
QLowEnergyAdvertisingParameters pars;
pars.setInterval(100, 100);
if (service)
leController->startAdvertising(QLowEnergyAdvertisingParameters(),
advertisingData, advertisingData);