Compare commits

...

111 Commits

Author SHA1 Message Date
Roberto Viola
39e2ef50cb Proform Rower (Proform Sport RL) cannot change resistance from app #1643 2023-09-28 14:46:28 +02:00
Roberto Viola
880ac67fbc creating releases automatically 2023-09-28 10:15:34 +02:00
Roberto Viola
b21695bc14 Proform Rower (Proform Sport RL) cannot change resistance from app #1643 2023-09-28 09:52:10 +02:00
Roberto Viola
eab058f401 Proform Rower (Proform Sport RL) cannot change resistance from app #1643 2023-09-27 14:25:50 +02:00
Roberto Viola
58f62303be Proform CX Carbon: some resistance levels missing #1674 2023-09-27 14:21:20 +02:00
Roberto Viola
22f935c827 Proform Rower (Proform Sport RL) cannot change resistance from app (Issue #1643) 2023-09-27 11:44:04 +02:00
Roberto Viola
dce2ba7067 Proform Rower (Proform Sport RL) cannot change resistance from app (Issue #1643) 2023-09-26 17:55:36 +02:00
Roberto Viola
51665c76b4 Video Playback seem's to be broken with last iOS Build (Issue #1665) 2023-09-26 15:27:43 +02:00
Roberto Viola
a76cf602d6 Proform Rower (Proform Sport RL) cannot change resistance from app #1643 2023-09-26 11:12:02 +02:00
Roberto Viola
a3f6bb8d0c Proform CX Carbon: some resistance levels missing #1674 2023-09-26 10:55:33 +02:00
Roberto Viola
3f83e4a999 Walkingpad C2 #1672 2023-09-25 21:59:57 +02:00
Roberto Viola
14546d4ed3 Wahoo Blue SC without a heart rate monitor. #1651 2023-09-18 10:39:18 +02:00
Roberto Viola
1afff6a79d power zone max on the floating window 2023-09-18 08:26:40 +02:00
Roberto Viola
e95bf77c9d Yesoul C1H Resistance Value is jumping in iOS (Issue #1641) 2023-09-15 21:34:29 +02:00
Roberto Viola
76ebceccbc Add date tag to generic activities (Ride Row etc) #1648 2023-09-15 10:46:08 +02:00
Roberto Viola
f54b900c18 Holofit connection issue #1623 2023-09-15 06:44:57 +02:00
Roberto Viola
42e7749856 New Peloton Rower Paces (Issue #1611) 2023-09-14 20:54:20 +02:00
Roberto Viola
13fb8ae05a New Peloton Rower Paces (Issue #1611) 2023-09-14 10:47:27 +02:00
Roberto Viola
ee27386d11 HRM from Nordictrack ADB bikes (#1636) 2023-09-14 07:57:13 +02:00
Roberto Viola
5073ed766f Proform Rower (Proform Sport RL) cannot change resistance from app #1643 2023-09-13 13:57:58 +02:00
Roberto Viola
c571a98775 2.16.11 2023-09-12 15:59:21 +02:00
Roberto Viola
1e66fb10b9 QZ not working with Echelon Stride (Issue #1644) 2023-09-12 15:58:37 +02:00
Roberto Viola
c9bcfb201e Focus fitness fox 5 iplus (Issue #1642) 2023-09-12 14:26:00 +02:00
Roberto Viola
d701d01f13 2.16.10 2023-09-12 14:02:59 +02:00
Roberto Viola
f3ae244587 WearOS Companion App (#1635)
* receiving HR

* Update build.gradle

* Revert "Update build.gradle"

This reverts commit 6876191cbb.

* Revert "receiving HR"

This reverts commit a4f7acffa8.

* builds!

* wearable controller added

* is it required?

* Update build.gradle

* heart rate works!
2023-09-12 11:46:32 +02:00
Roberto Viola
755e98d49a HRM from Nordictrack ADB bikes (#1636) 2023-09-12 06:56:54 +02:00
Roberto Viola
006c365fee Yesoul C1H Resistance Value is jumping in iOS #1641 2023-09-11 17:17:44 +02:00
Roberto Viola
2d9110c251 HRM from Nordictrack ADB bikes (#1636)
* trying to get HRM

https://github.com/cagnulein/QZCompanionNordictrackTreadmill/issues/59

* Update nordictrackifitadbbike.cpp
2023-09-11 14:30:25 +02:00
Roberto Viola
30e945f609 QZ PowerZone Real Time Chart Not working on QZ V2.16.8 for uBike FTMS [BUG] #1639 2023-09-11 08:21:37 +02:00
Roberto Viola
d3289fa5d1 2.16.8 2023-09-07 07:33:11 +02:00
Roberto Viola
1dd8846c2e Web Live Charts (#1637)
* starting

* Update dochartlivewatt.js

* Update dochartlivewatt.js

* Update dochartlivewatt.js

* adding to qml

* adding compatibility with windows

* Update Home.qml

* file names renamed

* working point!

* icon works too

* conditional qml is working on iOS

* responsive
2023-09-07 07:29:35 +02:00
Roberto Viola
9a3dc093a2 qtHttpServer Lib for Android CI (#1638)
* Update main.yml

* Update main.yml
2023-09-06 15:09:08 +02:00
Roberto Viola
5674c75f4f [WINDOWS] Incline and resistance not controlling s22i bike #1627 (#1629) 2023-09-02 03:08:38 +02:00
Roberto Viola
f97e11b1d2 [WINDOWS] Incline and resistance not controlling s22i bike #1627 (#1629) 2023-09-01 19:55:21 +02:00
Roberto Viola
68a5bf7f44 iOS17 cycling power,speed and cadence (#1515) 2023-09-01 13:33:19 +02:00
Roberto Viola
d644aa4523 saris M2 control resistance from qz #1634 2023-09-01 07:23:19 +02:00
Roberto Viola
d04a1a9f01 version 2.16.7 2023-09-01 06:28:09 +02:00
Roberto Viola
e907170ba2 Separate Gain & Offset tables for Bluetooth & ANT+ (Issue #1633) 2023-08-31 18:11:29 +02:00
Roberto Viola
8a55ac1088 fixing ios build 2023-08-31 14:42:18 +02:00
Roberto Viola
f83c7565cc version 2.16.6 2023-08-31 14:28:45 +02:00
Roberto Viola
7de500503e Separate Gain & Offset tables for Bluetooth & ANT+ (Issue #1633) 2023-08-31 14:27:11 +02:00
Roberto Viola
61b15254e1 qdebug from objective c is possible 2023-08-31 14:15:38 +02:00
Roberto Viola
2503835a59 saris m2 #1625 2023-08-30 16:28:39 +02:00
Roberto Viola
00ceee2ee0 Home trainee elite zumo no resistance #1626 2023-08-30 11:17:18 +02:00
Roberto Viola
c93cf781fb fixing build #1622 #1497 2023-08-30 09:19:31 +02:00
Roberto Viola
a17da417c2 some buggy TDF1 bikes send spurious wattage at the end with cadence = 0 2023-08-30 08:27:22 +02:00
Roberto Viola
306ce699d4 Custom made broadcoaster connected to Nordicktrack s15 bike not sending resistance to QZ [BUG] #1622 #1497 2023-08-30 08:10:41 +02:00
Roberto Viola
fbbd40a0f8 DKN Endurun Treadmill #492 2023-08-29 08:52:12 +02:00
Roberto Viola
5398b09643 Nordictrack Z1300i #1618 2023-08-23 12:22:22 +02:00
Roberto Viola
94046b18fe Nordictrack Z1300i #1618 2023-08-23 08:53:43 +02:00
Roberto Viola
4e9a03d931 Domyos Rower incorrect speed #1594 2023-08-22 09:11:29 +02:00
Roberto Viola
2392251992 Horizon Paragon X #637 2023-08-22 08:10:07 +02:00
Roberto Viola
973ff3a586 Horizon Paragon X #637 2023-08-21 10:04:08 +02:00
Roberto Viola
87e5a1905d version 2.16.2 2023-08-18 11:47:47 +02:00
Roberto Viola
f39ca765d8 Andorid App Connection Failing - NoblePro Elite E8i #1609 2023-08-18 11:37:58 +02:00
Roberto Viola
b33ce3233d Support for Flowfitness speedbike DSB600i (Iconsole+/FITHIWAY0038) #1610 2023-08-18 11:32:43 +02:00
Roberto Viola
4209f6f653 [BUG] - Andorid App Connection Failing - NoblePro Elite E8i #1609 2023-08-17 11:27:19 +02:00
Roberto Viola
7d36b4082b Runmaxx 9.1 Threadmill on Win 10 (Issue #1581) 2023-08-17 10:15:39 +02:00
Roberto Viola
a3015858f9 Runmaxx 9.1 Threadmill on Win 10 (Issue #1581) 2023-08-15 10:24:28 +02:00
Roberto Viola
12515752f6 Peloton Resistance green tolerance for echelon bikes #1608 2023-08-15 10:21:09 +02:00
Roberto Viola
9334c6b472 Heart rate drops to zero intermittently (Issue #1607) 2023-08-15 09:52:15 +02:00
Roberto Viola
884bea8352 BH Mycron T200 Treadmill #1606 2023-08-14 17:35:26 +02:00
Roberto Viola
8b4c579539 Renpho AI Bike new model #1605 2023-08-14 17:33:22 +02:00
Roberto Viola
c9f3a3a092 2.16 build 628 2023-08-12 10:43:12 +02:00
Roberto Viola
547bc8a5c9 HR on iPad issue #1529 2023-08-12 10:32:06 +02:00
Roberto Viola
9c600dbc00 fs5i nordick track more realistic ride when running with Rouvy or Zwift (Discussion #1345) 2023-08-12 09:16:13 +02:00
Roberto Viola
f29d9fd1e6 Update .gitignore 2023-08-11 09:13:41 +02:00
Roberto Viola
8d1e98e81f version 2.16.0 2023-08-11 09:10:32 +02:00
Roberto Viola
94ac9d1bdd Treadmill Peloton issue #1603 2023-08-11 09:08:50 +02:00
Roberto Viola
9e74918b65 Domyos Rower incorrect speed #1594 2023-08-08 15:22:09 +02:00
Roberto Viola
3be43c14e1 "Recovery" tag on PowerZone peloton workout #1596 2023-08-08 14:44:27 +02:00
Roberto Viola
e1b951e664 next row fixed for power zone 2023-08-07 17:34:59 +02:00
Roberto Viola
53126b16d3 Watts not matching on Sole E95 Elliptical (Issue #1595) 2023-08-07 17:33:14 +02:00
Roberto Viola
0fb8e12d31 Watts not matching on Sole E95 Elliptical (Issue #1595) 2023-08-07 15:16:59 +02:00
Roberto Viola
679300c930 version 2.15.4 2023-08-07 14:10:02 +02:00
Roberto Viola
4008d588d7 "Recovery" tag on PowerZone peloton workout #1596 2023-08-07 14:02:56 +02:00
Roberto Viola
fda528babc Domyos Rower incorrect speed #1594 2023-08-06 14:52:11 +02:00
Roberto Viola
0e9bac6168 Watts not matching on Sole E95 Elliptical #1595 2023-08-06 14:49:14 +02:00
Roberto Viola
f8c1330862 version 2.15.1 for iOS 2023-08-06 10:39:41 +02:00
Roberto Viola
8e81df67d0 HR on iPad issue #1529 2023-08-06 10:12:32 +02:00
Roberto Viola
1a3c13eac9 Domyos Rower incorrect speed #1594 2023-08-05 11:05:17 +02:00
Roberto Viola
15b16c7e5c Domyos Rower incorrect speed #1594 2023-08-05 10:00:35 +02:00
Roberto Viola
b5ceb0a0f1 Costaway folding treadmill #1350 2023-08-02 17:11:24 +02:00
Roberto Viola
00939c56dd Costaway folding treadmill #1350 2023-08-02 15:18:41 +02:00
Roberto Viola
5f1ebc439e ANT+ Running speed sensor #1586 (#1589)
* first version (not tested)

* Update ChannelService.java

* Update SDMChannelController.java

* Update SDMChannelController.java

* Update bluetooth.cpp
2023-08-01 23:15:44 +02:00
Roberto Viola
81db615692 Runmaxx 9.1 Threadmill on Win 10 (Issue #1581) 2023-08-01 13:53:02 +02:00
Roberto Viola
65d12f00f4 fixing crash on ios 12 or lower 2023-07-31 21:37:58 +02:00
Roberto Viola
adcb87b3c4 Rowing Concept2 Pm3, Pm4 (Bluetooth) #1486 2023-07-31 11:55:41 +02:00
Roberto Viola
90900b786a fs5i nordick track more realistic ride when running with Rouvy or Zwift #1345 2023-07-27 08:40:49 +02:00
Roberto Viola
b495e833bd Inxide XS08 #1583 2023-07-27 08:19:34 +02:00
Roberto Viola
fb01341dd1 Release Android for Fdroid (#1582)
* Update main.yml

* fixing build error

* Update main.yml
2023-07-25 22:43:50 +02:00
Roberto Viola
867143e43e android 2.14.1 2023-07-24 21:01:32 +02:00
Roberto Viola
b85d7d72f5 2023 Sole F80 integration #1576 2023-07-24 11:00:17 +02:00
Roberto Viola
32cb3b9e37 2023 Sole F80 integration #1576 2023-07-24 10:19:24 +02:00
Roberto Viola
3ff8938b41 datetime tile now follow the locale of the user's system 2023-07-24 09:26:31 +02:00
Roberto Viola
5156cb38f0 adding virtual peloton bike to nordictrackadbbike 2023-07-24 06:26:19 +02:00
Roberto Viola
dd4a5dbc54 fixing hr on ios on ftmsbike 2023-07-24 06:25:58 +02:00
Roberto Viola
cf37b8705e fixing hr on ios on ftmsbike 2023-07-21 20:28:16 +02:00
Roberto Viola
bc8302c761 adding virtual peloton bike to nordictrackadbbike 2023-07-21 09:06:38 +02:00
Roberto Viola
76ecf5c66e adding virtual peloton bike to nordictrackadbbike 2023-07-21 09:03:31 +02:00
Roberto Viola
3693c2d1b2 v 2.14.0 2023-07-20 23:25:46 +02:00
Roberto Viola
3905bd8ba7 fixed peloton xml path 2023-07-20 23:12:21 +02:00
Roberto Viola
92cac7485e HR on iPad issue #1529 2023-07-20 16:34:15 +02:00
Roberto Viola
458c44758e Android QT Jars patched (#1575)
* Add files via upload

* adding only bluetooth one
2023-07-20 11:45:30 +02:00
Roberto Viola
be12859343 No charts when using floating window #1565 2023-07-19 09:53:21 +02:00
Roberto Viola
d201919b55 HR on iPad issue #1529 2023-07-19 09:38:07 +02:00
Roberto Viola
138a42c2e6 fixing CI build 2023-07-18 16:29:08 +02:00
Roberto Viola
bbe69f3f60 fixing build error 2023-07-18 16:02:50 +02:00
Roberto Viola
b95b3a5018 adding gears to wahookickrsnapbike as did for tacxneo
b443b03d49
2023-07-18 15:54:48 +02:00
Roberto Viola
fb0cbb74a5 adding smtp server info on the bottom of the email 2023-07-18 14:28:54 +02:00
Roberto Viola
deed6019ab adding restore purchase button for iOS guidelines 2023-07-18 14:28:33 +02:00
80 changed files with 4641 additions and 710 deletions

View File

@@ -176,6 +176,9 @@ jobs:
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
if: matrix.config.python == false
- name: patching qt for bluetooth
run: cp qt-patches/windows/5.15.2/binary/*.* ${{ github.workspace }}/src/debug/output/
- name: Zip artifact for deployment
run: Compress-Archive src/debug/output release.zip
@@ -193,6 +196,28 @@ jobs:
path: release.zip
if: ${{ ! matrix.config.python }}
- name: upload windows artifact
uses: actions/upload-release-asset@v1
if: ${{ ! matrix.config.python && startsWith(github.ref, 'refs/tags/') }}
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: release.zip
asset_name: windows-binary-no-python.zip
asset_content_type: application/zip
- name: upload windows artifact
uses: actions/upload-release-asset@v1
if: ${{ matrix.config.python && startsWith(github.ref, 'refs/tags/') }}
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: release.zip
asset_name: windows-binary.zip
asset_content_type: application/zip
# window-steam-build:
# runs-on: windows-latest
#
@@ -277,6 +302,19 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: release
uses: actions/create-release@v1
if: startsWith(github.ref, 'refs/tags/')
id: create_release
with:
draft: false
prerelease: false
release_name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref }}
body_path: CHANGELOG.md
env:
GITHUB_TOKEN: ${{ github.token }}
# - name: Cache Qt Linux Desktop
# id: cache-qt-linux-desktop
# uses: actions/cache@v1
@@ -478,6 +516,13 @@ jobs:
path: "tst/googletest/"
ref: "release-1.12.1"
- uses: actions/checkout@v2
- name: Checkout qHttpServer
uses: actions/checkout@v2
with:
repository: qt-labs/qthttpserver
path: "src/qthttpserver"
- name: Install packages required to run QZ inside workflow
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
@@ -513,7 +558,7 @@ jobs:
- name: Install Qt Android
uses: jurplel/install-qt-action@v3
with:
version: '5.15.2'
version: '5.15.0'
host: 'linux'
target: 'android'
arch: 'android'
@@ -527,6 +572,20 @@ jobs:
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '11'
- name: patching qt for bluetooth
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
- name: download 3rd party files for qthttpserver
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
- name: Build qthttpserver
run: |
cd src/qthttpserver
qmake
make -j8
make install
cd ../..
- name: Set Android NDK 21 && build
run: |
@@ -538,6 +597,13 @@ jobs:
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define LICENSE" >> secret.h
cd ..
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
@@ -546,6 +612,23 @@ jobs:
- name: Build APK (not usable for production due to unpatched QT library)
run: cd src; androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab
- name: Archive apk binary
uses: actions/upload-artifact@v2
with:
name: fdroid-android-trial
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/
- name: upload windows artifact
uses: actions/upload-release-asset@v1
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/
asset_name: fdroid-android-trial.zip
asset_content_type: application/zip
ios-build:
# The type of runner that the job will run on
runs-on: macos-latest

1
.gitignore vendored
View File

@@ -49,3 +49,4 @@ src/inner_templates/googlemaps/cesium-key.js
*.autosave
.vscode/settings.json
/tst/Devices/.vs
src/inner_templates/googlemaps/cesium-key.js

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
@@ -3666,7 +3666,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 612;
CURRENT_PROJECT_VERSION = 645;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@@ -3741,7 +3741,7 @@
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
);
MARKETING_VERSION = 2.13;
MARKETING_VERSION = 2.16;
OTHER_CFLAGS = (
"-pipe",
"-g",
@@ -3834,7 +3834,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 612;
CURRENT_PROJECT_VERSION = 645;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -3911,7 +3911,7 @@
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
);
MARKETING_VERSION = 2.13;
MARKETING_VERSION = 2.16;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"-pipe",
@@ -4038,7 +4038,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 612;
CURRENT_PROJECT_VERSION = 645;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4063,7 +4063,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.13;
MARKETING_VERSION = 2.16;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4134,7 +4134,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 612;
CURRENT_PROJECT_VERSION = 645;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -4155,7 +4155,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.13;
MARKETING_VERSION = 2.16;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4226,7 +4226,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 612;
CURRENT_PROJECT_VERSION = 645;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -4271,7 +4271,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.13;
MARKETING_VERSION = 2.16;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4340,7 +4340,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 612;
CURRENT_PROJECT_VERSION = 645;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -4381,7 +4381,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.13;
MARKETING_VERSION = 2.16;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";

View File

@@ -104,6 +104,9 @@ extension MainController: WorkoutTrackingDelegate {
"\(heartRate)" as AnyObject])
WorkoutTracking.distance = WatchKitConnection.distance
WorkoutTracking.kcal = WatchKitConnection.kcal
WorkoutTracking.speed = WatchKitConnection.speed
WorkoutTracking.power = WatchKitConnection.power
WorkoutTracking.cadence = WatchKitConnection.cadence
if Locale.current.measurementSystem != "Metric" {
self.distanceLabel.setText("Distance \(String(format:"%.2f", WorkoutTracking.distance))")

View File

@@ -24,6 +24,9 @@ class WatchKitConnection: NSObject {
public static var distance = 0.0
public static var kcal = 0.0
public static var stepCadence = 0
public static var speed = 0.0
public static var cadence = 0.0
public static var power = 0.0
weak var delegate: WatchKitConnectionDelegate?
private override init() {
@@ -66,6 +69,13 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
WatchKitConnection.distance = dDistance
let dKcal = Double(result["kcal"] as! Double)
WatchKitConnection.kcal = dKcal
let dSpeed = Double(result["speed"] as! Double)
WatchKitConnection.speed = dSpeed
let dPower = Double(result["power"] as! Double)
WatchKitConnection.power = dPower
let dCadence = Double(result["cadence"] as! Double)
WatchKitConnection.cadence = dCadence
}, errorHandler: { (error) in
print(error)
})

View File

@@ -31,6 +31,10 @@ class WorkoutTracking: NSObject {
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
public static var cadenceLastSteps = Double()
public static var cadenceSteps = 0
public static var speed = Double()
public static var power = Double()
public static var cadence = Double()
public static var lastDateMetric = Date()
var sport: Int = 0
let healthStore = HKHealthStore()
let configuration = HKWorkoutConfiguration()
@@ -146,14 +150,31 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.workoutType()
])
let infoToShare = Set([
HKSampleType.quantityType(forIdentifier: .stepCount)!,
HKSampleType.quantityType(forIdentifier: .heartRate)!,
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.workoutType()
])
var infoToShare: Set<HKSampleType> = []
if #available(watchOSApplicationExtension 10.0, *) {
infoToShare = Set([
HKSampleType.quantityType(forIdentifier: .stepCount)!,
HKSampleType.quantityType(forIdentifier: .heartRate)!,
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
HKSampleType.workoutType()
])
} else {
// Fallback on earlier versions
infoToShare = Set([
HKSampleType.quantityType(forIdentifier: .stepCount)!,
HKSampleType.quantityType(forIdentifier: .heartRate)!,
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.workoutType()
])
}
HKHealthStore().requestAuthorization(toShare: infoToShare, read: infoToRead) { (success, error) in
if success {
@@ -168,6 +189,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
}
func startWorkOut() {
WorkoutTracking.lastDateMetric = Date()
print("Start workout")
configWorkout()
workoutSession.startActivity(with: Date())
@@ -312,6 +334,68 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
handleSendStatisticsData(statistics)
}
}
if #available(watchOSApplicationExtension 10.0, *) {
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
doubleValue: WorkoutTracking.power)
if(WorkoutTracking.lastDateMetric.distance(to: Date()) < 1) {
return
}
guard let powerType = HKQuantityType.quantityType(
forIdentifier: .cyclingPower) else {
return
}
let wattPerIntervalSample = HKQuantitySample(type: powerType,
quantity: wattPerInterval,
start: WorkoutTracking.lastDateMetric,
end: Date())
workoutBuilder.add([wattPerIntervalSample]) {(success, error) in
if let error = error {
print(error)
}
}
let cadencePerInterval = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.minute()),
doubleValue: WorkoutTracking.cadence)
guard let cadenceType = HKQuantityType.quantityType(
forIdentifier: .cyclingCadence) else {
return
}
let cadencePerIntervalSample = HKQuantitySample(type: cadenceType,
quantity: cadencePerInterval,
start: WorkoutTracking.lastDateMetric,
end: Date())
workoutBuilder.add([cadencePerIntervalSample]) {(success, error) in
if let error = error {
print(error)
}
}
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
doubleValue: WorkoutTracking.speed * 0.277778)
guard let speedType = HKQuantityType.quantityType(
forIdentifier: .cyclingSpeed) else {
return
}
let speedPerIntervalSample = HKQuantitySample(type: speedType,
quantity: speedPerInterval,
start: WorkoutTracking.lastDateMetric,
end: Date())
workoutBuilder.add([speedPerIntervalSample]) {(success, error) in
if let error = error {
print(error)
}
}
} else {
// Fallback on earlier versions
}
WorkoutTracking.lastDateMetric = Date()
}
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

25
src/ChartFooter.qml Normal file
View File

@@ -0,0 +1,25 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.0
import Qt.labs.settings 1.0
ColumnLayout {
anchors.fill: parent
Loader {
id: chartFooterLoader
sourceComponent: ChartFooterInnerJS
anchors.fill: parent
active: false
}
Loader {
anchors.fill: parent
source: CHARTJS ? "ChartFooterInnerJS.qml":"ChartFooterInnerNoJS.qml"
onLoaded: {
if(CHARTJS) {
chartFooterLoader.active = true;
}
}
}
}

View File

@@ -0,0 +1,31 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.0
import Qt.labs.settings 1.0
import QtWebView 1.1
ColumnLayout {
anchors.fill: parent
Settings {
id: settings
}
WebView {
id: webView
anchors.fill: parent
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/chartjs/chartlive.htm"
visible: rootItem.chartFooterVisible
onLoadingChanged: {
if (loadRequest.errorString) {
console.error(loadRequest.errorString);
console.error("port " + settings.value("template_inner_QZWS_port"));
}
}
onVisibleChanged: {
console.log("onVisibleChanged" + visible)
if(visible === true) {
reload();
}
}
}
}

View File

@@ -0,0 +1,12 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.0
import Qt.labs.settings 1.0
ColumnLayout {
anchors.fill: parent
Settings {
id: settings
}
}

View File

@@ -291,59 +291,75 @@ HomeForm{
}
footer:
Rectangle {
objectName: "footerrectangle"
visible: rootItem.videoVisible
anchors.top: gridView.bottom
Item {
width: parent.width
height: parent.height / 2
// Removed Timer, Play/Pause/Resume is now done via Homeform.cpp
/*
Timer {
id: pauseTimer
interval: 1000; running: true; repeat: true
onTriggered: { if(visible == true) { (rootItem.currentSpeed > 0 ?
videoPlaybackHalf.play() :
videoPlaybackHalf.pause()) } }
height: (rootItem.chartFooterVisible ? parent.height / 4 : parent.height / 2)
anchors.top: gridView.bottom
visible: rootItem.chartFooterVisible || rootItem.videoVisible
Rectangle {
id: chartFooterRectangle
visible: rootItem.chartFooterVisible
anchors.fill: parent
ChartFooter {
anchors.fill: parent
visible: rootItem.chartFooterVisible
}
}
*/
onVisibleChanged: {
if(visible === true) {
console.log("mediaPlayer onCompleted: " + rootItem.videoPath)
console.log("videoRate: " + rootItem.videoRate)
videoPlaybackHalf.source = rootItem.videoPath
//videoPlaybackHalf.playbackRate = rootItem.videoRate
Rectangle {
objectName: "footerrectangle"
visible: rootItem.videoVisible
anchors.fill: parent
// Removed Timer, Play/Pause/Resume is now done via Homeform.cpp
/*
Timer {
id: pauseTimer
interval: 1000; running: true; repeat: true
onTriggered: { if(visible == true) { (rootItem.currentSpeed > 0 ?
videoPlaybackHalf.play() :
videoPlaybackHalf.pause()) } }
}
*/
onVisibleChanged: {
if(visible === true) {
console.log("mediaPlayer onCompleted: " + rootItem.videoPath)
console.log("videoRate: " + rootItem.videoRate)
videoPlaybackHalf.source = rootItem.videoPath
//videoPlaybackHalf.playbackRate = rootItem.videoRate
videoPlaybackHalf.seek(rootItem.videoPosition)
videoPlaybackHalf.play()
videoPlaybackHalf.muted = rootItem.currentCoordinateValid
} else {
videoPlaybackHalf.stop()
}
videoPlaybackHalf.seek(rootItem.videoPosition)
videoPlaybackHalf.play()
videoPlaybackHalf.muted = rootItem.currentCoordinateValid
} else {
videoPlaybackHalf.stop()
}
}
MediaPlayer {
id: videoPlaybackHalf
objectName: "videoplaybackhalf"
autoPlay: false
playbackRate: rootItem.videoRate
MediaPlayer {
id: videoPlaybackHalf
objectName: "videoplaybackhalf"
autoPlay: false
playbackRate: rootItem.videoRate
onError: {
if (videoPlaybackHalf.NoError !== error) {
console.log("[qmlvideo] VideoItem.onError error " + error + " errorString " + errorString)
onError: {
if (videoPlaybackHalf.NoError !== error) {
console.log("[qmlvideo] VideoItem.onError error " + error + " errorString " + errorString)
}
}
}
}
VideoOutput {
id:videoPlayer
anchors.fill: parent
source: videoPlaybackHalf
}
}
VideoOutput {
id:videoPlayer
anchors.fill: parent
source: videoPlaybackHalf
}
}
}
MouseArea {
property int currentId: -1 // Original position in model

View File

@@ -93,7 +93,7 @@ Item {
onLinkActivated: Qt.openUrlExternally(link)
}
/*Button {
Button {
id: restoreButton
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
@@ -101,7 +101,8 @@ Item {
text: "Restore Purchases"
onClicked: {
console.log("restoring...");
toast.show("Restoring...");
iapStore.restorePurchases();
}
}*/
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.13.98" android:versionCode="614" android:installLocation="auto">
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.16.15" android:versionCode="644" 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 -->
@@ -79,6 +79,19 @@
android:name=".ForegroundService"
android:enabled="true"
android:exported="true"></service>
<service
android:name=".WearableMessageListenerService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<data
android:host="*"
android:pathPrefix="/qz"
android:scheme="wear" />
</intent-filter>
</service>
<service android:name=".ChannelService"></service>
<service android:name=".FloatingWindowGFG" android:enabled="true" android:exported="true"/>

View File

@@ -46,6 +46,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.github.mik3y:usb-serial-for-android:v3.4.6'
androidTestImplementation "com.android.support:support-annotations:28.0.0"
implementation 'com.google.android.gms:play-services-wearable:+'
}
android {

View File

@@ -35,11 +35,13 @@ public class Ant {
static boolean speedRequest = false;
static boolean heartRequest = false;
static boolean garminKey = false;
static boolean treadmill = false;
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey) {
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill) {
Log.v(TAG, "antStart");
speedRequest = SpeedRequest;
heartRequest = HeartRequest;
treadmill = Treadmill;
garminKey = GarminKey;
activity = a;

View File

@@ -50,6 +50,7 @@ public class ChannelService extends Service {
HeartChannelController heartChannelController = null;
PowerChannelController powerChannelController = null;
SpeedChannelController speedChannelController = null;
SDMChannelController sdmChannelController = null;
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
@Override
@@ -104,6 +105,9 @@ public class ChannelService extends Service {
if (null != speedChannelController) {
speedChannelController.speed = speed;
}
if (null != sdmChannelController) {
sdmChannelController.speed = speed;
}
}
void setPower(int power) {
@@ -119,6 +123,9 @@ public class ChannelService extends Service {
if (null != speedChannelController) {
speedChannelController.cadence = cadence;
}
if (null != sdmChannelController) {
sdmChannelController.cadence = cadence;
}
}
int getHeart() {
@@ -141,8 +148,12 @@ public class ChannelService extends Service {
heartChannelController = new HeartChannelController(acquireChannel());
if (Ant.speedRequest) {
powerChannelController = new PowerChannelController(acquireChannel());
speedChannelController = new SpeedChannelController(acquireChannel());
if(Ant.treadmill) {
sdmChannelController = new SDMChannelController(acquireChannel());
} else {
powerChannelController = new PowerChannelController(acquireChannel());
speedChannelController = new SpeedChannelController(acquireChannel());
}
}
}
@@ -153,9 +164,12 @@ public class ChannelService extends Service {
powerChannelController.close();
if (speedChannelController != null)
speedChannelController.close();
if (sdmChannelController != null)
sdmChannelController.close();
heartChannelController = null;
powerChannelController = null;
speedChannelController = null;
sdmChannelController = null;
}
AntChannel acquireChannel() throws ChannelNotAvailableException {

View File

@@ -0,0 +1,308 @@
/*
* Copyright 2012 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
import com.dsi.ant.channel.IAntChannelEventHandler;
import com.dsi.ant.message.ChannelId;
import com.dsi.ant.message.ChannelType;
import com.dsi.ant.message.EventCode;
import com.dsi.ant.message.fromant.ChannelEventMessage;
import com.dsi.ant.message.fromant.MessageFromAntType;
import com.dsi.ant.message.ipc.AntMessageParcel;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.Random;
public class SDMChannelController {
// The device type and transmission type to be part of the channel ID message
private static final int CHANNEL_SPEED_DEVICE_TYPE = 0x7C;
private static final int CHANNEL_SPEED_TRANSMISSION_TYPE = 1;
// The period and frequency values the channel will be configured to
private static final int CHANNEL_SPEED_PERIOD = 8134; // 1 Hz
private static final int CHANNEL_SPEED_FREQUENCY = 57;
private static final String TAG = SDMChannelController.class.getSimpleName();
public static final int SPEED_SENSOR_ID = 0x9e3d4b99;
private static final double MILLISECOND_TO_1_1024_CONVERSION = 0.9765625;
private AntChannel mAntChannel;
private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback();
private boolean mIsOpen;
double speed = 0.0;
int cadence = 0;
byte stride_count = 0;
public SDMChannelController(AntChannel antChannel) {
mAntChannel = antChannel;
openChannel();
}
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
Log.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type. In
// order for master (TX) channels and slave (RX) channels to connect, they
// must have the same channel ID, or wildcard (0) is used.
ChannelId channelId = new ChannelId(SPEED_SENSOR_ID & 0xFFFF,
CHANNEL_SPEED_DEVICE_TYPE, CHANNEL_SPEED_TRANSMISSION_TYPE);
try {
// Setting the channel event handler so that we can receive messages from ANT
mAntChannel.setChannelEventHandler(mChannelEventCallback);
// Performs channel assignment by assigning the type to the channel. Additional
// features (such as, background scanning and frequency agility) can be enabled
// by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment).
mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER);
/*
* Configures the channel ID, messaging period and rf frequency after assigning,
* then opening the channel.
*
* For any additional ANT features such as proximity search or background scanning, refer to
* the ANT Protocol Doc found at:
* http://www.thisisant.com/resources/ant-message-protocol-and-usage/
*/
mAntChannel.setChannelId(channelId);
mAntChannel.setPeriod(CHANNEL_SPEED_PERIOD);
mAntChannel.setRfFrequency(CHANNEL_SPEED_FREQUENCY);
mAntChannel.open();
mIsOpen = true;
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
// This will release, and therefore unassign if required
channelError("Open failed", e);
}
}
} else {
Log.w(TAG, "No channel available");
}
return mIsOpen;
}
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
Log.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
StringBuilder logString;
if (e.getResponseMessage() != null) {
String initiatingMessageId = "0x" + Integer.toHexString(
e.getResponseMessage().getInitiatingMessageId());
String rawResponseCode = "0x" + Integer.toHexString(
e.getResponseMessage().getRawResponseCode());
logString = new StringBuilder(error)
.append(". Command ")
.append(initiatingMessageId)
.append(" failed with code ")
.append(rawResponseCode);
} else {
String attemptedMessageId = "0x" + Integer.toHexString(
e.getAttemptedMessageType().getMessageId());
String failureReason = e.getFailureReason().toString();
logString = new StringBuilder(error)
.append(". Command ")
.append(attemptedMessageId)
.append(" failed with reason ")
.append(failureReason);
}
Log.e(TAG, logString.toString());
mAntChannel.release();
Log.e(TAG, "ANT Command Failed");
}
public void close() {
// TODO kill all our resources
if (null != mAntChannel) {
mIsOpen = false;
// Releasing the channel to make it available for others.
// After releasing, the AntChannel instance cannot be reused.
mAntChannel.release();
mAntChannel = null;
}
Log.e(TAG, "Channel Closed");
}
/**
* Implements the Channel Event Handler Interface so that messages can be
* received and channel death events can be handled.
*/
public class ChannelEventCallback implements IAntChannelEventHandler {
long lastTime = 0;
double totalWay = 0.0;
double totalRotations = 0.0;
long lastSpeedEventTime = 0;
long lastCadenceEventTime = 0;
long elapsedMillis = 0;
int rotations;
int rev;
double wheel = 0.1;
Timer carousalTimer = null;
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
Log.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Log.d(TAG, "Tx Unsollicited");
long realtimeMillis = SystemClock.elapsedRealtime();
double speedM_s = speed / 3.6;
long deltaTime = (realtimeMillis - lastTime);
lastTime = realtimeMillis;
byte[] payload = new byte[8];
payload[0] = (byte) 0x01;
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
payload[2] = (byte) ((lastTime % 256000) / 1000);
payload[3] = (byte) 0x00;
payload[4] = (byte) speedM_s;
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
payload[6] = (byte) stride_count++; // bad but it works on zwift
payload[7] = (byte) ((double)deltaTime * 0.03125);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
}
}, 0, 250); // delay
}
// Switching on message type to handle different types of messages
switch (messageType) {
// If data message, construct from parcel and update channel data
case BROADCAST_DATA:
// Rx Data
//updateData(new BroadcastDataMessage(antParcel).getPayload());
break;
case ACKNOWLEDGED_DATA:
// Rx Data
//updateData(new AcknowledgedDataMessage(antParcel).getPayload());
break;
case CHANNEL_EVENT:
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
Log.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
case TX:
long realtimeMillis = SystemClock.elapsedRealtime();
double speedM_s = speed / 3.6;
long deltaTime = (realtimeMillis - lastTime);
// in case the treadmill doesn't provide cadence, I have to force it. ANT+ requires cadence
lastTime = realtimeMillis;
byte[] payload = new byte[8];
payload[0] = (byte) 0x01;
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
payload[2] = (byte) ((lastTime % 256000) / 1000);
payload[3] = (byte) 0x00;
payload[4] = (byte) speedM_s;
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
payload[6] = (byte) stride_count;
payload[7] = (byte) ((double)deltaTime * 0.03125);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
break;
case CHANNEL_COLLISION:
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
Log.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:
case RX_FAIL_GO_TO_SEARCH:
case TRANSFER_RX_FAILED:
case TRANSFER_TX_COMPLETED:
case TRANSFER_TX_FAILED:
case TRANSFER_TX_START:
case UNKNOWN:
// TODO More complex communication will need to handle these events
break;
}
break;
case ANT_VERSION:
case BURST_TRANSFER_DATA:
case CAPABILITIES:
case CHANNEL_ID:
case CHANNEL_RESPONSE:
case CHANNEL_STATUS:
case SERIAL_NUMBER:
case OTHER:
// TODO More complex communication will need to handle these message types
break;
}
}
}
}

View File

@@ -0,0 +1,42 @@
package org.cagnulen.qdomyoszwift;
import android.app.ActivityManager;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
public class WearableController {
static Context _context;
static Intent _intent = null;
public static void start(Context context) {
_context = context;
if(_intent == null)
_intent = new Intent(context, WearableMessageListenerService.class);
// FloatingWindowGFG service is started
context.startService(_intent);
Log.v("WearableController", "started");
}
public static int getHeart() {
return WearableMessageListenerService.getHeart();
}
}

View File

@@ -0,0 +1,131 @@
package org.cagnulen.qdomyoszwift;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.MessageClient;
import com.google.android.gms.wearable.DataClient;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.DataMap;
import android.util.Log;
import android.os.Bundle;
import com.google.android.gms.common.api.Status;
import java.io.InputStream;
public class WearableMessageListenerService extends Service implements
MessageClient.OnMessageReceivedListener, GoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener,DataClient.OnDataChangedListener {
private GoogleApiClient googleApiClient;
private MessageClient mWearableClient;
private String TAG = "WearableMessageListenerService";
private static int heart_rate = 0;
@Override
public void onCreate() {
super.onCreate();
Log.v("WearableMessageListenerService","onCreate");
}
public static int getHeart() {
return heart_rate;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Your service logic here
googleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.addConnectionCallbacks (this)
.addOnConnectionFailedListener(this)
.build();
googleApiClient.connect();
// Register the MessageClient.OnMessageReceivedListener
mWearableClient = Wearable.getMessageClient(this);
mWearableClient.addListener(this);
Wearable.getDataClient(this).addListener(this);
Log.v("WearableMessageListenerService","onStartCommand");
// Return START_STICKY to restart the service if it's killed by the system
return START_STICKY;
}
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent event : dataEvents) {
if (event.getType() == DataEvent.TYPE_DELETED) {
Log.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
Log.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
if(event.getDataItem().getUri().getPath().equals("/qz")) {
new Thread(new Runnable() {
@Override
public void run() {
DataItemBuffer result = Wearable.DataApi.getDataItems(googleApiClient).await();
if (result.getStatus().isSuccess()) {
if (result.getCount() == 1) {
heart_rate = DataMap.fromByteArray(result.get(0).getData())
.getInt("heart_rate", 0);
} else {
Log.e(TAG, "Unexpected number of DataItems found.\n"
+ "\tExpected: 1\n"
+ "\tActual: " + result.getCount());
}
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onHandleIntent: failed to get current alarm state");
}
Log.d(TAG, "Heart: " + heart_rate);
}
}).start();
}
}
}
}
@Override
public void onConnected(Bundle bundle) {
Log.v("WearableMessageListenerService","onConnected");
}
@Override
public void onConnectionSuspended(int i) {
Log.v("WearableMessageListenerService","onConnectionSuspended");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.v("WearableMessageListenerService","onConnectionFailed");
}
@Override
public void onMessageReceived(final MessageEvent messageEvent) {
String path = messageEvent.getPath();
byte[] data = messageEvent.getData();
// Handle the received message data here
String messageData = new String(data); // Assuming it's a simple string message
Log.v("Wearable", path);
Log.v("Wearable", messageData);
// You can then perform actions or update data in your service based on the received message
}
@Override
public IBinder onBind(Intent intent) {
// This service does not support binding
return null;
}
}

View File

@@ -288,6 +288,8 @@ uint16_t bike::wattFromHR(bool useSpeedAndCadence) {
} else {
watt = 0;
}
} else {
watt = currentCadence().value() * 1.2; // random value cloned from Zwift when HR is not available
}
return watt;
}

View File

@@ -109,8 +109,9 @@ void bluetooth::finished() {
QSettings settings;
QString nordictrack_2950_ip =
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
// wifi devices on windows
if (!nordictrack_2950_ip.isEmpty()) {
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty()) {
// faking a bluetooth device
qDebug() << "faking a bluetooth device for nordictrack_2950_ip";
deviceDiscovered(QBluetoothDeviceInfo());
@@ -422,6 +423,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
bool sole_inclination =
settings.value(QZSettings::sole_treadmill_inclination, QZSettings::default_sole_treadmill_inclination).toBool();
QString ftms_rower = settings.value(QZSettings::ftms_rower, QZSettings::default_ftms_rower).toString();
QString ftms_bike = settings.value(QZSettings::ftms_bike, QZSettings::default_ftms_bike).toString();
QString ftms_treadmill = settings.value(QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill).toString();
if (!heartRateBeltFound) {
@@ -711,7 +714,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(nordictrackifitadbTreadmill);
} else if (!tdf_10_ip.isEmpty() && !nordictrackifitadbBike) {
this->stopDiscovery();
nordictrackifitadbBike = new nordictrackifitadbbike(noWriteResistance, noHeartService);
nordictrackifitadbBike = new nordictrackifitadbbike(noWriteResistance, noHeartService,
bikeResistanceOffset, bikeResistanceGain);
emit deviceConnected(b);
connect(nordictrackifitadbBike, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
@@ -1025,6 +1029,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("WALKINGPAD")) ||
!b.name().toUpper().compare(QStringLiteral("RE")) || // just "RE"
b.name().toUpper().startsWith(QStringLiteral("KS-H")) ||
b.name().toUpper().startsWith(QStringLiteral("KS-BLC")) || // Walkingpad C2 #1672
b.name().toUpper().startsWith(
QStringLiteral("KS-BLR"))) && // Treadmill KingSmith WalkingPad R2 Pro KS-HCR1AA
!kingsmithR1ProTreadmill &&
@@ -1089,7 +1094,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
if (this->discoveryAgent && !this->discoveryAgent->isActive())
emit searchingStop();
this->signalBluetoothDeviceConnected(trueTreadmill);
} else if ((b.name().toUpper().startsWith(QStringLiteral("F80")) ||
} else if (((b.name().toUpper().startsWith(QStringLiteral("F80")) && sole_inclination) ||
b.name().toUpper().startsWith(QStringLiteral("F65")) ||
b.name().toUpper().startsWith(QStringLiteral("TT8")) ||
b.name().toUpper().startsWith(QStringLiteral("F63")) ||
@@ -1160,16 +1165,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("CT800")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TRX4500")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("MATRIXTF50")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("T01_")) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("TF-")) &&
horizon_treadmill_force_ftms) || // FTMS, TF-769DF2
((b.name().toUpper().startsWith(QStringLiteral("TOORX")) ||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")))) &&
!toorx_ftms && toorx_ftms_treadmill) ||
!b.name().compare(ftms_treadmill, Qt::CaseInsensitive) ||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("F85")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
b.name().toUpper().startsWith(QStringLiteral("ESANGLINKER"))) &&
!horizonTreadmill && filter) {
this->setLastBluetoothDevice(b);
@@ -1314,10 +1322,12 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton)
.toBool()) || // ss2k on a peloton bike
(b.name().toUpper().startsWith("KICKR CORE")) ||
(b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) ||
(b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) ||
(b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) ||
(b.name().toUpper().startsWith("DIRETO XR")) || (b.name().toUpper().startsWith("SMB1")) ||
(b.name().toUpper().startsWith("INRIDE"))) &&
(b.name().toUpper().startsWith("DIRETO XR")) ||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) &&
!ftmsBike && !snodeBike && !fitPlusBike && !stagesBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -1429,6 +1439,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(ftmsRower);
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-SD-SPT"))) &&
!echelonStride && filter) {
this->setLastBluetoothDevice(b);
@@ -1845,6 +1856,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
trxappgateusb->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(trxappgateusb);
} else if ((b.name().toUpper().startsWith(QStringLiteral("TUN ")) ||
b.name().toUpper().startsWith(QStringLiteral("FITHIWAY")) ||
((b.name().startsWith(QStringLiteral("TOORX")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOIE+")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) ||
@@ -1915,6 +1927,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
skandikaWiriBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(skandikaWiriBike);
} else if (((b.name().toUpper().startsWith("RQ") && b.name().length() == 5) ||
(b.name().toUpper().startsWith("R-Q") && b.name().length() > 6) ||
(b.name().toUpper().startsWith("SCH130")) || // not a renpho bike an FTMS one
((b.name().startsWith(QStringLiteral("TOORX"))) && toorx_ftms && !toorx_ftms_treadmill)) &&
!renphoBike && !snodeBike && !fitPlusBike && filter) {
@@ -2305,10 +2318,12 @@ void bluetooth::connectedAndDiscovered() {
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
"activity", "()Landroid/app/Activity;");
KeepAwakeHelper::antObject(true)->callMethod<void>(
"antStart", "(Landroid/app/Activity;ZZZ)V", activity.object<jobject>(),
"antStart", "(Landroid/app/Activity;ZZZZ)V", activity.object<jobject>(),
settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool(),
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool(),
settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool());
settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool(),
device()->deviceType() == bluetoothdevice::TREADMILL ||
device()->deviceType() == bluetoothdevice::ELLIPTICAL);
}
if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) {

View File

@@ -247,9 +247,15 @@ void bluetoothdevice::update_hr_from_external() {
long appleWatchHeartRate = h.heartRate();
h.setKcal(KCal.value());
h.setDistance(Distance.value());
h.setSpeed(Speed.value());
h.setPower(m_watt.value());
h.setCadence(Cadence.value());
Heart = appleWatchHeartRate;
qDebug() << "Current Heart from Apple Watch: " << QString::number(appleWatchHeartRate);
#endif
#endif
#ifdef Q_OS_ANDROID
Heart = QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/WearableController", "getHeart", "()I");
#endif
}
}

View File

@@ -6,18 +6,15 @@ CharacteristicNotifier2A53::CharacteristicNotifier2A53(bluetoothdevice *Bike, QO
int CharacteristicNotifier2A53::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
value.append(0x02); // total distance
uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256;
uint32_t distance = Bike->odometer() * 10000.0;
value.append((char)((speed & 0xFF)));
value.append((char)((speed >> 8) & 0xFF));
value.append((char)(Bike->currentCadence().value()));
value.append((char)((distance & 0xFF)));
value.append((char)((distance >> 8) & 0xFF));
value.append((char)((distance >> 16) & 0xFF));
value.append((char)((distance >> 24) & 0xFF));
return CN_OK;
} else
return CN_INVALID;
value.append(0x02); // total distance
uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256;
uint32_t distance = Bike->odometer() * 10000.0;
value.append((char)((speed & 0xFF)));
value.append((char)((speed >> 8) & 0xFF));
value.append((char)(Bike->currentCadence().value()));
value.append((char)((distance & 0xFF)));
value.append((char)((distance >> 8) & 0xFF));
value.append((char)((distance >> 16) & 0xFF));
value.append((char)((distance >> 24) & 0xFF));
return CN_OK;
}

View File

@@ -1,5 +1,6 @@
#include "characteristicnotifier2ad2.h"
#include "elliptical.h"
#include "rower.h"
#include "treadmill.h"
#include <QSettings>
@@ -8,11 +9,17 @@ CharacteristicNotifier2AD2::CharacteristicNotifier2AD2(bluetoothdevice *Bike, QO
int CharacteristicNotifier2AD2::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
QSettings settings;
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
bool rowerAsABike = !virtual_device_rower && dt == bluetoothdevice::ROWING;
double normalizeWattage = Bike->wattsMetric().value();
if (normalizeWattage < 0)
normalizeWattage = 0;
if (dt == bluetoothdevice::BIKE) {
if (dt == bluetoothdevice::BIKE || rowerAsABike) {
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
@@ -32,7 +39,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
value.append(char(Bike->currentHeart().value())); // Actual value.
value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix
return CN_OK;
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL || dt == bluetoothdevice::ROWING) {
QSettings settings;
bool double_cadence = settings.value(QZSettings::powr_sensor_running_cadence_double, QZSettings::default_powr_sensor_running_cadence_double).toBool();
double cadence_multiplier = 2.0;
@@ -50,6 +57,8 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
cadence = ((elliptical *)Bike)->currentCadence().value();
else if (dt == bluetoothdevice::TREADMILL)
cadence = ((treadmill *)Bike)->currentCadence().value();
else if (dt == bluetoothdevice::ROWING)
cadence = ((rower *)Bike)->currentCadence().value();
value.append((char)((uint16_t)(cadence * cadence_multiplier) & 0xFF)); // cadence
value.append((char)(((uint16_t)(cadence * cadence_multiplier) >> 8) & 0xFF)); // cadence

View File

@@ -391,17 +391,19 @@ void csaferower::update() {
} else
#endif
#endif
{
if (virtual_device_enabled) {
if (!virtual_device_rower) {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
// connect(virtualBike,&virtualbike::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
if (!virtual_device_rower) {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
// connect(virtualBike,&virtualbike::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
}
}

View File

@@ -56,8 +56,7 @@ void domyosrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
QStringLiteral(" // ") + info);
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info);
}
loop.exec();
@@ -196,7 +195,8 @@ void domyosrower::update() {
this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
debug("creating virtual bike interface...");
auto virtualBike = new virtualbike(this);
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset,
bikeResistanceGain);
connect(virtualBike, &virtualbike::changeInclination, this,
&domyosrower::changeInclinationRequested);
connect(virtualBike, &virtualbike::changeInclination, this, &domyosrower::changeInclination);
@@ -367,8 +367,9 @@ void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characte
double domyosrower::GetSpeedFromPacket(const QByteArray &packet) {
uint16_t convertedData = (packet.at(6) << 8) | packet.at(7);
double data = (double)convertedData / 10.0f;
return data;
if (convertedData > 65000 || convertedData == 0 || currentCadence().value() == 0)
return 0;
return (60.0 / (double)(convertedData)) * 30.0;
}
double domyosrower::GetKcalFromPacket(const QByteArray &packet) {

View File

@@ -404,6 +404,8 @@ void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) {
QSettings settings;
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
@@ -418,12 +420,19 @@ void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) {
#endif
#endif
if (virtual_device_enabled) {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike =
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
// connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
if (virtual_device_rower) {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
} else {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike =
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
// connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
}
firstStateChanged = 1;

View File

@@ -28,6 +28,7 @@
#include "bike.h"
#include "virtualbike.h"
#include "virtualrower.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"

View File

@@ -67,6 +67,7 @@ double elliptical::speedFromWatts() {
if (wattsMetric().value() > 0) {
double vwatts = ((9.8 * weight) * (currentInclination().value() / 100.0));
speed = 210.0 / ((wattsMetric().value() - vwatts) / 75.0 / weight * 1000.0);
speed = 60.0 / speed;
}
return speed;
}

View File

@@ -27,7 +27,7 @@ class elliptical : public bluetoothdevice {
void clearStats() override;
void setPaused(bool p) override;
void setLap() override;
uint16_t watts();
virtual uint16_t watts();
double speedFromWatts();
void setGears(double d);
double gears();

View File

@@ -390,7 +390,10 @@ void eslinkertreadmill::characteristicChanged(const QLowEnergyCharacteristic &ch
}
} else if (treadmill_type == COSTAWAY) {
const double miles = 1.60934;
Speed = newValue.at(3) * miles;
if(newValue.at(3) == 0xFF)
Speed = 0;
else
Speed = (double)((uint8_t)newValue.at(3)) / 10.0 * miles;
Inclination = 0; // this treadmill doesn't have inclination
emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value()));
}

View File

@@ -276,7 +276,8 @@ void fitshowtreadmill::serviceDiscovered(const QBluetoothUuid &gatt) {
QBluetoothUuid nobleproconnect(QStringLiteral("0000ae00-0000-1000-8000-00805f9b34fb"));
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString() + QStringLiteral(" ") +
QString::number(servRepr));
if (gatt == nobleproconnect || servRepr == 0xfff0 || (servRepr == 0xffe0 && serviceId.isNull())) {
if ((gatt == nobleproconnect && serviceId.isNull()) || servRepr == 0xfff0 || (servRepr == 0xffe0 && serviceId.isNull())) {
qDebug() << "adding" << gatt.toString() << "as the default service";
serviceId = gatt; // NOTE: clazy-rule-of-tow
}
}
@@ -470,7 +471,11 @@ void fitshowtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
.toBool())
miles = 1.60934;
Speed = speed * miles;
if(IS_RUNNING)
Speed = speed * miles;
else
Speed = 0;
if (Speed.value() != speed) {
emit speedChanged(speed);
}
@@ -499,6 +504,9 @@ void fitshowtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
long appleWatchHeartRate = h->heartRate();
h->setKcal(KCal.value());
h->setDistance(Distance.value());
h->setSpeed(Speed.value());
h->setPower(m_watt.value());
h->setCadence(Cadence.value());
Heart = appleWatchHeartRate;
debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate));
#else

View File

@@ -52,11 +52,15 @@ void ftmsbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer);
if (gattWriteCharControlPointId.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
} else {
gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer);
}
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
QStringLiteral(" // ") + info);
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info);
}
loop.exec();
@@ -89,7 +93,8 @@ void ftmsbike::forcePower(int16_t requestPower) {
void ftmsbike::forceResistance(resistance_t requestResistance) {
QSettings settings;
if (!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool()) {
if (!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool() &&
resistance_lvl_mode == false) {
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
double fr = (((double)requestResistance) * bikeResistanceGain) + ((double)bikeResistanceOffset);
@@ -272,16 +277,20 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
}
if (Flags.totDistance) {
/*
* the distance sent from the most trainers is a total distance, so it's useless for QZ
*
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint32_t)((uint8_t)newValue.at(index)))) /
1000.0;
1000.0;*/
index += 3;
} else {
Distance += ((Speed.value() / 3600000.0) *
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
}
Distance += ((Speed.value() / 3600000.0) *
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
if (Flags.resistanceLvl) {
@@ -290,7 +299,8 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
emit resistanceRead(Resistance.value());
index += 2;
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
} else {
resistance_received = true;
}
double ac = 0.01243107769;
double bc = 1.145964912;
double cc = -23.50977444;
@@ -308,10 +318,13 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
(2.0 * ar)) *
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
Resistance = m_pelotonResistance;
emit resistanceRead(Resistance.value());
if (!resistance_received) {
Resistance = m_pelotonResistance;
emit resistanceRead(Resistance.value());
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
}
}
}
if (Flags.instantPower) {
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
@@ -361,7 +374,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
#endif
{
if (Flags.heartRate && !disable_hr_frommachinery && newValue.length() > index) {
Heart = ((double)((newValue.at(index))));
Heart = ((double)(((uint8_t)newValue.at(index))));
// index += 1; // NOTE: clang-analyzer-deadcode.DeadStores
emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value()));
} else {
@@ -551,7 +564,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
#endif
{
if (Flags.heartRate && !disable_hr_frommachinery && newValue.length() > index) {
Heart = ((double)((newValue.at(index))));
Heart = ((double)(((uint8_t)newValue.at(index))));
// index += 1; // NOTE: clang-analyzer-deadcode.DeadStores
emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value()));
} else {
@@ -847,6 +860,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
if (bluetoothDevice.name().toUpper().startsWith("SUITO")) {
qDebug() << QStringLiteral("SUITO found");
max_resistance = 16;
} else if ((bluetoothDevice.name().toUpper().startsWith("MAGNUS "))) {
qDebug() << QStringLiteral("MAGNUS found");
resistance_lvl_mode = true;
}
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);

View File

@@ -104,6 +104,9 @@ class ftmsbike : public bike {
bool powerForced = false;
bool resistance_lvl_mode = false;
bool resistance_received = false;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif

View File

@@ -54,8 +54,7 @@ void ftmsrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer);
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
QStringLiteral(" // ") + info);
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info);
}
loop.exec();
@@ -466,6 +465,8 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
QSettings settings;
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
@@ -481,16 +482,25 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
#endif
#endif
{
if (virtual_device_enabled) {
emit debug(QStringLiteral("creating virtual bike interface..."));
if (!virtual_device_rower) {
emit debug(QStringLiteral("creating virtual bike interface..."));
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
// connect(virtualBike,&virtualbike::debug ,this,&ftmsrower::debug);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
// connect(virtualBike,&virtualbike::debug ,this,&ftmsrower::debug);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
}
firstStateChanged = 1;
// ********************************************************************************************************
}
firstStateChanged = 1;
// ********************************************************************************************************
}
void ftmsrower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {

View File

@@ -28,6 +28,7 @@
#include "rower.h"
#include "virtualbike.h"
#include "virtualrower.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"

View File

@@ -47,17 +47,6 @@ using namespace std::chrono_literals;
#include <QtAndroid>
#endif
#if __has_include("secret.h")
#include "secret.h"
#else
#define STRAVA_SECRET_KEY test
#if defined(WIN32)
#pragma message("DEFINE STRAVA_SECRET_KEY!!!")
#else
#warning "DEFINE STRAVA_SECRET_KEY!!!"
#endif
#endif
#ifndef STRAVA_CLIENT_ID
#define STRAVA_CLIENT_ID 7976
#if defined(WIN32)
@@ -760,7 +749,7 @@ void homeform::peloton_start_workout() {
if (!stravaPelotonActivityName.isEmpty() && !stravaPelotonInstructorName.isEmpty()) {
QString path = getWritableAppDir() + "training/" + workoutNameBasedOnBluetoothDevice() + "/" +
stravaPelotonInstructorName + "/";
QDir().mkdir(path);
QDir().mkpath(path);
lastTrainProgramFileSaved =
path + stravaPelotonActivityName.replace("/", "-") + " - " + stravaPelotonInstructorName + ".xml";
trainProgram->save(lastTrainProgramFileSaved);
@@ -1038,6 +1027,19 @@ void homeform::trainProgramSignals() {
connect(this, &homeform::workoutEventStateChanged, bluetoothManager->device(),
&bluetoothdevice::workoutEventStateChanged);
if (trainProgram) {
setChartIconVisible(trainProgram->powerzoneWorkout());
if (chartFooterVisible()) {
if (trainProgram->powerzoneWorkout()) {
// reloading
setChartFooterVisible(false);
setChartFooterVisible(true);
} else {
setChartFooterVisible(false);
}
}
}
qDebug() << QStringLiteral("trainProgram associated to a device");
} else {
qDebug() << QStringLiteral("trainProgram NOT associated to a device");
@@ -2242,6 +2244,12 @@ void homeform::sortTiles() {
target_pace->setGridId(i);
dataList.append(target_pace);
}
if (settings.value(QZSettings::tile_pace_enabled, true).toBool() &&
settings.value(QZSettings::tile_pace_order, 51).toInt() == i) {
pace->setGridId(i);
dataList.append(pace);
}
}
}
@@ -2347,6 +2355,13 @@ void homeform::deviceConnected(QBluetoothDeviceInfo b) {
if (settings.value(QZSettings::floating_startup, QZSettings::default_floating_startup).toBool()) {
floatingOpen();
}
if(!settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString().compare(QZSettings::default_heart_rate_belt_name) &&
!settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
QAndroidJniObject::callStaticMethod<void>(
"org/cagnulen/qdomyoszwift/WearableController", "start", "(Landroid/content/Context;)V",
QtAndroid::androidContext().object());
}
#endif
if (settings.value(QZSettings::gears_restore_value, QZSettings::default_gears_restore_value).toBool()) {
@@ -3202,6 +3217,9 @@ void homeform::StopRequested() {
void homeform::Stop() {
QSettings settings;
m_startRequested = false;
qDebug() << QStringLiteral("Stop pressed - paused") << paused << QStringLiteral("stopped") << stopped;
if (stopped) {
@@ -3460,7 +3478,7 @@ void homeform::update() {
else if (next.speed != -1)
nextRows->setValue(QStringLiteral("S") + QString::number(next.speed) + QStringLiteral(" ") +
next.duration.toString(QStringLiteral("mm:ss")));
else if (next.inclination != -1)
else if (next.inclination != -200)
nextRows->setValue(QStringLiteral("I") + QString::number(next.inclination) + QStringLiteral(" ") +
next.duration.toString(QStringLiteral("mm:ss")));
else if (next.power != -1) {
@@ -3533,7 +3551,22 @@ void homeform::update() {
wattKg->setSecondLine(
QStringLiteral("AVG: ") + QString::number(bluetoothManager->device()->wattKg().average(), 'f', 1) +
QStringLiteral("MAX: ") + QString::number(bluetoothManager->device()->wattKg().max(), 'f', 1));
datetime->setValue(QTime::currentTime().toString(QStringLiteral("hh:mm:ss")));
QLocale locale = QLocale::system();
// Format the time based on the locale
QString timeFormat = locale.timeFormat(QLocale::ShortFormat);
bool usesAMPMFormat = timeFormat.toUpper().contains("A");
QDateTime currentTime = QDateTime::currentDateTime();
QString formattedTime;
if (usesAMPMFormat) {
// The locale uses 12-hour format with AM/PM
formattedTime = currentTime.toString("h:mm:ss AP");
} else {
// The locale uses 24-hour format
formattedTime = currentTime.toString("H:mm:ss");
}
datetime->setValue(formattedTime);
if (power5s)
watts = bluetoothManager->device()->wattsMetric().average5s();
else
@@ -3809,17 +3842,23 @@ void homeform::update() {
}
switch (trainProgram->currentRow().pace_intensity) {
case 0:
this->target_zone->setValue(tr("Easy"));
this->target_zone->setValue(tr("Rec."));
break;
case 1:
this->target_zone->setValue(tr("Moder."));
this->target_zone->setValue(tr("Easy"));
break;
case 2:
this->target_zone->setValue(tr("Chall."));
this->target_zone->setValue(tr("Moder."));
break;
case 3:
this->target_zone->setValue(tr("Chall."));
break;
case 4:
this->target_zone->setValue(tr("Max"));
break;
default:
this->target_zone->setValue(tr("N/A"));
break;
}
}
odometer->setValue(QString::number(bluetoothManager->device()->odometer() * 1000.0, 'f', 0));
@@ -3901,6 +3940,16 @@ void homeform::update() {
}
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
if (((elliptical *)bluetoothManager->device())->currentSpeed().value() > 2)
this->pace->setValue(
((elliptical *)bluetoothManager->device())->currentPace().toString(QStringLiteral("m:ss")));
else
this->pace->setValue("N/A");
this->pace->setSecondLine(
QStringLiteral("AVG: ") +
((elliptical *)bluetoothManager->device())->averagePace().toString(QStringLiteral("m:ss")) +
QStringLiteral(" MAX: ") +
((elliptical *)bluetoothManager->device())->maxPace().toString(QStringLiteral("m:ss")));
odometer->setValue(QString::number(bluetoothManager->device()->odometer() * unit_conversion, 'f', 2));
resistance = ((elliptical *)bluetoothManager->device())->currentResistance().value();
peloton_resistance = ((elliptical *)bluetoothManager->device())->pelotonResistance().value();
@@ -3953,6 +4002,18 @@ void homeform::update() {
if (trainProgram) {
int8_t lower_requested_peloton_resistance = trainProgram->currentRow().lower_requested_peloton_resistance;
int8_t upper_requested_peloton_resistance = trainProgram->currentRow().upper_requested_peloton_resistance;
double lower_requested_peloton_resistance_to_bike_resistance = 0;
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)
lower_requested_peloton_resistance_to_bike_resistance =
((bike *)bluetoothManager->device())->pelotonToBikeResistance(lower_requested_peloton_resistance);
else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING)
lower_requested_peloton_resistance_to_bike_resistance =
((rower *)bluetoothManager->device())->pelotonToBikeResistance(lower_requested_peloton_resistance);
else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL)
lower_requested_peloton_resistance_to_bike_resistance =
((elliptical *)bluetoothManager->device())
->pelotonToEllipticalResistance(lower_requested_peloton_resistance);
if (lower_requested_peloton_resistance != -1) {
this->target_peloton_resistance->setSecondLine(
QStringLiteral("MIN: ") + QString::number(lower_requested_peloton_resistance, 'f', 0) +
@@ -3967,7 +4028,10 @@ void homeform::update() {
.toBool()) {
if (lower_requested_peloton_resistance == -1) {
this->peloton_resistance->setValueFontColor(QStringLiteral("white"));
} else if (((int8_t)qRound(peloton_resistance)) < lower_requested_peloton_resistance) {
} else if (resistance < lower_requested_peloton_resistance_to_bike_resistance) {
// we need to compare the real resistance and not the peloton resistance because most of the bikes
// have a 1:3 conversion so this compare will be always true even if the actual resistance is the
// same #1608
this->peloton_resistance->setValueFontColor(QStringLiteral("red"));
} else if (((int8_t)qRound(peloton_resistance)) <= upper_requested_peloton_resistance) {
this->peloton_resistance->setValueFontColor(QStringLiteral("limegreen"));
@@ -4322,9 +4386,11 @@ void homeform::update() {
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() &&
KeepAwakeHelper::antObject(false)) {
KeepAwakeHelper::antObject(false)->callMethod<void>(
"setCadenceSpeedPower", "(FII)V", (float)bluetoothManager->device()->currentSpeed().value(), (int)watts,
(int)cadence);
double v = bluetoothManager->device()->currentSpeed().value();
v *= settings.value(QZSettings::ant_speed_gain, QZSettings::default_ant_speed_gain).toDouble();
v += settings.value(QZSettings::ant_speed_offset, QZSettings::default_ant_speed_offset).toDouble();
KeepAwakeHelper::antObject(false)->callMethod<void>("setCadenceSpeedPower", "(FII)V", (float)v, (int)watts,
(int)cadence);
}
#endif
@@ -5424,6 +5490,10 @@ bool homeform::strava_upload_file(const QByteArray &data, const QString &remoten
activityNamePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant(QStringLiteral("form-data; name=\"name\"")));
QString prefix = QStringLiteral("");
if(settings.value(QZSettings::strava_date_prefix, QZSettings::default_strava_date_prefix).toBool())
prefix = " " + QDate::currentDate().toString(Qt::TextDate);
// use metadata config if the user selected it
QString activityName =
QStringLiteral(" ") + settings.value(QZSettings::strava_suffix, QZSettings::default_strava_suffix).toString();
@@ -5436,11 +5506,11 @@ bool homeform::strava_upload_file(const QByteArray &data, const QString &remoten
pelotonHandler->current_ride_id;
} else {
if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) {
activityName = QStringLiteral("Run") + activityName;
activityName = prefix + QStringLiteral("Run") + activityName;
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) {
activityName = QStringLiteral("Row") + activityName;
activityName = prefix + QStringLiteral("Row") + activityName;
} else {
activityName = QStringLiteral("Ride") + activityName;
activityName = prefix + QStringLiteral("Ride") + activityName;
}
}
activityNamePart.setHeader(QNetworkRequest::ContentTypeHeader,
@@ -5739,6 +5809,14 @@ void homeform::setVideoIconVisible(bool value) {
emit videoIconVisibleChanged(m_VideoIconVisible);
}
bool homeform::chartIconVisible() { return m_ChartIconVisible; }
void homeform::setChartIconVisible(bool value) {
m_ChartIconVisible = value;
emit chartIconVisibleChanged(m_ChartIconVisible);
}
int homeform::videoPosition() { return m_VideoPosition; }
void homeform::setVideoPosition(int value) {
@@ -5959,6 +6037,10 @@ void homeform::sendMail() {
}
}
#ifdef SMTP_SERVER
textMessage += QStringLiteral("\n\nSMTP server: ") + QString(STRINGIFY(SMTP_SERVER));
#endif
text.setText(textMessage);
message.addPart(&text);

View File

@@ -24,6 +24,18 @@
#include "qmdnsengine/cache.h"
#include "qmdnsengine/resolver.h"
#if __has_include("secret.h")
#include "secret.h"
#else
#define STRAVA_SECRET_KEY test
#if defined(WIN32)
#pragma message("DEFINE STRAVA_SECRET_KEY!!!")
#else
#warning "DEFINE STRAVA_SECRET_KEY!!!"
#endif
#endif
class DataObject : public QObject {
Q_OBJECT
@@ -137,6 +149,8 @@ class homeform : public QObject {
Q_PROPERTY(bool mapsVisible READ mapsVisible NOTIFY mapsVisibleChanged WRITE setMapsVisible)
Q_PROPERTY(bool videoIconVisible READ videoIconVisible NOTIFY videoIconVisibleChanged WRITE setVideoIconVisible)
Q_PROPERTY(bool videoVisible READ videoVisible NOTIFY videoVisibleChanged WRITE setVideoVisible)
Q_PROPERTY(bool chartIconVisible READ chartIconVisible NOTIFY chartIconVisibleChanged WRITE setChartIconVisible)
Q_PROPERTY(bool chartFooterVisible READ chartFooterVisible NOTIFY chartFooterVisibleChanged WRITE setChartFooterVisible)
Q_PROPERTY(QUrl videoPath READ videoPath NOTIFY videoPathChanged)
Q_PROPERTY(int videoPosition READ videoPosition NOTIFY videoPositionChanged WRITE setVideoPosition)
Q_PROPERTY(double videoRate READ videoRate NOTIFY videoRateChanged WRITE setVideoRate)
@@ -381,9 +395,11 @@ class homeform : public QObject {
void setPelotonProvider(const QString &value) { m_pelotonProvider = value; }
bool generalPopupVisible();
bool licensePopupVisible();
bool mapsVisible();
bool mapsVisible();
bool videoIconVisible();
bool videoVisible() { return m_VideoVisible; }
bool chartIconVisible();
bool chartFooterVisible() { return m_ChartFooterVisible; }
int videoPosition();
double videoRate();
double currentSpeed() {
@@ -415,10 +431,15 @@ class homeform : public QObject {
}
void setLicensePopupVisible(bool value);
void setVideoIconVisible(bool value);
void setChartIconVisible(bool value);
void setVideoVisible(bool value) {
m_VideoVisible = value;
emit videoVisibleChanged(m_VideoVisible);
}
void setChartFooterVisible(bool value) {
m_ChartFooterVisible = value;
emit chartFooterVisibleChanged(m_ChartFooterVisible);
}
void setVideoPosition(int position); // on startup
void videoSeekPosition(int ms); // in realtime
void setVideoRate(double rate);
@@ -559,6 +580,8 @@ class homeform : public QObject {
bool m_MapsVisible = false;
bool m_VideoIconVisible = false;
bool m_VideoVisible = false;
bool m_ChartFooterVisible = false;
bool m_ChartIconVisible = false;
int m_VideoPosition = 0;
double m_VideoRate = 1;
QOAuth2AuthorizationCodeFlow *strava = nullptr;
@@ -804,6 +827,8 @@ class homeform : public QObject {
void videoPositionChanged(int value);
void videoPathChanged(QUrl value);
void videoRateChanged(double value);
void chartIconVisibleChanged(bool value);
void chartFooterVisibleChanged(bool value);
void currentSpeedChanged(double value);
void mapsVisibleChanged(bool value);
void autoResistanceChanged(bool value);

View File

@@ -845,7 +845,8 @@ void horizontreadmill::update() {
}
if (requestSpeed != -1) {
bool minSpeed = fabs(requestSpeed - float_one_point_round(currentSpeed().value())) >= minStepSpeed();
bool minSpeed =
fabs(requestSpeed - float_one_point_round(currentSpeed().value())) >= (minStepSpeed() - 0.09);
bool forceSpeedNeed = checkIfForceSpeedNeeding(requestSpeed);
qDebug() << "requestSpeed=" << requestSpeed << minSpeed << forceSpeedNeed
<< float_one_point_round(currentSpeed().value());

View File

@@ -0,0 +1,43 @@
<!doctype html>
<html>
<head>
<title>Line Chart</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1">
<script src="resize-observer.min.js"></script>
<script src="jquery-3.6.0.min.js"></script>
<script src="chartjs.3.4.1.min.js"></script>
<script src="moment.js"></script>
<script src="chartjs-adapter-moment.js"></script>
<script src="chartjs-plugin-annotation.min.js"></script>
<script src="globals.js"></script>
<script src="main_ws_manager.js"></script>
<script src="dochartlive.js"></script>
<script src="html2canvas.min.js"></script>
<style>
canvas{
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
html, body
{
-ms-content-zooming:none;
touch-action: none;
content-zooming: none;
overflow-y: hidden;
overflow-x: hidden;
overflow-y: none;
overflow-x: none;
margin: 0px;
}
</style>
</head>
<body style="background-color:#1d2330">
<div id="divcanvas" style="width:100vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
<canvas id="canvas"></canvas>
</div>
</body>
</html>

View File

@@ -0,0 +1,530 @@
window.chartColors = {
red: 'rgb(255, 29, 0)',
redt: 'rgb(255, 29, 0, 0.55)',
orange: 'rgb(255, 159, 64)',
oranget: 'rgb(255, 159, 64, 0.55)',
darkorange: 'rgb(255, 140, 0)',
darkoranget: 'rgb(255, 140, 0, 0.55)',
orangered: 'rgb(255, 69, 0)',
orangeredt: 'rgb(255, 69, 0, 0.55)',
yellow: 'rgb(255, 205, 86)',
yellowt: 'rgb(255, 205, 86, 0.55)',
green: 'rgb(75, 192, 192)',
greent: 'rgb(75, 192, 192, 0.55)',
blue: 'rgb(54, 162, 235)',
purple: 'rgb(153, 102, 255)',
grey: 'rgb(201, 203, 207)',
greyt: 'rgb(201, 203, 207, 0.55)',
white: 'rgb(255, 255, 255)',
whitet: 'rgb(255, 255, 255, 0.55)',
limegreen: 'rgb(50, 205, 50)',
limegreent: 'rgb(50, 205, 50, 0.55)',
gold: 'rgb(255, 215, 0)',
goldt: 'rgb(255, 215, 0, 0.55)',
black: 'rgb(0, 0, 0)',
blackt: 'rgb(0, 0, 0, 0.55)',
lightsteelblue: 'rgb(176,192,222)',
lightsteelbluet: 'rgb(176,192,222, 0.55)',
};
var ftp = 200;
var ftpZones = [];
var maxHeartRate = 190;
var heartZones = [];
var miles = 1;
var powerChart = null;
function process_trainprogram(arr) {
let powerWorkout = false;
let elapsed = 0;
for (let el of arr.list) {
if(el.power != -1) {
powerWorkout = true;
for (i=0; i<el.duration_s; i++) {
powerChart.data.datasets[1].data.push({x: elapsed++, y: el.power});
}
}
}
powerChart.options.scales.x.max = elapsed;
powerChart.update();
}
function process_arr(arr) {
let ctx = document.getElementById('canvas').getContext('2d');
let div = document.getElementById('divcanvas');
let reqpower = [];
let reqcadence = [];
let heart = [];
let cadence = [];
let speed = [];
let inclination = [];
let resistance = [];
let watts = [];
let reqresistance = [];
let pelotonresistance = [];
let pelotonreqresistance = [];
let distributionPowerZones = [];
let maxEl = 0;
let saveScreenshot = [];
let workoutName = '';
let workoutStartDate = '';
let instructorName = '';
let watts_avg = 0;
let watts_max = 0;
let heart_avg = 0;
let heart_max = 0;
let jouls = 0;
let deviceType = 0;
let cadence_avg = 0;
let peloton_resistance_avg = 0;
let calories = 0;
let distance = 0;
saveScreenshot[0] = false;
saveScreenshot[1] = false;
saveScreenshot[2] = false;
saveScreenshot[3] = false;
saveScreenshot[4] = false;
saveScreenshot[5] = false;
saveScreenshot[6] = false;
saveScreenshot[7] = false;
distributionPowerZones[0] = 0;
distributionPowerZones[1] = 0;
distributionPowerZones[2] = 0;
distributionPowerZones[3] = 0;
distributionPowerZones[4] = 0;
distributionPowerZones[5] = 0;
distributionPowerZones[6] = 0;
for (let el of arr) {
let wattel = {};
let reqpowerel = {};
let reqcadenceel = {};
let heartel = {};
let cadenceel = {};
let resistanceel = {};
let reqresistanceel = {};
let pelotonresistanceel = {};
let pelotonreqresistanceel = {};
let speedel = {};
let inclinationel = {};
let time = el.elapsed_s + el.elapsed_m * 60 + el.elapsed_h * 3600;
workoutName = el.workoutName;
workoutStartDate = el.workoutStartDate;
instructorName = el.instructorName;
watts_avg = el.watts_avg;
watts_max = el.watts_max;
heart_avg = el.heart_avg;
heart_max = el.heart_max;
jouls = el.jouls;
deviceType = el.deviceType;
peloton_resistance_avg = el.peloton_resistance_avg;
cadence_avg = el.cadence_avg;
distance = el.distance;
calories = el.calories;
maxEl = time;
wattel.x = time;
wattel.y = el.watts;
watts.push(wattel);
if(el.watts < ftpZones[0])
distributionPowerZones[0]++;
else if(el.watts < ftpZones[1])
distributionPowerZones[1]++;
else if(el.watts < ftpZones[2])
distributionPowerZones[2]++;
else if(el.watts < ftpZones[3])
distributionPowerZones[3]++;
else if(el.watts < ftpZones[4])
distributionPowerZones[4]++;
else if(el.watts < ftpZones[5])
distributionPowerZones[5]++;
else
distributionPowerZones[6]++;
reqpowerel.x = time;
reqpowerel.y = el.req_power;
// they are added from the process_trainprogram()
//reqpower.push(reqpowerel);
reqcadenceel.x = time;
reqcadenceel.y = el.req_cadence;
reqcadence.push(reqcadenceel);
heartel.x = time;
heartel.y = el.heart;
heart.push(heartel);
cadenceel.x = time;
cadenceel.y = el.cadence;
cadence.push(cadenceel);
resistanceel.x = time;
resistanceel.y = el.resistance;
resistance.push(resistanceel);
reqresistanceel.x = time;
reqresistanceel.y = el.req_resistance;
reqresistance.push(reqresistanceel);
pelotonresistanceel.x = time;
pelotonresistanceel.y = el.peloton_resistance;
pelotonresistance.push(pelotonresistanceel);
pelotonreqresistanceel.x = time;
pelotonreqresistanceel.y = el.peloton_req_resistance;
pelotonreqresistance.push(pelotonreqresistanceel);
speedel.x = time;
speedel.y = el.speed;
speed.push(speedel);
inclinationel.x = time;
inclinationel.y = el.inclination;
inclination.push(inclinationel);
}
const backgroundFill = {
id: 'custom_canvas_background_color',
beforeDraw: (chart) => {
const ctx = chart.canvas.getContext('2d');
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
};
let config = {
type: 'line',
plugins: [backgroundFill],
data: {
datasets: [{
label: 'Watts',
backgroundColor: window.chartColors.red,
borderColor: window.chartColors.red,
cubicInterpolationMode: 'monotone',
data: watts,
fill: false,
pointRadius: 0,
borderWidth: 2,
segment: {
borderColor: ctx => ctx.p0.parsed.y < ftpZones[0] && ctx.p1.parsed.y < ftpZones[0] ? window.chartColors.grey :
ctx.p0.parsed.y < ftpZones[1] && ctx.p1.parsed.y < ftpZones[1] ? window.chartColors.limegreen :
ctx.p0.parsed.y < ftpZones[2] && ctx.p1.parsed.y < ftpZones[2] ? window.chartColors.gold :
ctx.p0.parsed.y < ftpZones[3] && ctx.p1.parsed.y < ftpZones[3] ? window.chartColors.orange :
ctx.p0.parsed.y < ftpZones[4] && ctx.p1.parsed.y < ftpZones[4] ? window.chartColors.darkorange :
ctx.p0.parsed.y < ftpZones[5] && ctx.p1.parsed.y < ftpZones[5] ? window.chartColors.orangered :
window.chartColors.red,
}
}, {
label: 'Req. Watts',
backgroundColor: window.chartColors.black,
borderColor: window.chartColors.black,
//cubicInterpolationMode: 'monotone',
data: reqpower,
fill: false,
pointRadius: 0,
borderWidth: 2,
},
]
},
options: {
responsive: true,
aspectRatio: div.width / div.height,
grid: {
zeroLineColor: 'rgba(0,255,0,1)'
},
plugins: {
/*
title:{
display:true,
backgroundColor: "#1d2330",
padding: {
top: 2,
bottom: 2
},
text:'Watt'
},*/
tooltips: {
mode: 'index',
intersect: false,
},
legend: {
display: false
},
annotation: {
annotations: {
box1: {
// Indicates the type of annotation
type: 'box',
xMin: 0,
//xMax: maxEl,
yMin: 0,
yMax: ftpZones[0],
backgroundColor: "#d6d6d620"
},
box2: {
// Indicates the type of annotation
type: 'box',
xMin: 0,
//xMax: maxEl,
yMin: ftpZones[0],
yMax: ftpZones[1],
backgroundColor: window.chartColors.limegreent,
},
box3: {
// Indicates the type of annotation
type: 'box',
xMin: 0,
//xMax: maxEl,
yMin: ftpZones[1],
yMax: ftpZones[2],
backgroundColor: window.chartColors.goldt,
},
box4: {
// Indicates the type of annotation
type: 'box',
xMin: 0,
//xMax: maxEl,
yMin: ftpZones[2],
yMax: ftpZones[3],
backgroundColor: window.chartColors.oranget,
},
box5: {
// Indicates the type of annotation
type: 'box',
xMin: 0,
//xMax: maxEl,
yMin: ftpZones[3],
yMax: ftpZones[4],
backgroundColor: window.chartColors.darkoranget,
},
box6: {
// Indicates the type of annotation
type: 'box',
xMin: 0,
//xMax: maxEl,
yMin: ftpZones[4],
yMax: ftpZones[5],
backgroundColor: window.chartColors.orangeredt,
},
box7: {
// Indicates the type of annotation
type: 'box',
xMin: 0,
//xMax: maxEl,
yMin: ftpZones[5],
yMax: (watts_max > ftpZones[3] * 2 ? watts_max + 10 : ftpZones[3] * 2),
backgroundColor: window.chartColors.redt,
},
}
}
},
hover: {
mode: 'nearest',
intersect: true
},
scales: {
x: {
type: 'linear',
display: true,
title: {
display: false,
text: 'Time'
},
ticks: {
// Include a dollar sign in the ticks
callback: function(value, index, values) {
return value !== 0 ? Math.floor(value / 3600).toString().padStart(2, "0") + ":" + Math.floor((value / 60) - (Math.floor(value / 3600) * 60)).toString().padStart(2, "0") : "";
},
padding: -20,
//stepSize: 300,
align: "end",
},
//max: maxEl,
},
y: {
display: true,
title: {
display: false,
text: 'Watt'
},
min: 0,
max: (watts_max > ftpZones[4] + 10 ? watts_max + 10 : ftpZones[4] + 10),
ticks: {
stepSize: 1,
autoSkip: false,
callback: value => [ftpZones[0] * 0.8, ftpZones[0], ftpZones[1], ftpZones[2], ftpZones[3], ftpZones[4], ftpZones[5]].includes(value) ?
value === ftpZones[0] * 0.8 ? 'zone 1' :
value === ftpZones[0] ? 'zone 2' :
value === ftpZones[1] ? 'zone 3' :
value === ftpZones[2] ? 'zone 4' :
value === ftpZones[3] ? 'zone 5' :
value === ftpZones[4] ? 'zone 6' :
value === ftpZones[5] ? 'zone 7' : undefined : undefined,
color: 'black',
padding: -50,
align: 'end',
z: 1,
}
}
}
}
};
powerChart = new Chart(ctx, config);
refresh();
}
function refresh() {
el = new MainWSQueueElement({
msg: null
}, function(msg) {
if (msg.msg === 'workout') {
return msg.content;
}
return null;
}, 2000, 1);
el.enqueue().then(process_workout).catch(function(err) {
console.error('Error is ' + err);
refresh();
});
}
function process_workout(arr) {
powerChart.data.datasets[0].data.push({x: arr.elapsed_s + (arr.elapsed_m * 60) + (arr.elapsed_h * 3600), y: arr.watts});
powerChart.update();
refresh();
}
function dochart_init() {
onSettingsOK = true;
keys_arr = ['ftp', 'miles_unit', 'age', 'heart_rate_zone1', 'heart_rate_zone2', 'heart_rate_zone3', 'heart_rate_zone4', 'heart_max_override_enable', 'heart_max_override_value']
let el = new MainWSQueueElement({
msg: 'getsettings',
content: {
keys: keys_arr
}
}, function(msg) {
if (msg.msg === 'R_getsettings') {
var heart_max_override_enable = false;
var heart_max_override_value = 195;
var heart_rate_zone1 = 0;
var heart_rate_zone2 = 0;
var heart_rate_zone3 = 0;
var heart_rate_zone4 = 0;
for (let key of keys_arr) {
if (msg.content[key] === undefined)
return null;
if (key === 'ftp') {
ftp = msg.content[key];
ftpZones[0] = Math.round(ftp * 0.55);
ftpZones[1] = Math.round(ftp * 0.75);
ftpZones[2] = Math.round(ftp * 0.90);
ftpZones[3] = Math.round(ftp * 1.05);
ftpZones[4] = Math.round(ftp * 1.20);
ftpZones[5] = Math.round(ftp * 1.50);
} else if (key === 'age') {
age = msg.content[key];
maxHeartRate = 220 - age;
} else if (key === 'heart_max_override_enable') {
heart_max_override_enable = msg.content[key];
} else if (key === 'heart_max_override_value') {
heart_max_override_value = msg.content[key];
} else if (key === 'heart_rate_zone1') {
heart_rate_zone1 = msg.content[key];
heartZones[0] = Math.round(maxHeartRate * (msg.content[key] / 100));
} else if (key === 'heart_rate_zone2') {
heart_rate_zone2 = msg.content[key];
heartZones[1] = Math.round(maxHeartRate * (msg.content[key] / 100));
} else if (key === 'heart_rate_zone3') {
heart_rate_zone3 = msg.content[key];
heartZones[2] = Math.round(maxHeartRate * (msg.content[key] / 100));
} else if (key === 'heart_rate_zone4') {
heart_rate_zone4 = msg.content[key];
heartZones[3] = Math.round(maxHeartRate * (msg.content[key] / 100));
} else if (key === 'miles_unit') {
if(msg.content[key] === true || msg.content[key] === 'true')
miles = 0.621371;
}
}
if(heart_max_override_enable) {
maxHeartRate = heart_max_override_value;
heartZones[0] = Math.round(maxHeartRate * (heart_rate_zone1 / 100));
heartZones[1] = Math.round(maxHeartRate * (heart_rate_zone2 / 100));
heartZones[2] = Math.round(maxHeartRate * (heart_rate_zone3 / 100));
heartZones[3] = Math.round(maxHeartRate * (heart_rate_zone4 / 100));
}
return msg.content;
}
return null;
}, 5000, 3);
el.enqueue().then(onSettingsOK).catch(function(err) {
console.error('Error is ' + err);
})
el = new MainWSQueueElement({
msg: 'getsessionarray'
}, function(msg) {
if (msg.msg === 'R_getsessionarray') {
return msg.content;
}
return null;
}, 15000, 3);
el.enqueue().then(process_arr).catch(function(err) {
console.error('Error is ' + err);
});
el = new MainWSQueueElement({
msg: 'gettrainingprogram'
}, function(msg) {
if (msg.msg === 'R_gettrainingprogram') {
return msg.content;
}
return null;
}, 15000, 3);
el.enqueue().then(process_trainprogram).catch(function(err) {
console.error('Error is ' + err);
});
}
$(window).on('load', function () {
dochart_init(); return;
// DEBUG
ftpZones[0] = Math.round(ftp * 0.55);
ftpZones[1] = Math.round(ftp * 0.75);
ftpZones[2] = Math.round(ftp * 0.90);
ftpZones[3] = Math.round(ftp * 1.05);
ftpZones[4] = Math.round(ftp * 1.20);
ftpZones[5] = Math.round(ftp * 1.50);
heartZones[0] = 110;
heartZones[1] = 130;
heartZones[2] = 150;
heartZones[3] = 170;
arr = [{'watts': 50, 'req_power': 150, 'elapsed_s':0,'elapsed_m':0,'elapsed_h':0, 'heart':90, 'resistance': 10, 'req_resistance': 15, 'cadence': 80, 'req_cadence': 90, 'speed': 10, 'inclination': 1, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 60, 'req_power': 150, 'elapsed_s':1,'elapsed_m':1,'elapsed_h':0, 'heart':92, 'resistance': 11, 'req_resistance': 30, 'cadence': 90, 'req_cadence': 100, 'speed': 8, 'inclination': 2, 'peloton_resistance': 20, 'peloton_req_resistance': 25},
{'watts': 70, 'req_power': 170, 'elapsed_s':2,'elapsed_m':2,'elapsed_h':0, 'heart':110, 'resistance': 12, 'req_resistance': 40, 'cadence': 100, 'req_cadence': 90, 'speed': 9, 'inclination': 2.5, 'peloton_resistance': 30, 'peloton_req_resistance': 35},
{'watts': 140, 'req_power': 170, 'elapsed_s':3,'elapsed_m':3,'elapsed_h':0, 'heart':115, 'resistance': 16, 'req_resistance': 41, 'cadence': 90, 'req_cadence': 95, 'speed': 11, 'inclination': 1, 'peloton_resistance': 40, 'peloton_req_resistance': 45},
{'watts': 130, 'req_power': 170, 'elapsed_s':4,'elapsed_m':4,'elapsed_h':0, 'heart':130, 'resistance': 18, 'req_resistance': 43, 'cadence': 95, 'req_cadence': 95, 'speed': 10, 'inclination': 4, 'peloton_resistance': 50, 'peloton_req_resistance': 55},
{'watts': 160, 'req_power': 170, 'elapsed_s':5,'elapsed_m':5,'elapsed_h':0, 'heart':135, 'resistance': 22, 'req_resistance': 43, 'cadence': 95, 'req_cadence': 95, 'speed': 12, 'inclination': 1, 'peloton_resistance': 60, 'peloton_req_resistance': 15},
{'watts': 180, 'req_power': 130, 'elapsed_s':6,'elapsed_m':6,'elapsed_h':0, 'heart':140, 'resistance': 31, 'req_resistance': 43, 'cadence': 95, 'req_cadence': 90, 'speed': 10, 'inclination': 3, 'peloton_resistance': 70, 'peloton_req_resistance': 15},
{'watts': 120, 'req_power': 130, 'elapsed_s':7,'elapsed_m':7,'elapsed_h':0, 'heart':150, 'resistance': 18, 'req_resistance': 35, 'cadence': 95, 'req_cadence': 80, 'speed': 10, 'inclination': 4, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 190, 'req_power': 150, 'elapsed_s':1,'elapsed_m':8,'elapsed_h':0, 'heart':155, 'resistance': 17, 'req_resistance': 35, 'cadence': 95, 'req_cadence': 80, 'speed': 13, 'inclination': 1, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 195, 'req_power': 170, 'elapsed_s':2,'elapsed_m':9,'elapsed_h':0, 'heart':165, 'resistance': 19, 'req_resistance': 30, 'cadence': 80, 'req_cadence': 80, 'speed': 12, 'inclination': 3, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 200, 'req_power': 170, 'elapsed_s':3,'elapsed_m':10,'elapsed_h':0, 'heart':153, 'resistance': 20, 'req_resistance': 25, 'cadence': 90, 'req_cadence': 90, 'speed': 10, 'inclination': 2, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 206, 'req_power': 170, 'elapsed_s':4,'elapsed_m':11,'elapsed_h':0, 'heart':152, 'resistance': 21, 'req_resistance': 35, 'cadence': 90, 'req_cadence': 90, 'speed': 12, 'inclination': 7, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 211, 'req_power': 170, 'elapsed_s':5,'elapsed_m':12,'elapsed_h':0, 'heart':180, 'resistance': 25, 'req_resistance': 35, 'cadence': 90, 'req_cadence': 70, 'speed': 10, 'inclination': 10, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 222, 'req_power': 130, 'elapsed_s':6,'elapsed_m':13,'elapsed_h':0, 'heart':182, 'resistance': 31, 'req_resistance': 35, 'cadence': 80, 'req_cadence': 70, 'speed': 7, 'inclination': 12, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 237, 'req_power': 130, 'elapsed_s':7,'elapsed_m':14,'elapsed_h':0, 'heart':160, 'resistance': 20, 'req_resistance': 50, 'cadence': 90, 'req_cadence': 70, 'speed': 6, 'inclination': 1, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 250, 'req_power': 170, 'elapsed_s':3,'elapsed_m':15,'elapsed_h':0, 'heart':115, 'resistance': 20, 'req_resistance': 50, 'cadence': 90, 'req_cadence': 90, 'speed': 10, 'inclination': 14, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 266, 'req_power': 170, 'elapsed_s':4,'elapsed_m':16,'elapsed_h':0, 'heart':120, 'resistance': 11, 'req_resistance': 35, 'cadence': 80, 'req_cadence': 60, 'speed': 10, 'inclination': 10, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 351, 'req_power': 170, 'elapsed_s':5,'elapsed_m':17,'elapsed_h':0, 'heart':112, 'resistance': 22, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 60, 'speed': 5, 'inclination': 9, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 322, 'req_power': 130, 'elapsed_s':6,'elapsed_m':18,'elapsed_h':0, 'heart':90, 'resistance': 25, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 96, 'speed': 10, 'inclination': 5, 'peloton_resistance': 10, 'peloton_req_resistance': 15},
{'watts': 257, 'req_power': 130, 'elapsed_s':7,'elapsed_m':19,'elapsed_h':0, 'heart':120, 'resistance': 10, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 97, 'speed': 10, 'inclination': 1, 'workoutName': '45min Power Zone Ride', 'workoutStartDate': '20/12/2021', 'instructorName': "Robin Arzon", 'watts_avg': 200, 'watts_max' : 351, 'heart_avg': 120, 'heart_max' : 150, 'jouls': 138000, 'calories': 950, 'distance': 11, 'cadence_avg': 65, 'peloton_resistance_avg': 22, 'deviceType': 1},
]
process_arr(arr);
});
$(document).ready(function () {
$('#loading').hide();
});

View File

@@ -849,7 +849,7 @@
else
$('.powerzone-value').html("<b>" + powerzone.toFixed(1) + "</b>");
$('.powerzone-avg').html(powerzone_lapavg.toFixed(1));
$('.spepowerzoneed-max').html(powerzone_lapmax.toFixed(1));
$('.powerzone-max').html(powerzone_lapmax.toFixed(1));
}
return null;
}, 15000, 3);

View File

@@ -62,8 +62,8 @@ var pedometer = CMPedometer()
} else {
sender = "PHONE"
}
Server.server?.send("SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#ODO=\(distance)#")
WatchKitConnection.distance = distance;
Server.server?.send(createString(sender: sender))
}
@objc public func setKcal(kcal: Double) -> Void
@@ -74,8 +74,48 @@ var pedometer = CMPedometer()
} else {
sender = "PHONE"
}
Server.server?.send("SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(kcal)#")
WatchKitConnection.kcal = kcal;
Server.server?.send(createString(sender: sender))
}
@objc public func setCadence(cadence: Double) -> Void
{
var sender: String
if UIDevice.current.userInterfaceIdiom == .pad {
sender = "PAD"
} else {
sender = "PHONE"
}
WatchKitConnection.cadence = cadence;
Server.server?.send(createString(sender: sender))
}
@objc public func setSpeed(speed: Double) -> Void
{
var sender: String
if UIDevice.current.userInterfaceIdiom == .pad {
sender = "PAD"
} else {
sender = "PHONE"
}
WatchKitConnection.speed = speed;
Server.server?.send(createString(sender: sender))
}
@objc public func setPower(power: Double) -> Void
{
var sender: String
if UIDevice.current.userInterfaceIdiom == .pad {
sender = "PAD"
} else {
sender = "PHONE"
}
WatchKitConnection.power = power;
Server.server?.send(createString(sender: sender))
}
func createString(sender: String) -> String {
return "SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(WatchKitConnection.kcal)#BCAD=\(WatchKitConnection.cadence)#SPD=\(WatchKitConnection.speed)#PWR=\(WatchKitConnection.power)#CAD=\(WatchKitConnection.stepCadence)#ODO=\(WatchKitConnection.distance)#";
}
@objc func updateHeartRate() {
@@ -85,8 +125,7 @@ var pedometer = CMPedometer()
} else {
sender = "PHONE"
}
Server.server?.send("SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#CAD=\(WatchKitConnection.stepCadence)#")
Server.server?.send(createString(sender: sender))
}
}
/*

View File

@@ -97,6 +97,18 @@ class Connection {
if sender?.contains("PAD") ?? false && message.contains("ODO=") {
let odo : String = message.slice(from: "ODO=", to: "#") ?? ""
WatchKitConnection.distance = (Double(odo) ?? 0)
}
if sender?.contains("PAD") ?? false && message.contains("BCAD=") {
let cad : String = message.slice(from: "BCAD=", to: "#") ?? ""
WatchKitConnection.cadence = (Double(cad) ?? 0)
}
if sender?.contains("PAD") ?? false && message.contains("SPD=") {
let spd : String = message.slice(from: "SPD=", to: "#") ?? ""
WatchKitConnection.speed = (Double(spd) ?? 0)
}
if sender?.contains("PAD") ?? false && message.contains("PWR=") {
let pwr : String = message.slice(from: "PWR=", to: "#") ?? ""
WatchKitConnection.power = (Double(pwr) ?? 0)
}
}
}

View File

@@ -25,6 +25,9 @@ class WatchKitConnection: NSObject {
static var distance = 0.0
static var stepCadence = 0
static var kcal = 0.0
static var speed = 0.0
static var power = 0.0
static var cadence = 0.0
private override init() {
super.init()
@@ -130,6 +133,9 @@ extension WatchKitConnection: WCSessionDelegate {
replyValues["distance"] = WatchKitConnection.distance
replyValues["kcal"] = WatchKitConnection.kcal
replyValues["cadence"] = WatchKitConnection.cadence
replyValues["power"] = WatchKitConnection.power
replyValues["speed"] = WatchKitConnection.speed
replyHandler(replyValues)

View File

@@ -9,6 +9,9 @@ class lockscreen {
long stepCadence();
void setKcal(double kcal);
void setDistance(double distance);
void setSpeed(double speed);
void setPower(double power);
void setCadence(double cadence);
// virtualbike
void virtualbike_ios();
@@ -53,6 +56,9 @@ class lockscreen {
void garminconnect_init();
int getHR();
int getFootCad();
// debug
static void debug(const char* debugstring);
};
#endif // LOCKSCREEN_H

View File

@@ -7,6 +7,7 @@
#import <ConnectIQ/ConnectIQ.h>
#import "qdomyoszwift-Swift2.h"
#include "ios/lockscreen.h"
#include <QDebug>
@class virtualbike_ios_swift;
@class virtualbike_zwift;
@@ -30,7 +31,9 @@ void lockscreen::request()
{
h = [[healthkit alloc] init];
[h request];
Garmin = [[GarminConnect alloc] init];
if (@available(iOS 13, *)) {
Garmin = [[GarminConnect alloc] init];
}
}
long lockscreen::heartRate()
@@ -53,6 +56,20 @@ void lockscreen::setDistance(double distance)
[h setDistanceWithDistance:distance * 0.621371];
}
void lockscreen::setPower(double power)
{
[h setPowerWithPower:power];
}
void lockscreen::setCadence(double cadence)
{
[h setCadenceWithCadence:cadence];
}
void lockscreen::setSpeed(double speed)
{
[h setSpeedWithSpeed:speed];
}
void lockscreen::virtualbike_ios()
{
_virtualbike = [[virtualbike_ios_swift alloc] init];
@@ -237,4 +254,8 @@ double lockscreen::getVolume()
[[AVAudioSession sharedInstance] setActive:true error:0];
return [[AVAudioSession sharedInstance] outputVolume];
}
void lockscreen::debug(const char* debugstring) {
qDebug() << debugstring;
}
#endif

View File

@@ -723,6 +723,9 @@ void m3ibike::processAdvertising(const QByteArray &data) {
long appleWatchHeartRate = h->heartRate();
h->setKcal(KCal.value());
h->setDistance(Distance.value());
h->setSpeed(Speed.value());
h->setPower(m_watt.value());
h->setCadence(Cadence.value());
if (appleWatchHeartRate == 0)
Heart = k3.pulse;
else

View File

@@ -548,7 +548,7 @@ ApplicationWindow {
id: toolButtonMaps
icon.source: ( "icons/icons/maps-icon-16.png" )
onClicked: { loadMaps(); }
anchors.right: toolButtonLockTiles.left
anchors.right: toolButtonChart.left
visible: rootItem.mapsVisible
}
@@ -569,6 +569,14 @@ ApplicationWindow {
visible: rootItem.videoIconVisible
}
ToolButton {
id: toolButtonChart
icon.source: ( "icons/icons/chart.png" )
onClicked: { rootItem.chartFooterVisible = !rootItem.chartFooterVisible }
anchors.right: toolButtonLockTiles.left
visible: rootItem.chartIconVisible
}
ToolButton {
id: toolButtonLockTiles
icon.source: ( window.lockTiles ? "icons/icons/unlock.png" : "icons/icons/lock.png")
@@ -737,7 +745,7 @@ ApplicationWindow {
}
ItemDelegate {
text: "version 2.13.98"
text: "version 2.16.15"
width: parent.width
}

View File

@@ -6,6 +6,7 @@
#include <QDateTime>
#include <QFile>
#include <QMetaEnum>
#include <QProcess>
#include <QSettings>
#include <QThread>
#include <chrono>
@@ -13,9 +14,100 @@
using namespace std::chrono_literals;
nordictrackifitadbbike::nordictrackifitadbbike(bool noWriteResistance, bool noHeartService) {
nordictrackifitadbbikeLogcatAdbThread::nordictrackifitadbbikeLogcatAdbThread(QString s) { Q_UNUSED(s) }
void nordictrackifitadbbikeLogcatAdbThread::run() {
QSettings settings;
bool nordictrack_ifit_adb_remote = settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote).toBool();
QString ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
runAdbCommand("connect " + ip);
while (1) {
runAdbTailCommand("logcat");
if(adbCommandPending.length() != 0) {
runAdbCommand(adbCommandPending);
adbCommandPending = "";
}
msleep(100);
}
}
QString nordictrackifitadbbikeLogcatAdbThread::runAdbCommand(QString command) {
#ifdef Q_OS_WINDOWS
QProcess process;
emit debug("adb >> " + command);
process.start("adb/adb.exe", QStringList(command.split(' ')));
process.waitForFinished(-1); // will wait forever until finished
QString out = process.readAllStandardOutput();
QString err = process.readAllStandardError();
emit debug("adb << OUT " + out);
emit debug("adb << ERR" + err);
#else
QString out;
#endif
return out;
}
bool nordictrackifitadbbikeLogcatAdbThread::runCommand(QString command) {
if(adbCommandPending.length() == 0) {
adbCommandPending = command;
return true;
}
return false;
}
void nordictrackifitadbbikeLogcatAdbThread::runAdbTailCommand(QString command) {
#ifdef Q_OS_WINDOWS
auto process = new QProcess;
QObject::connect(process, &QProcess::readyReadStandardOutput, [process, this]() {
QString output = process->readAllStandardOutput();
// qDebug() << "adbLogCat STDOUT << " << output;
QStringList lines = output.split('\n', Qt::SplitBehaviorFlags::SkipEmptyParts);
bool wattFound = false;
bool hrmFound = false;
foreach (QString line, lines) {
if (line.contains("Changed KPH")) {
emit debug(line);
speed = line.split(' ').last().toDouble();
} else if (line.contains("Changed Grade")) {
emit debug(line);
inclination = line.split(' ').last().toDouble();
} else if (line.contains("Changed Watts")) {
emit debug(line);
watt = line.split(' ').last().toDouble();
wattFound = true;
} else if (line.contains("HeartRateDataUpdate")) {
emit debug(line);
QStringList splitted = line.split(' ', Qt::SkipEmptyParts);
if (splitted.length() > 14) {
hrm = splitted[14].toInt();
hrmFound = true;
}
}
}
emit onSpeedInclination(speed, inclination);
if (wattFound)
emit onWatt(watt);
if (hrmFound)
emit onHRM(hrm);
});
QObject::connect(process, &QProcess::readyReadStandardError, [process, this]() {
auto output = process->readAllStandardError();
emit debug("adbLogCat ERROR << " + output);
});
emit debug("adbLogCat >> " + command);
process->start("adb/adb.exe", QStringList(command.split(' ')));
process->waitForFinished(-1);
#endif
}
nordictrackifitadbbike::nordictrackifitadbbike(bool noWriteResistance, bool noHeartService,
uint8_t bikeResistanceOffset, double bikeResistanceGain) {
QSettings settings;
bool nordictrack_ifit_adb_remote =
settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote)
.toBool();
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
@@ -36,31 +128,55 @@ nordictrackifitadbbike::nordictrackifitadbbike(bool noWriteResistance, bool noHe
if (!firstStateChanged && !this->hasVirtualDevice()) {
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
if (virtual_device_enabled) {
debug("creating virtual bike interface...");
auto virtualBike = new virtualbike(this);
connect(virtualBike, &virtualbike::changeInclination, this,
&nordictrackifitadbbike::changeInclinationRequested);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
firstStateChanged = 1;
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence) {
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
} else
#endif
#endif
if (virtual_device_enabled) {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike =
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
// connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
connect(virtualBike, &virtualbike::changeInclination, this, &nordictrackifitadbbike::changeInclination);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
firstStateChanged = 1;
// ********************************************************************************************************
if (nordictrack_ifit_adb_remote) {
#ifdef Q_OS_ANDROID
if(nordictrack_ifit_adb_remote) {
QAndroidJniObject IP = QAndroidJniObject::fromString(ip).object<jstring>();
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/QZAdbRemote", "createConnection",
"(Ljava/lang/String;Landroid/content/Context;)V", IP.object<jstring>(), QtAndroid::androidContext().object());
}
"(Ljava/lang/String;Landroid/content/Context;)V",
IP.object<jstring>(), QtAndroid::androidContext().object());
#elif defined Q_OS_WIN
logcatAdbThread = new nordictrackifitadbbikeLogcatAdbThread("logcatAdbThread");
/*connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::onSpeedInclination, this,
&nordictrackifitadbbike::onSpeedInclination);
connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::onWatt, this,
&nordictrackifitadbbike::onWatt);*/
connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::onHRM, this, &nordictrackifitadbbike::onHRM);
connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::debug, this, &nordictrackifitadbbike::debug);
logcatAdbThread->start();
#endif
}
}
bool nordictrackifitadbbike::inclinationAvailableByHardware() { return true; }
double nordictrackifitadbbike::getDouble(QString v) {
QChar d = QLocale().decimalPoint();
if(d == ',') {
if (d == ',') {
v = v.replace('.', ',');
}
return QLocale().toDouble(v);
@@ -117,8 +233,9 @@ void nordictrackifitadbbike::processPendingDatagrams() {
if (aValues.length()) {
resistance = getDouble(aValues.last());
m_pelotonResistance = (100 / 32) * resistance;
qDebug() << QStringLiteral("Current Peloton Resistance: ") << m_pelotonResistance.value() << resistance;
//Resistance = resistance;
qDebug() << QStringLiteral("Current Peloton Resistance: ") << m_pelotonResistance.value()
<< resistance;
// Resistance = resistance;
}
} else if (line.contains(QStringLiteral("Changed Watts"))) {
QStringList aValues = line.split(" ");
@@ -136,28 +253,36 @@ void nordictrackifitadbbike::processPendingDatagrams() {
}
// since the motor of the bike is slow, let's filter the inclination changes to more than 4 seconds
if(lastInclinationChanged.secsTo(QDateTime::currentDateTime()) > 4) {
if (lastInclinationChanged.secsTo(QDateTime::currentDateTime()) > 4) {
lastInclinationChanged = QDateTime::currentDateTime();
#ifdef Q_OS_ANDROID
bool nordictrack_ifit_adb_remote = settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote).toBool();
if(nordictrack_ifit_adb_remote) {
if(requestInclination != -100) {
bool nordictrack_ifit_adb_remote =
settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote)
.toBool();
if (nordictrack_ifit_adb_remote) {
if (requestInclination != -100) {
double inc = qRound(requestInclination / 0.5) * 0.5;
if(inc != currentInclination().value()) {
if (inc != currentInclination().value()) {
int x1 = 75;
int y2 = (int) (616.18 - (17.223 * (inc + gears())));
int y1Resistance = (int) (616.18 - (17.223 * currentInclination().value()));
int y2 = (int)(616.18 - (17.223 * (inc + gears())));
int y1Resistance = (int)(616.18 - (17.223 * currentInclination().value()));
lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " + QString::number(x1) + " " + QString::number(y2) + " 200";
lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " +
QString::number(x1) + " " + QString::number(y2) + " 200";
qDebug() << " >> " + lastCommand;
#ifdef Q_OS_ANDROID
QAndroidJniObject command = QAndroidJniObject::fromString(lastCommand).object<jstring>();
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/QZAdbRemote", "sendCommand",
"(Ljava/lang/String;)V", command.object<jstring>());
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/QZAdbRemote",
"sendCommand", "(Ljava/lang/String;)V",
command.object<jstring>());
#elif defined(Q_OS_WIN)
if (logcatAdbThread)
logcatAdbThread->runCommand("shell " + lastCommand);
#endif
}
}
requestInclination = -100;
}
#endif
QByteArray message = (QString::number(requestInclination).toLocal8Bit()) + ";";
requestInclination = -100;
@@ -175,6 +300,11 @@ void nordictrackifitadbbike::processPendingDatagrams() {
Distance += ((Speed.value() / 3600000.0) *
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
if (Cadence.value() > 0) {
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
#ifdef Q_OS_ANDROID
@@ -188,8 +318,21 @@ void nordictrackifitadbbike::processPendingDatagrams() {
}
}
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadencep =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadencep && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
#endif
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
emit debug(QStringLiteral("Current Gear: ") + QString::number(gear));
emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value()));
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
@@ -199,6 +342,24 @@ void nordictrackifitadbbike::processPendingDatagrams() {
}
}
void nordictrackifitadbbike::onHRM(int hrm) {
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
bool disable_hr_frommachinery =
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
if (
#ifdef Q_OS_ANDROID
(!settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) &&
#endif
heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery) {
Heart = hrm;
emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value()));
}
}
resistance_t nordictrackifitadbbike::pelotonToBikeResistance(int pelotonResistance) {
if (pelotonResistance <= 10) {
return 1;
@@ -287,4 +448,3 @@ void nordictrackifitadbbike::changeInclinationRequested(double grade, double per
}
bool nordictrackifitadbbike::connected() { return true; }

View File

@@ -16,6 +16,7 @@
#include <QDateTime>
#include <QObject>
#include <QString>
#include <QThread>
#include <QUdpSocket>
#include "bike.h"
@@ -25,10 +26,42 @@
#include "ios/lockscreen.h"
#endif
class nordictrackifitadbbikeLogcatAdbThread : public QThread {
Q_OBJECT
public:
explicit nordictrackifitadbbikeLogcatAdbThread(QString s);
bool runCommand(QString command);
void run() override;
signals:
void onSpeedInclination(double speed, double inclination);
void debug(QString message);
void onWatt(double watt);
void onHRM(int hrm);
private:
QString adbCommandPending = "";
QString runAdbCommand(QString command);
double speed = 0;
double inclination = 0;
double watt = 0;
int hrm = 0;
QString name;
struct adbfile {
QDateTime date;
QString name;
};
void runAdbTailCommand(QString command);
};
class nordictrackifitadbbike : public bike {
Q_OBJECT
public:
nordictrackifitadbbike(bool noWriteResistance, bool noHeartService);
nordictrackifitadbbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset,
double bikeResistanceGain);
bool connected() override;
resistance_t pelotonToBikeResistance(int pelotonResistance) override;
bool inclinationAvailableByHardware() override;
@@ -55,9 +88,9 @@ class nordictrackifitadbbike : public bike {
QUdpSocket *socket = nullptr;
QHostAddress lastSender;
#ifdef Q_OS_ANDROID
nordictrackifitadbbikeLogcatAdbThread *logcatAdbThread = nullptr;
QString lastCommand;
#endif
QString ip;
@@ -73,6 +106,7 @@ class nordictrackifitadbbike : public bike {
void processPendingDatagrams();
void changeInclinationRequested(double grade, double percentage);
void onHRM(int hrm);
void update();
};

View File

@@ -367,7 +367,8 @@ void peloton::ride_onfinish(QNetworkReply *reply) {
bool atLeastOnePower = false;
if (trainrows.empty() && !segments_segment_list.isEmpty() &&
bluetoothManager->device()->deviceType() != bluetoothdevice::ROWING) {
bluetoothManager->device()->deviceType() != bluetoothdevice::ROWING &&
bluetoothManager->device()->deviceType() != bluetoothdevice::TREADMILL) {
foreach (QJsonValue o, segments_segment_list) {
QJsonArray subsegments_v2 = o["subsegments_v2"].toArray();
if (!subsegments_v2.isEmpty()) {
@@ -432,6 +433,14 @@ void peloton::ride_onfinish(QNetworkReply *reply) {
trainrows.append(row);
atLeastOnePower = true;
}
} else if (!zone.toUpper().compare(QStringLiteral("RECOVERY"))) {
r.duration = QTime(0, len / 60, len % 60, 0);
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.45;
if (r.power != -1) {
atLeastOnePower = true;
}
trainrows.append(r);
qDebug() << r.duration << "power" << r.power;
} else if (!zone.toUpper().compare(QStringLiteral("FLAT ROAD"))) {
r.duration = QTime(0, len / 60, len % 60, 0);
r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.50;
@@ -512,6 +521,16 @@ void peloton::ride_onfinish(QNetworkReply *reply) {
}
trainrows.append(r);
qDebug() << r.duration << "power" << r.power;
} else {
if(len > 0 && atLeastOnePower) {
r.duration = QTime(0, len / 60, len % 60, 0);
r.power = -1;
if (r.power != -1) {
atLeastOnePower = true;
}
qDebug() << "ERROR not handled!" << zone;
trainrows.append(r);
}
}
}
}
@@ -524,36 +543,50 @@ void peloton::ride_onfinish(QNetworkReply *reply) {
QJsonObject target_metrics_data_list = ride[QStringLiteral("target_metrics_data")].toObject();
QJsonArray pace_intensities_list = target_metrics_data_list[QStringLiteral("pace_intensities")].toArray();
int pace_count = 0;
int pace_count = 0;
rower_pace_offset = 0;
foreach (QJsonValue o, pace_intensities_list) {
if(o["value"].toInt() < 0) {
if(abs(o["value"].toInt()) > rower_pace_offset)
rower_pace_offset = abs(o["value"].toInt());
}
}
qDebug() << "rower_pace_offset" << rower_pace_offset;
foreach (QJsonValue o, pace_intensities_list) {
qDebug() << o;
pace_count = o["value"].toInt();
if (pace_count < 4 && pace_count >= 0) {
pace_count = o["value"].toInt() + rower_pace_offset;
if (pace_count < 5 && pace_count >= 0) {
rower_pace[pace_count].display_name = o["display_name"].toString();
rower_pace[pace_count].value = o["value"].toInt();
QJsonArray levels = o["pace_levels"].toArray();
if (levels.count() > 6) {
if (levels.count() > 10) {
qDebug() << "peloton pace levels had been changed!";
}
int count = 0;
foreach (QJsonValue level, levels) {
count = level["slug"].toString().right(1).toInt() - 1;
if (count >= 0 && count < 6) {
rower_pace[pace_count].levels[count].fast_pace = level["fast_pace"].toDouble();
rower_pace[pace_count].levels[count].slow_pace = level["slow_pace"].toDouble();
rower_pace[pace_count].levels[count].display_name = level["display_name"].toString();
rower_pace[pace_count].levels[count].slug = level["slug"].toString();
qDebug() << count << rower_pace[pace_count].levels[count].display_name
<< rower_pace[pace_count].levels[count].fast_pace
<< rower_pace[pace_count].levels[count].slow_pace
<< rower_pace[pace_count].levels[count].slug;
if(level["slug"].toString().split("_").count() > 1 ) {
count = level["slug"].toString().split("_")[1].toInt() - 1;
if (count >= 0 && count < 11) {
rower_pace[pace_count].levels[count].fast_pace = level["fast_pace"].toDouble();
rower_pace[pace_count].levels[count].slow_pace = level["slow_pace"].toDouble();
rower_pace[pace_count].levels[count].display_name = level["display_name"].toString();
rower_pace[pace_count].levels[count].slug = level["slug"].toString();
qDebug() << count << level << rower_pace[pace_count].levels[count].display_name
<< rower_pace[pace_count].levels[count].fast_pace
<< rower_pace[pace_count].levels[count].slow_pace
<< rower_pace[pace_count].levels[count].slug;
} else {
qDebug() << level["slug"].toString() << "slug error";
}
} else {
qDebug() << level["slug"].toString() << "slug error";
qDebug() << level["slug"].toString() << "slug count error";
}
}
qDebug() << pace_count << rower_pace[pace_count].display_name << rower_pace[pace_count].value;
} else {
qDebug() << "pace_count error!";
@@ -694,8 +727,8 @@ void peloton::performance_onfinish(QNetworkReply *reply) {
1;
double strokes_rate_lower = strokes_rate[QStringLiteral("lower")].toDouble();
double strokes_rate_upper = strokes_rate[QStringLiteral("upper")].toDouble();
int pace_intensity_lower = pace_intensity[QStringLiteral("lower")].toInt();
int pace_intensity_upper = pace_intensity[QStringLiteral("upper")].toInt();
int pace_intensity_lower = pace_intensity[QStringLiteral("lower")].toInt() + rower_pace_offset;
int pace_intensity_upper = pace_intensity[QStringLiteral("upper")].toInt() + rower_pace_offset;
int offset_start = offset[QStringLiteral("start")].toInt();
int offset_end = offset[QStringLiteral("end")].toInt();
double strokes_rate_average = ((strokes_rate_upper - strokes_rate_lower) / 2.0) + strokes_rate_lower;
@@ -710,7 +743,7 @@ void peloton::performance_onfinish(QNetworkReply *reply) {
r.cadence = ((strokes_rate_upper - strokes_rate_lower) / 2.0) + strokes_rate_lower;
}
if (pace_intensity_lower >= 0 && pace_intensity_lower < 4) {
if (pace_intensity_lower >= 0 && pace_intensity_lower < 5) {
r.average_speed =
(rowerpaceToSpeed(rower_pace[pace_intensity_lower].levels[peloton_rower_level].fast_pace) +
rowerpaceToSpeed(rower_pace[pace_intensity_lower].levels[peloton_rower_level].slow_pace)) /

View File

@@ -94,10 +94,11 @@ class peloton : public QObject {
typedef struct _peloton_rower_pace_intensities {
QString display_name;
int value;
_peloton_rower_pace_intensities_level levels[6];
_peloton_rower_pace_intensities_level levels[10];
} _peloton_rower_pace_intensities;
_peloton_rower_pace_intensities rower_pace[4];
_peloton_rower_pace_intensities rower_pace[5];
int rower_pace_offset = 0;
private slots:
void login_onfinish(QNetworkReply *reply);

View File

@@ -1042,6 +1042,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
m_pelotonResistance = 30;
break;
case 0x0b:
case 0x0c:
Resistance = 5;
m_pelotonResistance = 35;
break;
@@ -1050,6 +1051,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
m_pelotonResistance = 40;
break;
case 0x10:
case 0x11:
Resistance = 7;
m_pelotonResistance = 45;
break;
@@ -1078,6 +1080,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
m_pelotonResistance = 75;
break;
case 0x21:
case 0x22:
Resistance = 14;
m_pelotonResistance = 80;
break;
@@ -1086,6 +1089,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
m_pelotonResistance = 85;
break;
case 0x26:
case 0x27:
Resistance = 16;
m_pelotonResistance = 100;
break;

View File

@@ -214,9 +214,10 @@ void proformelliptical::characteristicChanged(const QLowEnergyCharacteristic &ch
if (newValue.length() == 20 && newValue.at(0) == 0x01 && newValue.at(1) == 0x12 &&
((newValue.at(2) == 0x46) || (newValue.at(2) == 0x5a))) {
double speed_from_machinery =
(double)(((uint16_t)((uint8_t)newValue.at(15)) << 8) + (uint16_t)((uint8_t)newValue.at(14))) / 100.0;
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed =
(double)(((uint16_t)((uint8_t)newValue.at(15)) << 8) + (uint16_t)((uint8_t)newValue.at(14))) / 100.0;
Speed = speed_from_machinery;
} else {
Speed = speedFromWatts();
}
@@ -235,6 +236,7 @@ void proformelliptical::characteristicChanged(const QLowEnergyCharacteristic &ch
Resistance = GetResistanceFromPacket(newValue);
Cadence = (newValue.at(18) * cadence_gain) + cadence_offset;
m_watt = (double)(((uint16_t)((uint8_t)newValue.at(13)) << 8) + (uint16_t)((uint8_t)newValue.at(12)));
if (watts())
KCal += ((((0.048 * ((double)watts()) + 1.19) * weight * 3.5) / 200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
@@ -518,3 +520,5 @@ void proformelliptical::controllerStateChanged(QLowEnergyController::ControllerS
m_control->connectToDevice();
}
}
uint16_t proformelliptical::watts() { return m_watt.value(); }

View File

@@ -49,6 +49,7 @@ class proformelliptical : public elliptical {
void sendPoll();
void forceIncline(double incline);
void forceSpeed(double speed);
uint16_t watts() override;
QTimer *refresh;
uint8_t counterPoll = 0;

View File

@@ -54,128 +54,262 @@ void proformrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QS
}
void proformrower::forceResistance(resistance_t requestResistance) {
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02,
0x00, 0x10, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02,
0x00, 0x10, 0x03, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02,
0x00, 0x10, 0x04, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x58, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xf9, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x9a, 0x09, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x3a, 0x0b, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xdb, 0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x7c, 0x0e, 0x00, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x1c, 0x10, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xbd, 0x11, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x5e, 0x13, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xfe, 0x14, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x9f, 0x16, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x40, 0x18, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xe0, 0x19, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x81, 0x1b, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x22, 0x1d, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xc2, 0x1e, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x63, 0x20, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x04, 0x22, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xa4, 0x23, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x45, 0x25, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xe6, 0x26, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00};
QSettings settings;
bool proform_rower_sport_rl =
settings.value(QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl).toBool();
switch (requestResistance) {
case 1:
writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true);
break;
case 2:
writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true);
break;
case 3:
writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true);
break;
case 4:
writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true);
break;
case 5:
writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true);
break;
case 6:
writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true);
break;
case 7:
writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true);
break;
case 8:
writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true);
break;
case 9:
writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true);
break;
case 10:
writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true);
break;
case 11:
writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true);
break;
case 12:
writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true);
break;
case 13:
writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true);
break;
case 14:
writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true);
break;
case 15:
writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true);
break;
case 16:
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
break;
case 17:
writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true);
break;
case 18:
writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true);
break;
case 19:
writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true);
break;
case 20:
writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true);
break;
case 21:
writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true);
break;
case 22:
writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true);
break;
case 23:
writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true);
break;
case 24:
writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true);
break;
if (proform_rower_sport_rl) {
const uint8_t unlock_res[] = {0xfe, 0x02, 0x0d, 0x02};
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x76, 0x01, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x17, 0x03, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xb8, 0x04, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x58, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xf9, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x9a, 0x09, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x3a, 0x0b, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xdb, 0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x7c, 0x0e, 0x00, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x1c, 0x10, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xbd, 0x11, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x5e, 0x13, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xfe, 0x14, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x9f, 0x16, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x40, 0x18, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xe0, 0x19, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x81, 0x1b, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x22, 0x1d, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xc2, 0x1e, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x63, 0x20, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x04, 0x22, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xa4, 0x23, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x45, 0x25, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xe6, 0x26, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00};
writeCharacteristic((uint8_t*)unlock_res, sizeof(unlock_res), QStringLiteral("unlock_resistance"), false, false);
switch (requestResistance) {
case 1:
writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true);
break;
case 2:
writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true);
break;
case 3:
writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true);
break;
case 4:
writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true);
break;
case 5:
writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true);
break;
case 6:
writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true);
break;
case 7:
writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true);
break;
case 8:
writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true);
break;
case 9:
writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true);
break;
case 10:
writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true);
break;
case 11:
writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true);
break;
case 12:
writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true);
break;
case 13:
writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true);
break;
case 14:
writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true);
break;
case 15:
writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true);
break;
case 16:
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
break;
case 17:
writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true);
break;
case 18:
writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true);
break;
case 19:
writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true);
break;
case 20:
writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true);
break;
case 21:
writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true);
break;
case 22:
writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true);
break;
case 23:
writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true);
break;
case 24:
writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true);
break;
}
} else {
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02,
0x00, 0x10, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02,
0x00, 0x10, 0x03, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02,
0x00, 0x10, 0x04, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x58, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xf9, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x9a, 0x09, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x3a, 0x0b, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xdb, 0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x7c, 0x0e, 0x00, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x1c, 0x10, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xbd, 0x11, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x5e, 0x13, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xfe, 0x14, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x9f, 0x16, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x40, 0x18, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xe0, 0x19, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x81, 0x1b, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x22, 0x1d, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xc2, 0x1e, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x63, 0x20, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x04, 0x22, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xa4, 0x23, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0x45, 0x25, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01,
0x04, 0xe6, 0x26, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00};
switch (requestResistance) {
case 1:
writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true);
break;
case 2:
writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true);
break;
case 3:
writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true);
break;
case 4:
writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true);
break;
case 5:
writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true);
break;
case 6:
writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true);
break;
case 7:
writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true);
break;
case 8:
writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true);
break;
case 9:
writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true);
break;
case 10:
writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true);
break;
case 11:
writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true);
break;
case 12:
writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true);
break;
case 13:
writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true);
break;
case 14:
writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true);
break;
case 15:
writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true);
break;
case 16:
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
break;
case 17:
writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true);
break;
case 18:
writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true);
break;
case 19:
writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true);
break;
case 20:
writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true);
break;
case 21:
writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true);
break;
case 22:
writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true);
break;
case 23:
writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true);
break;
case 24:
writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true);
break;
}
}
}
@@ -195,6 +329,9 @@ void proformrower::update() {
update_metrics(true, watts());
{
bool proform_rower_sport_rl =
settings.value(QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl).toBool();
uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03};
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x14, 0x13, 0x02, 0x00,
0x0d, 0x1c, 0x9e, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80};
@@ -217,8 +354,16 @@ void proformrower::update() {
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"));
break;
case 3:
if (requestResistance != -1 && proform_rower_sport_rl) {
if (requestResistance != currentResistance().value() && requestResistance >= 0 &&
requestResistance <= max_resistance) {
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
forceResistance(requestResistance);
}
requestResistance = -1;
}
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"), true);
if (requestResistance != -1) {
if (requestResistance != -1 && !proform_rower_sport_rl) {
if (requestResistance != currentResistance().value() && requestResistance >= 0 &&
requestResistance <= max_resistance) {
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
@@ -284,12 +429,14 @@ double proformrower::GetResistanceFromPacket(QByteArray packet) {
case 6:
return 4;
case 7:
case 8:
return 5;
case 9:
return 6;
case 0x0b:
return 7;
case 0x0c:
case 0x0d:
return 8;
case 0x0e:
return 9;
@@ -300,12 +447,14 @@ double proformrower::GetResistanceFromPacket(QByteArray packet) {
case 0x13:
return 12;
case 0x14:
case 0x15:
return 13;
case 0x16:
return 14;
case 0x18:
return 15;
case 0x19:
case 0x1a:
return 16;
case 0x1b:
return 17;
@@ -322,6 +471,7 @@ double proformrower::GetResistanceFromPacket(QByteArray packet) {
case 0x25:
return 23;
case 0x26:
case 0x27:
return 24;
}
return 1;
@@ -398,6 +548,9 @@ void proformrower::characteristicChanged(const QLowEnergyCharacteristic &charact
}
void proformrower::btinit() {
QSettings settings;
bool proform_rower_sport_rl =
settings.value(QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl).toBool();
{
uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02};
@@ -415,6 +568,7 @@ void proformrower::btinit() {
uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04};
uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x14, 0x28, 0x90, 0x07,
0x01, 0xed, 0xe8, 0xe9, 0xe8, 0xf5, 0xf0, 0x09, 0x00, 0x1d};
uint8_t initData11[] = {0x01, 0x12, 0x28, 0x39, 0x48, 0x55, 0x60, 0x99, 0xb0, 0xad,
@@ -438,6 +592,13 @@ void proformrower::btinit() {
uint8_t noOpData9[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x13, 0x13, 0x02, 0x0c,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData10_proform_rower_sport_rl[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x14, 0x28, 0x90, 0x07, 0x01, 0xf2, 0xf4, 0xf4, 0xf2, 0xfe, 0x08, 0x00, 0x1e, 0x2a};
uint8_t initData11_proform_rower_sport_rl[] = {0x01, 0x12, 0x3c, 0x4c, 0x5a, 0x66, 0x90, 0x88, 0xa6, 0xc2, 0xe4, 0x04, 0x22, 0x4e, 0x98, 0xb0, 0xce, 0x1a, 0x2c, 0x7c};
uint8_t initData12_proform_rower_sport_rl[] = {0xff, 0x08, 0x8a, 0xd6, 0x20, 0x98, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t noOpData7_proform_rower_sport_rl[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x14, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t noOpData8_proform_rower_sport_rl[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false);
@@ -464,26 +625,53 @@ void proformrower::btinit() {
QThread::msleep(400);
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
if (proform_rower_sport_rl) {
writeCharacteristic(initData10_proform_rower_sport_rl, sizeof(initData10_proform_rower_sport_rl), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData11_proform_rower_sport_rl, sizeof(initData11_proform_rower_sport_rl), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData12_proform_rower_sport_rl, sizeof(initData12_proform_rower_sport_rl), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData7_proform_rower_sport_rl, sizeof(noOpData7_proform_rower_sport_rl), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData8_proform_rower_sport_rl, sizeof(noOpData8_proform_rower_sport_rl), QStringLiteral("init"), false, true);
QThread::msleep(400);
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false);
QThread::msleep(400);
} else {
writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
}
/*writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData8, sizeof(noOpData8), QStringLiteral("init"), false, false);

View File

@@ -72,6 +72,8 @@ void proformtreadmill::forceIncline(double incline) {
settings.value(QZSettings::proform_treadmill_se, QZSettings::default_proform_treadmill_se).toBool();
bool norditrack_s25i_treadmill =
settings.value(QZSettings::norditrack_s25i_treadmill, QZSettings::default_norditrack_s25i_treadmill).toBool();
bool proform_treadmill_z1300i =
settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool();
if (proform_treadmill_1800i) {
uint8_t i = abs(incline * 10);
@@ -92,7 +94,7 @@ void proformtreadmill::forceIncline(double incline) {
if (norditrack_s25i_treadmill) {
write[14] = write[11] + write[12] + 0x11;
} else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se) {
} else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_z1300i) {
write[14] = write[11] + write[12] + 0x12;
} else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_t65s_83_treadmill) {
for (uint8_t i = 0; i < 7; i++) {
@@ -127,6 +129,8 @@ void proformtreadmill::forceSpeed(double speed) {
.toBool();
bool norditrack_s25i_treadmill =
settings.value(QZSettings::norditrack_s25i_treadmill, QZSettings::default_norditrack_s25i_treadmill).toBool();
bool proform_treadmill_z1300i =
settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool();
uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02};
uint8_t write[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x04, 0x09, 0x02, 0x01,
@@ -137,7 +141,8 @@ void proformtreadmill::forceSpeed(double speed) {
if (norditrack_s25i_treadmill) {
write[14] = write[11] + write[12] + 0x11;
} else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_cadence_lt) {
} else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_cadence_lt ||
proform_treadmill_z1300i) {
write[14] = write[11] + write[12] + 0x11;
} else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_t65s_83_treadmill) {
for (uint8_t i = 0; i < 7; i++) {
@@ -198,6 +203,8 @@ void proformtreadmill::update() {
settings
.value(QZSettings::nordictrack_incline_trainer_x7i, QZSettings::default_nordictrack_incline_trainer_x7i)
.toBool();
bool proform_treadmill_z1300i =
settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool();
// bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i,
// QZSettings::default_proform_treadmill_995i).toBool();
@@ -240,7 +247,7 @@ void proformtreadmill::update() {
counterPoll = 0;
}
} else*/
if (proform_treadmill_9_0) {
if (proform_treadmill_9_0 || proform_treadmill_z1300i) {
uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03};
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00,
0x0d, 0x92, 0x1a, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
@@ -265,14 +272,15 @@ void proformtreadmill::update() {
if (requestInclination < 0)
requestInclination = 0;
if (requestInclination != currentInclination().value() && requestInclination >= -3 &&
requestInclination <= 15) {
requestInclination <= (proform_treadmill_z1300i ? 12 : 15)) {
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
forceIncline(requestInclination);
}
requestInclination = -100;
}
if (requestSpeed != -1) {
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 &&
requestSpeed <= (proform_treadmill_z1300i ? 19.3 : 22)) {
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
forceSpeed(requestSpeed);
}
@@ -1134,6 +1142,8 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
bool nordictrack_incline_trainer_x7i =
settings.value(QZSettings::nordictrack_incline_trainer_x7i, QZSettings::default_nordictrack_incline_trainer_x7i)
.toBool();
bool proform_treadmill_z1300i =
settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool();
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
@@ -1143,8 +1153,8 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
if (newValue.length() != 20 || newValue.at(0) != 0x00 || newValue.at(1) != 0x12 || newValue.at(2) != 0x01 ||
newValue.at(3) != 0x04 ||
((nordictrack10 || nordictrackt70 || proform_treadmill_1800i || proform_treadmill_8_0 ||
proform_treadmill_9_0 || nordictrack_incline_trainer_x7i) &&
((nordictrack10 || nordictrackt70 || proform_treadmill_1800i || proform_treadmill_z1300i ||
proform_treadmill_8_0 || proform_treadmill_9_0 || nordictrack_incline_trainer_x7i) &&
(newValue.at(4) != 0x02 || (newValue.at(5) != 0x31 && newValue.at(5) != 0x34))) ||
((norditrack_s25i_treadmill) && (newValue.at(4) != 0x02 || (newValue.at(5) != 0x2f))) ||
((nordictrack_t65s_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill ||
@@ -1235,6 +1245,8 @@ void proformtreadmill::btinit() {
bool nordictrack_incline_trainer_x7i =
settings.value(QZSettings::nordictrack_incline_trainer_x7i, QZSettings::default_nordictrack_incline_trainer_x7i)
.toBool();
bool proform_treadmill_z1300i =
settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool();
// bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i,
// QZSettings::default_proform_treadmill_995i).toBool();
@@ -1348,6 +1360,83 @@ void proformtreadmill::btinit() {
uint8_t noOpData6[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3a, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false);
QThread::msleep(400);
} else if (proform_treadmill_z1300i) {
uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02};
uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x80, 0x88,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x88, 0x90,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData5[] = {0xfe, 0x02, 0x0a, 0x02};
uint8_t initData6[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x82, 0x00,
0x00, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData7[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00,
0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04};
uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28, 0x90, 0x04,
0x00, 0xb0, 0x4c, 0xda, 0x7e, 0x14, 0xb8, 0x46, 0xfa, 0x98};
uint8_t initData11[] = {0x01, 0x12, 0x34, 0xd2, 0x86, 0x3c, 0xf0, 0x9e, 0x52, 0xe0,
0xbc, 0x4a, 0x0e, 0xc4, 0x88, 0x56, 0x0a, 0xc8, 0x84, 0x42};
uint8_t initData12[] = {0xff, 0x08, 0x36, 0xec, 0xe0, 0x80, 0x02, 0x00, 0x00, 0x12,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03};
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00,
0x0d, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x48, 0x00, 0x00, 0xe0};
uint8_t noOpData3[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x62, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t noOpData4[] = {0xfe, 0x02, 0x17, 0x03};
uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x0c,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t noOpData6[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
QThread::msleep(400);
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false);

View File

@@ -458,17 +458,20 @@ void proformwifibike::characteristicChanged(const QString &newValue) {
}
}
if (!values[QStringLiteral("Current Watts")].isUndefined()) {
double watt = values[QStringLiteral("Current Watts")].toString().toDouble();
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled")))
// some buggy TDF1 bikes send spurious wattage at the end with cadence = 0
if (Cadence.value() > 0) {
if (!values[QStringLiteral("Current Watts")].isUndefined()) {
double watt = values[QStringLiteral("Current Watts")].toString().toDouble();
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled")))
m_watt = watt;
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
} else if (!values[QStringLiteral("Watt attuali")].isUndefined()) {
double watt = values[QStringLiteral("Watt attuali")].toString().toDouble();
m_watt = watt;
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
} else if (!values[QStringLiteral("Watt attuali")].isUndefined()) {
double watt = values[QStringLiteral("Watt attuali")].toString().toDouble();
m_watt = watt;
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
}
}
if (!values[QStringLiteral("Actual Incline")].isUndefined()) {

View File

@@ -720,6 +720,8 @@ DISTFILES += \
$$PWD/android/src/MediaProjection.java \
$$PWD/android/src/NotificationUtils.java \
$$PWD/android/src/ScreenCaptureService.java \
$$PWD/android/src/WearableController.java \
$$PWD/android/src/WearableMessageListenerService.java \
.clang-format \
AppxManifest.xml \
android/AndroidManifest.xml \
@@ -743,6 +745,7 @@ DISTFILES += \
android/src/MyActivity.java \
android/src/PowerChannelController.java \
android/src/SpeedChannelController.java \
android/src/SDMChannelController.java \
android/src/Usbserial.java \
android/src/com/cgutman/adblib/AdbBase64.java \
android/src/com/cgutman/adblib/AdbConnection.java \
@@ -812,4 +815,4 @@ INCLUDEPATH += purchasing/inapp
WINRT_MANIFEST = AppxManifest.xml
VERSION = 2.13.98
VERSION = 2.16.15

View File

@@ -102,5 +102,10 @@
<file>inner_templates/chartjs/bike.png</file>
<file>inner_templates/chartjs/html2canvas.min.js</file>
<file>inner_templates/chartjs/qzlogo.png</file>
<file>inner_templates/chartjs/dochartlive.js</file>
<file>inner_templates/chartjs/chartlive.htm</file>
<file>ChartFooter.qml</file>
<file>ChartFooterInnerJS.qml</file>
<file>ChartFooterInnerNoJS.qml</file>
</qresource>
</RCC>

View File

@@ -664,8 +664,19 @@ const QString QZSettings::fakedevice_rower = QStringLiteral("fakedevice_rower");
const QString QZSettings::zwift_ocr_climb_portal = QStringLiteral("zwift_ocr_climb_portal");
const QString QZSettings::poll_device_time = QStringLiteral("poll_device_time");
const QString QZSettings::proform_bike_PFEVEX71316_1 = QStringLiteral("proform_bike_PFEVEX71316_1");
const QString QZSettings::schwinn_bike_resistance_v3 = QStringLiteral("schwinn_bike_resistance_v3");
const QString QZSettings::watt_ignore_builtin = QStringLiteral("watt_ignore_builtin");
const QString QZSettings::proform_treadmill_z1300i = QStringLiteral("proform_treadmill_z1300i");
const QString QZSettings::ftms_bike = QStringLiteral("ftms_bike");
const QString QZSettings::default_ftms_bike = QStringLiteral("Disabled");
const QString QZSettings::ftms_treadmill = QStringLiteral("ftms_treadmill");
const QString QZSettings::default_ftms_treadmill = QStringLiteral("Disabled");
const QString QZSettings::ant_speed_offset = QStringLiteral("ant_speed_offset");
const QString QZSettings::ant_speed_gain = QStringLiteral("ant_speed_gain");
const QString QZSettings::proform_rower_sport_rl = QStringLiteral("proform_rower_sport_rl");
const QString QZSettings::strava_date_prefix = QStringLiteral("strava_date_prefix");
const uint32_t allSettingsCount = 556;
const uint32_t allSettingsCount = 565;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1228,6 +1239,15 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::zwift_ocr_climb_portal, QZSettings::default_zwift_ocr_climb_portal},
{QZSettings::poll_device_time, QZSettings::default_poll_device_time},
{QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1},
{QZSettings::schwinn_bike_resistance_v3, QZSettings::default_schwinn_bike_resistance_v3},
{QZSettings::watt_ignore_builtin, QZSettings::default_watt_ignore_builtin},
{QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i},
{QZSettings::ftms_bike, QZSettings::default_ftms_bike},
{QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill},
{QZSettings::ant_speed_offset, QZSettings::default_ant_speed_offset},
{QZSettings::ant_speed_gain, QZSettings::default_ant_speed_gain},
{QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl},
{QZSettings::strava_date_prefix, QZSettings::default_strava_date_prefix},
};
void QZSettings::qDebugAllSettings(bool showDefaults) {

View File

@@ -1863,6 +1863,39 @@ class QZSettings {
static const QString proform_bike_PFEVEX71316_1;
static constexpr bool default_proform_bike_PFEVEX71316_1 = false;
static const QString schwinn_bike_resistance_v3;
static constexpr bool default_schwinn_bike_resistance_v3 = false;
static const QString watt_ignore_builtin;
static constexpr bool default_watt_ignore_builtin = true;
static const QString proform_treadmill_z1300i;
static constexpr bool default_proform_treadmill_z1300i = false;
static const QString ftms_bike;
static const QString default_ftms_bike;
static const QString ftms_treadmill;
static const QString default_ftms_treadmill;
static const QString proform_rower_sport_rl;
static constexpr bool default_proform_rower_sport_rl = false;
static const QString strava_date_prefix;
static constexpr bool default_strava_date_prefix = false;
/**
* @brief Adjusts value in a metric object that's configured specifically for measuring SPEED on ANT+.
*/
static const QString ant_speed_offset;
static constexpr float default_ant_speed_offset = 0;
/**
* @brief Adjusts value in a metric object that's configured specifically for measuring SPEED on ANT+.
*/
static const QString ant_speed_gain;
static constexpr float default_ant_speed_gain = 1;
/**
* @brief Write the QSettings values using the constants from this namespace.
* @param showDefaults Optionally indicates if the default should be shown with the key.

View File

@@ -320,10 +320,19 @@ void schwinnic4bike::characteristicChanged(const QLowEnergyCharacteristic &chara
if (isnan(res)) {
res = 0;
}
if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance).toBool())
bool schwinn_bike_resistance_v2 =
settings.value(QZSettings::schwinn_bike_resistance_v2, QZSettings::default_schwinn_bike_resistance_v2).toBool();
bool schwinn_bike_resistance_v3 =
settings.value(QZSettings::schwinn_bike_resistance_v3, QZSettings::default_schwinn_bike_resistance_v3).toBool();
if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance).toBool() || schwinn_bike_resistance_v2 ||
schwinn_bike_resistance_v3) {
resistance = pelotonToBikeResistance(res);
else
} else {
resistance = res;
}
if (qFabs(resistance - Resistance.value()) >=
(double)settings.value(QZSettings::schwinn_resistance_smooth, QZSettings::default_schwinn_resistance_smooth)
.toInt()) {
@@ -361,6 +370,7 @@ void schwinnic4bike::characteristicChanged(const QLowEnergyCharacteristic &chara
#endif
#endif
emit debug(QStringLiteral("Current Peloton Resistance: ") + QString::number(m_pelotonResistance.value()));
emit debug(QStringLiteral("Current Calculated Resistance: ") + QString::number(Resistance.value()));
emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs));
emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime));
@@ -555,7 +565,15 @@ resistance_t schwinnic4bike::pelotonToBikeResistance(int pelotonResistance) {
QSettings settings;
bool schwinn_bike_resistance_v2 =
settings.value(QZSettings::schwinn_bike_resistance_v2, QZSettings::default_schwinn_bike_resistance_v2).toBool();
if (!schwinn_bike_resistance_v2) {
bool schwinn_bike_resistance_v3 =
settings.value(QZSettings::schwinn_bike_resistance_v3, QZSettings::default_schwinn_bike_resistance_v3).toBool();
if (schwinn_bike_resistance_v3) {
// y = -35,3 + 1,91x + -0,0358x^2 + 4,3E-04x^3
if (pelotonResistance < 30)
return 0;
return -35.3 + 1.91 * pelotonResistance - 0.0358 * pow(pelotonResistance, 2) + 4.3E-04 * pow(pelotonResistance, 3);
} else if (!schwinn_bike_resistance_v2) {
if (pelotonResistance > 54)
return pelotonResistance;
if (pelotonResistance < 26)

View File

@@ -810,6 +810,27 @@ import QtQuick.Dialogs 1.0
// from version 2.13.99
property bool proform_bike_PFEVEX71316_1: false
property bool schwinn_bike_resistance_v3: false
// from version 2.15.2
property bool watt_ignore_builtin: true
// from version 2.16.4
property bool proform_treadmill_z1300i: false
// from version 2.16.5
property string ftms_bike: "Disabled"
property string ftms_treadmill: "Disabled"
// from version 2.16.6
property real ant_speed_offset: 0
property real ant_speed_gain: 1
// from version 2.16.12
property bool proform_rower_sport_rl: false
// from version 2.16.13
property bool strava_date_prefix: false
}
function paddingZeros(text, limit) {
@@ -2175,6 +2196,45 @@ import QtQuick.Dialogs 1.0
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
Label {
text: qsTr("FTMS Bike:")
Layout.fillWidth: true
}
RowLayout {
spacing: 10
ComboBox {
id: ftmsBikeTextField
model: rootItem.bluetoothDevices
displayText: settings.ftms_bike
Layout.fillHeight: false
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onActivated: {
console.log("combomodel activated" + ftmsBikeTextField.currentIndex)
displayText = ftmsBikeTextField.currentValue
}
}
Button {
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.ftms_bike = ftmsBikeTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("If you have a generic FTMS bike and the tiles doesn't appear on the main QZ screen, select here the bluetooth name of your bike.")
font.bold: true
font.italic: true
font.pixelSize: 9
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
}
Label {
@@ -2214,7 +2274,7 @@ import QtQuick.Dialogs 1.0
}
SwitchDelegate {
id: schwinnBikeResistanceV2Delegate
text: qsTr("Resistance Alternative Calc.")
text: qsTr("Res. Alternative Calc. v2")
spacing: 0
bottomPadding: 0
topPadding: 0
@@ -2226,6 +2286,19 @@ import QtQuick.Dialogs 1.0
Layout.fillWidth: true
onClicked: settings.schwinn_bike_resistance_v2 = checked
}
SwitchDelegate {
text: qsTr("Res. Alternative Calc. v3")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.schwinn_bike_resistance_v3
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.schwinn_bike_resistance_v3 = checked
}
RowLayout {
spacing: 10
Label {
@@ -3119,6 +3192,79 @@ import QtQuick.Dialogs 1.0
color: Material.color(Material.Lime)
}
RowLayout {
spacing: 10
Label {
text: qsTr("ANT+ Speed Offset")
Layout.fillWidth: true
}
TextField {
id: antspeedOffsetTextField
text: settings.ant_speed_offset
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
inputMethodHints: Qt.ImhDigitsOnly
onAccepted: settings.ant_speed_offset = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.ant_speed_offset = antspeedOffsetTextField.text; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("You can increase/decrease your speed sent over ANT+. The number you enter as an Offset adds that amount to your speed.")
font.bold: true
font.italic: true
font.pixelSize: 9
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
RowLayout {
spacing: 10
Label {
text: qsTr("ANT+ Speed Gain:")
Layout.fillWidth: true
}
TextField {
id: antspeedGainTextField
text: settings.ant_speed_gain
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
//inputMethodHints: Qt.ImhFormattedNumbersOnly
onAccepted: settings.ant_speed_gain = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.ant_speed_gain = antspeedGainTextField.text; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("You can increase/decrease your speed output sent over ANT+. For example, to use a rower to cycle in Zwift, you could double your speed output to better match your cycling speed. The number you enter is a multiplier applied to your actual speed.")
font.bold: true
font.italic: true
font.pixelSize: 9
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
SwitchDelegate {
id: antHeartDelegate
text: qsTr("Ant+ Heart")
@@ -3687,7 +3833,7 @@ import QtQuick.Dialogs 1.0
}
ComboBox {
id: pelotonRowerLevelTextField
model: [ "1", "2", "3", "4", "5", "6" ]
model: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ]
displayText: settings.peloton_rower_level
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
@@ -5093,6 +5239,45 @@ import QtQuick.Dialogs 1.0
color: Material.color(Material.Lime)
}
Label {
text: qsTr("FTMS Treadmill:")
Layout.fillWidth: true
}
RowLayout {
spacing: 10
ComboBox {
id: ftmsTreadmillTextField
model: rootItem.bluetoothDevices
displayText: settings.ftms_treadmill
Layout.fillHeight: false
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onActivated: {
console.log("combomodel activated" + ftmsTreadmillTextField.currentIndex)
displayText = ftmsTreadmillTextField.currentValue
}
}
Button {
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.ftms_treadmill = ftmsTreadmillTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("If you have a generic FTMS bike and the tiles doesn't appear on the main QZ screen, select here the bluetooth name of your bike.")
font.bold: true
font.italic: true
font.pixelSize: 9
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
Label {
text: qsTr("Expand the bars to the right to display the options under this setting. Select your specific model (if it is listed) and leave all other settings on default. If you encounter problems or have a question about settings for your specific equipment with QZ, click here to open a support ticket on GitHub or ask the QZ community on the QZ Facebook Group.")
font.bold: true
@@ -5227,6 +5412,19 @@ import QtQuick.Dialogs 1.0
Layout.fillWidth: true
onClicked: { settings.proform_treadmill_1800i = checked; window.settings_restart_to_apply = true; }
}
SwitchDelegate {
text: qsTr("Proform z1300i")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.proform_treadmill_z1300i
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: { settings.proform_treadmill_z1300i = checked; window.settings_restart_to_apply = true; }
}
SwitchDelegate {
id: proformSEDelegate
text: qsTr("Proform SE")
@@ -6439,6 +6637,27 @@ import QtQuick.Dialogs 1.0
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
AccordionElement {
title: qsTr("Proform/Nordictrack Rower Options")
indicatRectColor: Material.color(Material.Grey)
textColor: Material.color(Material.Yellow)
color: Material.backgroundColor
accordionContent:
SwitchDelegate {
text: qsTr("Proform Sport RL")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.proform_rower_sport_rl
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: { settings.proform_rower_sport_rl = checked; window.settings_restart_to_apply = true; }
}
}
}
}
@@ -6994,6 +7213,33 @@ import QtQuick.Dialogs 1.0
color: Material.color(Material.Lime)
}
SwitchDelegate {
text: qsTr("Date Prefix on Strava Workout")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.strava_date_prefix
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.strava_date_prefix = checked
}
Label {
text: qsTr("Append the Date to the Strava Activity as a prefix only for non-peloton workout")
font.bold: true
font.italic: true
font.pixelSize: 9
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
SwitchDelegate {
id: volumeChangeGearsDelegate
text: qsTr("Volumes buttons change gears")
@@ -7181,6 +7427,33 @@ import QtQuick.Dialogs 1.0
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
SwitchDelegate {
text: qsTr("Disable Wattage from Machinery")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.watt_ignore_builtin
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.watt_ignore_builtin = checked
}
Label {
text: qsTr("This prevents your fitness device from sending its wattage calculation to QZ and defaults to QZs more accurate calculation.")
font.bold: true
font.italic: true
font.pixelSize: 9
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
}
}

View File

@@ -133,9 +133,12 @@ void soleelliptical::update() {
gattCommunicationChannelService && gattWriteCharacteristic.isValid() &&
gattNotifyCharacteristic.isValid() && initDone) {
update_metrics(true, watts());
QSettings settings;
bool watt_ignore_builtin =
settings.value(QZSettings::watt_ignore_builtin, QZSettings::default_watt_ignore_builtin).toBool();
update_metrics(watt_ignore_builtin, watts());
bool sole_elliptical_inclination =
settings.value(QZSettings::sole_elliptical_inclination, QZSettings::default_sole_elliptical_inclination)
.toBool();
@@ -339,16 +342,19 @@ void soleelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
// double distance = GetDistanceFromPacket(newValue) *
// settings.value(QZSettings::domyos_elliptical_speed_ratio,
// QZSettings::default_domyos_elliptical_speed_ratio).toDouble();
uint16_t watt = (newValue.at(13) << 8) | newValue.at(14);
uint16_t watt = ((uint16_t)((uint8_t)newValue.at(13)) << 8) | (uint16_t)((uint8_t)newValue.at(14));
bool disable_hr_frommachinery =
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
bool watt_ignore_builtin =
settings.value(QZSettings::watt_ignore_builtin, QZSettings::default_watt_ignore_builtin).toBool();
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = ((uint8_t)newValue.at(10));
}
// m_watt = watt;
if(!watt_ignore_builtin)
m_watt = watt;
if (Resistance.value() < 1) {
emit debug(QStringLiteral("invalid resistance value ") + QString::number(Resistance.value()) +
@@ -364,7 +370,7 @@ void soleelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
{
if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery) {
Heart = ((uint8_t)newValue.at(18));
} else {
} else if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
update_hr_from_external();
}
}

View File

@@ -19,6 +19,40 @@
using namespace std::chrono_literals;
#define TRAINPROGRAM_FIELD_TO_STRING() \
item[QStringLiteral("duration")] = row.duration.toString(); \
item[QStringLiteral("duration_s")] = QTime(0,0,0).secsTo(row.duration); \
item[QStringLiteral("distance")] = row.distance; \
item[QStringLiteral("speed")] = row.speed; \
item[QStringLiteral("minspeed")] = row.minSpeed; \
item[QStringLiteral("maxspeed")] = row.maxSpeed; \
item[QStringLiteral("fanspeed")] = row.fanspeed; \
item[QStringLiteral("inclination")] = row.inclination; \
item[QStringLiteral("resistance")] = row.resistance; \
item[QStringLiteral("maxresistance")] = row.maxResistance; \
item[QStringLiteral("mets")] = row.mets; \
item[QStringLiteral("pace_intensity")] = row.pace_intensity; \
item[QStringLiteral("lower_resistance")] = row.lower_resistance; \
item[QStringLiteral("upper_resistance")] = row.upper_resistance; \
item[QStringLiteral("requested_peloton_resistance")] = row.requested_peloton_resistance; \
item[QStringLiteral("lower_requested_peloton_resistance")] = row.lower_requested_peloton_resistance; \
item[QStringLiteral("upper_requested_peloton_resistance")] = row.upper_requested_peloton_resistance; \
item[QStringLiteral("power")] = row.power; \
item[QStringLiteral("cadence")] = row.cadence; \
item[QStringLiteral("lower_cadence")] = row.lower_cadence; \
item[QStringLiteral("upper_cadence")] = row.upper_cadence; \
item[QStringLiteral("forcespeed")] = row.forcespeed; \
item[QStringLiteral("loopTimeHR")] = row.loopTimeHR; \
item[QStringLiteral("zoneHR")] = row.zoneHR; \
item[QStringLiteral("HRmin")] = row.HRmin; \
item[QStringLiteral("HRmax")] = row.HRmax; \
item[QStringLiteral("maxSpeed")] = row.maxSpeed; \
item[QStringLiteral("latitude")] = row.latitude; \
item[QStringLiteral("longitude")] = row.longitude; \
item[QStringLiteral("altitude")] = row.altitude; \
item[QStringLiteral("azimuth")] = row.azimuth;
QHash<QString, TemplateInfoSenderBuilder *> TemplateInfoSenderBuilder::instanceMap;
TemplateInfoSenderBuilder::TemplateInfoSenderBuilder(QObject *parent) : QObject(parent) {
engine = new QJSEngine(this);
@@ -428,21 +462,7 @@ void TemplateInfoSenderBuilder::onLoadTrainingPrograms(const QJsonValue &msgCont
fileXml + QStringLiteral(".xml"));
for (auto &row : lst) {
QJsonObject item;
item[QStringLiteral("duration")] = row.duration.toString();
item[QStringLiteral("speed")] = row.speed;
item[QStringLiteral("fanspeed")] = row.fanspeed;
item[QStringLiteral("inclination")] = row.inclination;
item[QStringLiteral("resistance")] = row.resistance;
item[QStringLiteral("requested_peloton_resistance")] = row.requested_peloton_resistance;
item[QStringLiteral("cadence")] = row.cadence;
item[QStringLiteral("forcespeed")] = row.forcespeed;
item[QStringLiteral("loopTimeHR")] = row.loopTimeHR;
item[QStringLiteral("zoneHR")] = row.zoneHR;
item[QStringLiteral("HRmin")] = row.HRmin;
item[QStringLiteral("HRmax")] = row.HRmax;
item[QStringLiteral("maxSpeed")] = row.maxSpeed;
item[QStringLiteral("latitude")] = row.latitude;
item[QStringLiteral("longitude")] = row.longitude;
TRAINPROGRAM_FIELD_TO_STRING();
outArr.append(item);
}
}
@@ -454,6 +474,27 @@ void TemplateInfoSenderBuilder::onLoadTrainingPrograms(const QJsonValue &msgCont
tempSender->send(out.toJson());
}
void TemplateInfoSenderBuilder::onGetTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
QJsonObject main;
QJsonArray outArr;
QJsonObject outObj;
QString fileXml;
if (homeform::singleton() && homeform::singleton()->trainingProgram()) {
QList<trainrow> lst = homeform::singleton()->trainingProgram()->loadedRows;
for (auto &row : lst) {
QJsonObject item;
TRAINPROGRAM_FIELD_TO_STRING();
outArr.append(item);
}
}
outObj[QStringLiteral("list")] = outArr;
outObj[QStringLiteral("name")] = fileXml;
main[QStringLiteral("content")] = outObj;
main[QStringLiteral("msg")] = QStringLiteral("R_gettrainingprogram");
QJsonDocument out(main);
tempSender->send(out.toJson());
}
void TemplateInfoSenderBuilder::onAppendActivityDescription(const QJsonValue &msgContent,
TemplateInfoSender *tempSender) {
QJsonObject content;
@@ -793,6 +834,9 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) {
} else if (msg == QStringLiteral("loadtrainingprograms")) {
onLoadTrainingPrograms(jsonObject[QStringLiteral("content")], sender);
return;
} else if (msg == QStringLiteral("gettrainingprogram")) {
onGetTrainingProgram(jsonObject[QStringLiteral("content")], sender);
return;
} else if (msg == QStringLiteral("appendactivitydescription")) {
onAppendActivityDescription(jsonObject[QStringLiteral("content")], sender);
return;

View File

@@ -79,6 +79,7 @@ class TemplateInfoSenderBuilder : public QObject {
void onAutoresistance(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
void onSaveTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
void onLoadTrainingPrograms(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
void onGetTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
void onAppendActivityDescription(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
void onGetSessionArray(TemplateInfoSender *tempSender);
void onGetLatLon(TemplateInfoSender *tempSender);

View File

@@ -84,6 +84,12 @@ class trainprogram : public QObject {
double weightedInclination(int step);
double medianInclination(int step);
bool overridePowerForCurrentRow(double power);
bool powerzoneWorkout() {
foreach(trainrow r, rows) {
if(r.power != -1) return true;
}
return false;
}
QList<trainrow> rows;
QList<trainrow> loadedRows; // rows as loaded

View File

@@ -488,7 +488,7 @@ void trxappgateusbbike::btinit(bool startTape) {
writeCharacteristic((uint8_t *)initData4, sizeof(initData4), QStringLiteral("init"), false, true);
writeCharacteristic((uint8_t *)initData5, sizeof(initData5), QStringLiteral("init"), false, true);
writeCharacteristic((uint8_t *)initData6, sizeof(initData6), QStringLiteral("init"), false, true);
} else if (bike_type == TYPE::HERTZ_XR_770 || bike_type == TYPE::HERTZ_XR_770_2) {
} else if (bike_type == TYPE::HERTZ_XR_770 || bike_type == TYPE::HERTZ_XR_770_2 || bike_type == TYPE::FITHIWAY) {
const uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x01, 0x92};
const uint8_t initData2[] = {0xf0, 0xa0, 0x02, 0x01, 0x93};
const uint8_t initData3[] = {0xf0, 0xa3, 0x01, 0x01, 0x01, 0x96};
@@ -728,7 +728,8 @@ void trxappgateusbbike::stateChanged(QLowEnergyService::ServiceState state) {
if (bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE ||
bike_type == TYPE::JLL_IC400 || bike_type == TYPE::DKN_MOTION_2 || bike_type == TYPE::FYTTER_RI08 ||
bike_type == TYPE::HERTZ_XR_770_2 || bike_type == TYPE::VIRTUFIT_2 || bike_type == TYPE::TUNTURI) {
bike_type == TYPE::HERTZ_XR_770_2 || bike_type == TYPE::VIRTUFIT_2 || bike_type == TYPE::TUNTURI ||
bike_type == TYPE::FITHIWAY) {
uuidWrite = QStringLiteral("49535343-8841-43f4-a8d4-ecbe34729bb3");
uuidNotify1 = QStringLiteral("49535343-1E4D-4BD9-BA61-23C647249616");
uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e");
@@ -815,7 +816,8 @@ void trxappgateusbbike::serviceScanDone(void) {
QString uuid2 = QStringLiteral("49535343-FE7D-4AE5-8FA9-9FAFD205E455");
QString uuid3 = QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb");
if (bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE ||
bike_type == TYPE::JLL_IC400 || bike_type == TYPE::FYTTER_RI08 || bike_type == TYPE::TUNTURI) {
bike_type == TYPE::JLL_IC400 || bike_type == TYPE::FYTTER_RI08 || bike_type == TYPE::TUNTURI ||
bike_type == TYPE::FITHIWAY) {
uuid = uuid2;
}
@@ -982,6 +984,9 @@ void trxappgateusbbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
bike_type = TYPE::DKN_MOTION;
qDebug() << QStringLiteral("DKN MOTION bike found");
} else if (device.name().toUpper().startsWith(QStringLiteral("FITHIWAY"))) {
bike_type = TYPE::FITHIWAY;
qDebug() << QStringLiteral("FITHIWAY bike found");
}
bluetoothDevice = device;

View File

@@ -100,6 +100,7 @@ class trxappgateusbbike : public bike {
VIRTUFIT_2 = 15,
TUNTURI = 16,
TUNTURI_2 = 17,
FITHIWAY = 18,
} TYPE;
TYPE bike_type = TRXAPPGATE;

View File

@@ -41,7 +41,12 @@ void trxappgateusbtreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
} else {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
}
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info);

View File

@@ -234,7 +234,10 @@ void wahookickrsnapbike::update() {
uint8_t b[20];
memcpy(b, a.constData(), a.length());
writeCharacteristic(b, a.length(), "setResistance", false, true);
} else if (virtualBike && virtualBike->ftmsDeviceConnected() && lastGearValue != gears()) {
inclinationChanged(lastGrade, lastGrade);
}
lastGearValue = gears();
requestResistance = -1;
}
if (requestStart != -1) {
@@ -789,8 +792,9 @@ void wahookickrsnapbike::controllerStateChanged(QLowEnergyController::Controller
void wahookickrsnapbike::inclinationChanged(double grade, double percentage) {
Q_UNUSED(percentage);
lastGrade = grade;
emit debug(QStringLiteral("writing inclination ") + QString::number(grade));
QByteArray a = setSimGrade(grade);
QByteArray a = setSimGrade(grade + gears());
uint8_t b[20];
memcpy(b, a.constData(), a.length());
writeCharacteristic(b, a.length(), "setSimGrade", false, true);

View File

@@ -83,6 +83,8 @@ class wahookickrsnapbike : public bike {
uint8_t sec1Update = 0;
QByteArray lastPacket;
double lastGearValue = -1;
double lastGrade = 0;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
QDateTime lastGoodCadence = QDateTime::currentDateTime();
uint8_t firstStateChanged = 0;

View File

@@ -1,110 +1,107 @@
# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR
# Author: Al Udell
# Revised: April 22, 2023
# process-image.py - take Zwift screenshot, crop incline, OCR incline
# imports
import cv2
import numpy as np
import re
from datetime import datetime
from paddleocr import PaddleOCR
from PIL import Image, ImageGrab
# Take Zwift screenshot
screenshot = ImageGrab.grab()
# Scale image to 3000 x 2000
screenshot = screenshot.resize((3000, 2000))
# Convert screenshot to a numpy array
screenshot_np = np.array(screenshot)
# Crop image to incline area
screenwidth, screenheight = screenshot.size
# Values for Zwift climb portal incline
col1 = int(screenwidth/3000 * 2822)
row1 = int(screenheight/2000 * 218)
col2 = int(screenwidth/3000 * 2980)
row2 = int(screenheight/2000 * 302)
cropped_np = screenshot_np[row1:row2, col1:col2]
# Convert numpy array to PIL image
cropped_pil = Image.fromarray(cropped_np)
# Convert PIL Image to a cv2 image
cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR)
# Convert cv2 image to HSV
result = cropped_cv2.copy()
image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV)
# Isolate white mask
lower = np.array([0,0,159])
upper = np.array([0,0,255])
mask0 = cv2.inRange(image, lower, upper)
result0 = cv2.bitwise_and(result, result, mask=mask0)
# Isolate yellow mask
lower = np.array([24,239,241])
upper = np.array([24,253,255])
mask1 = cv2.inRange(image, lower, upper)
result1 = cv2.bitwise_and(result, result, mask=mask1)
# Isolate orange mask
lower = np.array([8,191,243])
upper = np.array([8,192,243])
mask2 = cv2.inRange(image, lower, upper)
result2 = cv2.bitwise_and(result, result, mask=mask2)
# Isolate red mask
lower = np.array([0,255,255])
upper = np.array([10,255,255])
mask3 = cv2.inRange(image, lower, upper)
result3 = cv2.bitwise_and(result, result, mask=mask3)
# Join colour masks
mask = mask0+mask1+mask2+mask3
# Set output image to zero everywhere except mask
merge = image.copy()
merge[np.where(mask==0)] = 0
# Convert to grayscale
gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY)
# Convert to black/white by threshold
ret,bin = cv2.threshold(gray,30,255,cv2.THRESH_BINARY)
# Closing
kernel = np.ones((3,3),np.uint8)
closing = cv2.morphologyEx(bin, cv2.MORPH_CLOSE, kernel)
# Invert black/white
inv = cv2.bitwise_not(closing)
# Apply average blur
averageBlur = cv2.blur(inv, (3, 3))
# OCR image
ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False)
result = ocr.ocr(averageBlur, cls=False, det=True, rec=True)
# Extract OCR text
ocr_text = ''
for line in result:
for word in line:
ocr_text += f"{word[1][0]}"
# Remove all characters that are not "-" and integers from OCR text
pattern = r"[^-\d]+"
ocr_text = re.sub(pattern, "", ocr_text)
if ocr_text:
incline = ocr_text
else:
incline = 'None'
print(incline)
# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR
# Author: Al Udell
# Revised: August 16, 2023
# zwift-incline-climb-portal.py - take Zwift screenshot, crop incline, OCR incline
# imports
import cv2
import numpy as np
import re
from datetime import datetime
from paddleocr import PaddleOCR
from PIL import Image, ImageGrab
# Take Zwift screenshot
screenshot = ImageGrab.grab()
# Scale image to 3000 x 2000
screenshot = screenshot.resize((3000, 2000))
# Crop image to incline area
screenwidth, screenheight = screenshot.size
# Values for Zwift climb portal incline
col1 = int(screenwidth/3000 * 2822)
row1 = int(screenheight/2000 * 218)
col2 = int(screenwidth/3000 * 2980)
row2 = int(screenheight/2000 * 302)
cropped = screenshot.crop((col1, row1, col2, row2))
# Scale image to correct size for borderless window mode
width, height = cropped.size
cropped = cropped.resize((int(width * 1.3), int(height * 1.3)))
# Convert image to np array
cropped_np = np.array(cropped)
# Convert np array to PIL
cropped_pil = Image.fromarray(cropped_np)
# Convert PIL image to cv2 RGB
cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR)
# Convert cv2 RGB to HSV
result = cropped_cv2.copy()
image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV)
# Isolate white mask
lower = np.array([0,0,159])
upper = np.array([0,0,255])
mask0 = cv2.inRange(image, lower, upper)
result0 = cv2.bitwise_and(result, result, mask=mask0)
# Isolate yellow mask
lower = np.array([24,239,241])
upper = np.array([24,253,255])
mask1 = cv2.inRange(image, lower, upper)
result1 = cv2.bitwise_and(result, result, mask=mask1)
# Isolate orange mask
lower = np.array([8,191,243])
upper = np.array([8,192,243])
mask2 = cv2.inRange(image, lower, upper)
result2 = cv2.bitwise_and(result, result, mask=mask2)
# Isolate red mask
lower = np.array([0,255,255])
upper = np.array([10,255,255])
mask3 = cv2.inRange(image, lower, upper)
result3 = cv2.bitwise_and(result, result, mask=mask3)
# Join colour masks
mask = mask0+mask1+mask2+mask3
# Set output image to zero everywhere except mask
merge = image.copy()
merge[np.where(mask==0)] = 0
# Convert to grayscale
gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY)
# Convert to black/white by threshold
ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV)
# Apply gaussian blur
gaussianBlur = cv2.GaussianBlur(bin,(3,3),0)
# OCR image
ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False)
result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True)
# Extract OCR text
ocr_text = ''
for line in result:
for word in line:
ocr_text += f"{word[1][0]}"
# Remove all characters that are not "-" and integers from OCR text
pattern = r"[^-\d]+"
ocr_text = re.sub(pattern, "", ocr_text)
if ocr_text:
incline = ocr_text
else:
incline = 'None'
print(incline)

View File

@@ -1,110 +1,103 @@
# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR
# Author: Al Udell
# Revised: April 22, 2023
# process-image.py - take Zwift screenshot, crop incline, OCR incline
# imports
import cv2
import numpy as np
import re
from datetime import datetime
from paddleocr import PaddleOCR
from PIL import Image, ImageGrab
# Take Zwift screenshot
screenshot = ImageGrab.grab()
# Scale image to 3000 x 2000
screenshot = screenshot.resize((3000, 2000))
# Convert screenshot to a numpy array
screenshot_np = np.array(screenshot)
# Crop image to incline area
screenwidth, screenheight = screenshot.size
# Values for Zwift regular incline
col1 = int(screenwidth/3000 * 2800)
row1 = int(screenheight/2000 * 75)
col2 = screenwidth
row2 = int(screenheight/2000 * 200)
cropped_np = screenshot_np[row1:row2, col1:col2]
# Convert numpy array to PIL image
cropped_pil = Image.fromarray(cropped_np)
# Convert PIL Image to a cv2 image
cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR)
# Convert cv2 image to HSV
result = cropped_cv2.copy()
image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV)
# Isolate white mask
lower = np.array([0,0,159])
upper = np.array([0,0,255])
mask0 = cv2.inRange(image, lower, upper)
result0 = cv2.bitwise_and(result, result, mask=mask0)
# Isolate yellow mask
lower = np.array([24,239,241])
upper = np.array([24,253,255])
mask1 = cv2.inRange(image, lower, upper)
result1 = cv2.bitwise_and(result, result, mask=mask1)
# Isolate orange mask
lower = np.array([8,191,243])
upper = np.array([8,192,243])
mask2 = cv2.inRange(image, lower, upper)
result2 = cv2.bitwise_and(result, result, mask=mask2)
# Isolate red mask
lower = np.array([0,255,255])
upper = np.array([10,255,255])
mask3 = cv2.inRange(image, lower, upper)
result3 = cv2.bitwise_and(result, result, mask=mask3)
# Join colour masks
mask = mask0+mask1+mask2+mask3
# Set output image to zero everywhere except mask
merge = image.copy()
merge[np.where(mask==0)] = 0
# Convert to grayscale
gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY)
# Convert to black/white by threshold
ret,bin = cv2.threshold(gray,30,255,cv2.THRESH_BINARY)
# Closing
kernel = np.ones((3,3),np.uint8)
closing = cv2.morphologyEx(bin, cv2.MORPH_CLOSE, kernel)
# Invert black/white
inv = cv2.bitwise_not(closing)
# Apply average blur
averageBlur = cv2.blur(inv, (3, 3))
# OCR image
ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False)
result = ocr.ocr(averageBlur, cls=False, det=True, rec=True)
# Extract OCR text
ocr_text = ''
for line in result:
for word in line:
ocr_text += f"{word[1][0]}"
# Remove all characters that are not "-" and integers from OCR text
pattern = r"[^-\d]+"
ocr_text = re.sub(pattern, "", ocr_text)
if ocr_text:
incline = ocr_text
else:
incline = 'None'
print(incline)
# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR
# Author: Al Udell
# Revised: August 16, 2023
# zwift-incline.py - take Zwift screenshot, crop incline, OCR incline
# imports
import cv2
import numpy as np
import re
from datetime import datetime
from paddleocr import PaddleOCR
from PIL import Image, ImageGrab
# Take Zwift screenshot
screenshot = ImageGrab.grab()
# Scale image to 3000 x 2000
screenshot = screenshot.resize((3000, 2000))
# Crop image to incline area
screenwidth, screenheight = screenshot.size
# Values for Zwift regular incline
col1 = int(screenwidth/3000 * 2800)
row1 = int(screenheight/2000 * 90)
col2 = int(screenwidth/3000 * 2975)
row2 = int(screenheight/2000 * 195)
cropped = screenshot.crop((col1, row1, col2, row2))
# Convert image to np array
cropped_np = np.array(cropped)
# Convert np array to PIL
cropped_pil = Image.fromarray(cropped_np)
# Convert PIL image to cv2 RGB
cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR)
# Convert cv2 RGB to HSV
result = cropped_cv2.copy()
image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV)
# Isolate white mask
lower = np.array([0,0,159])
upper = np.array([0,0,255])
mask0 = cv2.inRange(image, lower, upper)
result0 = cv2.bitwise_and(result, result, mask=mask0)
# Isolate yellow mask
lower = np.array([24,239,241])
upper = np.array([24,253,255])
mask1 = cv2.inRange(image, lower, upper)
result1 = cv2.bitwise_and(result, result, mask=mask1)
# Isolate orange mask
lower = np.array([8,191,243])
upper = np.array([8,192,243])
mask2 = cv2.inRange(image, lower, upper)
result2 = cv2.bitwise_and(result, result, mask=mask2)
# Isolate red mask
lower = np.array([0,255,255])
upper = np.array([10,255,255])
mask3 = cv2.inRange(image, lower, upper)
result3 = cv2.bitwise_and(result, result, mask=mask3)
# Join colour masks
mask = mask0+mask1+mask2+mask3
# Set output image to zero everywhere except mask
merge = image.copy()
merge[np.where(mask==0)] = 0
# Convert to grayscale
gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY)
# Convert to black/white by threshold
ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV)
# Apply gaussian blur
gaussianBlur = cv2.GaussianBlur(bin,(3,3),0)
# OCR image
ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False)
result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True)
# Extract OCR text
ocr_text = ''
for line in result:
for word in line:
ocr_text += f"{word[1][0]}"
# Remove all characters that are not "-" and integers from OCR text
pattern = r"[^-\d]+"
ocr_text = re.sub(pattern, "", ocr_text)
if ocr_text:
incline = ocr_text
else:
incline = 'None'
print(incline)

View File

@@ -1,67 +1,71 @@
# iFit-Workout - Auto-incline and auto-speed control of treadmill via ADB and OCR for Zwift workouts
# Author: Al Udell
# Revised: April 27, 2023
# process-image.py - take Zwift screenshot, crop speed/incline instruction, OCR speed/incline
# imports
import cv2
import numpy as np
import re
from datetime import datetime
from paddleocr import PaddleOCR
from PIL import Image, ImageGrab
# Take Zwift screenshot
screenshot = ImageGrab.grab()
# Scale image to 3000 x 2000
screenshot = screenshot.resize((3000, 2000))
# Convert screenshot to a numpy array
screenshot_np = np.array(screenshot)
# Convert numpy array to a cv2 RGB image
screenshot_cv2 = cv2.cvtColor(screenshot_np, cv2.COLOR_BGR2RGB)
# Crop image to workout instruction area
screenwidth, screenheight = screenshot.size
col1 = int(screenwidth/3000 * 1010)
row1 = int(screenheight/2000 * 260)
col2 = int(screenwidth/3000 * 1285)
row2 = int(screenheight/2000 * 480)
cropped_cv2 = screenshot_cv2[row1:row2, col1:col2]
# OCR image
ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False)
result = ocr.ocr(cropped_cv2, cls=False, det=True, rec=True)
# Extract OCR text
ocr_text = ''
for line in result:
for word in line:
ocr_text += f"{word[1][0]} "
# Find the speed number
num_pattern = r'\d+(\.\d+)?' # Regular expression pattern to match numbers with optional decimal places
unit_pattern = r'\s+(kph|mph)' # Regular expression pattern to match "kph" or "mph" units
speed_match = re.search(num_pattern + unit_pattern, ocr_text)
if speed_match:
speed = speed_match.group(0)
pattern = r'\d+\.\d+'
speed = re.findall(pattern, speed)[0]
else:
speed = 'None'
# Find the incline number
incline_pattern = r'\d+\s*%' # Regular expression pattern to match numbers with "%"
incline_match = re.search(incline_pattern, ocr_text)
if incline_match:
incline = incline_match.group(0)
pattern = r'\d+'
incline = re.findall(pattern, incline)[0]
else:
incline = 'None'
print(f"{speed};{incline}")
# iFit-Workout - Auto-incline and auto-speed control of treadmill via ADB and OCR for Zwift workouts
# Author: Al Udell
# Revised: August 16, 2023
# zwift-workout.py - take Zwift screenshot, crop speed/incline instruction, OCR speed/incline
# imports
import cv2
import numpy as np
import re
from datetime import datetime
from paddleocr import PaddleOCR
from PIL import Image, ImageGrab
# Take Zwift screenshot
screenshot = ImageGrab.grab()
# Scale image to 3000 x 2000
screenshot = screenshot.resize((3000, 2000))
# Crop image to workout instruction area
screenwidth, screenheight = screenshot.size
# Values for Zwift workout instructions
col1 = int(screenwidth/3000 * 1010)
row1 = int(screenheight/2000 * 260)
col2 = int(screenwidth/3000 * 1285)
row2 = int(screenheight/2000 * 480)
cropped = screenshot.crop((col1, row1, col2, row2))
# Scale image to correct size for borderless window mode
width, height = cropped.size
cropped = cropped.resize((int(width * 0.99), int(height * 0.99)))
# Convert image to np array
cropped_np = np.array(cropped)
# OCR image
ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False)
result = ocr.ocr(cropped_np, cls=False, det=True, rec=True)
# Extract OCR text
ocr_text = ''
for line in result:
for word in line:
ocr_text += f"{word[1][0]} "
# Find the speed number
num_pattern = r'\d+(\.\d+)?' # Regular expression pattern to match numbers with optional decimal places
unit_pattern = r'\s+(kph|mph)' # Regular expression pattern to match "kph" or "mph" units
speed_match = re.search(num_pattern + unit_pattern, ocr_text)
if speed_match:
speed = speed_match.group(0)
pattern = r'\d+\.\d+'
speed = re.findall(pattern, speed)[0]
else:
speed = 'None'
# Find the incline number
incline_pattern = r'-?\d+\s*%' # Regular expression pattern to match numbers with "%"
incline_match = re.search(incline_pattern, ocr_text)
if incline_match:
incline = incline_match.group(0)
pattern = r'-?\d+'
incline = re.findall(pattern, incline)[0]
else:
incline = 'None'
print(f"{speed};{incline}")

View File

@@ -7,7 +7,6 @@ class SoleF80TreadmillTestData : public BluetoothDeviceTestData {
public:
SoleF80TreadmillTestData() : BluetoothDeviceTestData("Sole F80") {
this->addDeviceName("F80", comparison::StartsWithIgnoreCase);
this->addDeviceName("F65", comparison::StartsWithIgnoreCase);
this->addDeviceName("S77", comparison::StartsWithIgnoreCase);
this->addDeviceName("TT8", comparison::StartsWithIgnoreCase);
@@ -40,6 +39,7 @@ class SoleF85TreadmillTestData : public BluetoothDeviceTestData {
public:
SoleF85TreadmillTestData() : BluetoothDeviceTestData("Sole F85 Treadmill") {
this->addDeviceName("F85", comparison::StartsWithIgnoreCase);
this->addDeviceName("F80", comparison::StartsWithIgnoreCase);
}
deviceType get_expectedDeviceType() const override { return deviceType::SoleF80Treadmill; }