Compare commits

...

73 Commits

Author SHA1 Message Date
Roberto Viola
ebe975321d Update Ant.java 2025-09-16 21:00:27 +02:00
Roberto Viola
397fe4f4de Update AntRemoteControl.java 2025-09-16 20:58:51 +02:00
Roberto Viola
d28fc0753f Update AntRemoteControl.java 2025-09-16 16:35:00 +02:00
Roberto Viola
597ef2259b Update AntRemoteControl.java 2025-09-16 15:52:34 +02:00
Roberto Viola
0394e56cd6 Ant Remote Controller 2025-09-16 14:51:57 +02:00
Roberto Viola
704c7f1f80 Kingsmith WalkingPad R3 Hybrid+ 2025-09-16 10:49:48 +02:00
Roberto Viola
678ac9d466 adding info for MAC 2025-09-16 10:00:15 +02:00
Roberto Viola
a8a6c5d736 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-16 09:39:33 +02:00
Roberto Viola
e8408710df adding MACCATALYST for ios 2025-09-16 09:39:13 +02:00
Roberto Viola
47825f0783 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-16 09:16:26 +02:00
Roberto Viola
f7ce518812 mobvoi se manual incline (Issue #3690) 2025-09-16 09:16:21 +02:00
Roberto Viola
f887a068b9 Power zone tiles not using erg mode (Issue #3681) 2025-09-15 13:49:29 +02:00
Roberto Viola
6ecbce4b87 adding KICKR RUN treadmill 2025-09-15 11:26:44 +02:00
Roberto Viola
9454d75f55 Update project.pbxproj 2025-09-12 10:39:04 +02:00
Roberto Viola
4063321418 Update project.pbxproj 2025-09-12 09:27:16 +02:00
Roberto Viola
bb88d58e47 Power zone tiles not using erg mode (Issue #3681) 2025-09-12 09:26:17 +02:00
Roberto Viola
7bc2f065c0 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-12 08:52:58 +02:00
Roberto Viola
c773b45ddf 2.20.11 2025-09-12 08:52:55 +02:00
Roberto Viola
eaf7db7813 Implement threaded FIT backup file writing (#3676)
* Implement threaded FIT backup file writing

- Add FitBackupWriter class to handle FIT file saving in background thread
- Move FIT backup writing from main thread to dedicated worker thread
- Use Qt's signal/slot mechanism with QueuedConnection for thread safety
- Similar implementation pattern to existing LogWriter threading
- Prevents UI blocking during FIT file saves every minute

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* build fix

* fix

* fix signal

* fix

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-11 20:46:36 +02:00
Roberto Viola
a29f6350d0 Update project.pbxproj 2025-09-11 12:39:53 +02:00
Roberto Viola
65ad925d37 How to Make 10s Intervals Work with Virtual Shifting (Discussion #3603) 2025-09-11 12:33:49 +02:00
Roberto Viola
8fd486d582 adding decimal places to current strokes length 2025-09-10 09:35:47 +02:00
Roberto Viola
8fa5dcadcb strokelength for Concept rower 2025-09-09 16:03:58 +02:00
Roberto Viola
6abf6c9cfd strokelength for Concept rower 2025-09-08 16:05:19 +02:00
Roberto Viola
b4603da714 Saris H3+ Slow to gear and resistance/incline changes on Zwift (Windows) + QZ on Android 16 (Pixel 8 Pro) #3660 2025-09-08 15:26:17 +02:00
Roberto Viola
b27e84de69 Update project.pbxproj 2025-09-05 11:44:02 +02:00
Roberto Viola
49337cbbc6 Update toorxtreadmill.h 2025-09-05 11:34:34 +02:00
Roberto Viola
fe2f5e923c Pafer treadmill #2985 2025-09-05 09:20:43 +02:00
Roberto Viola
69f54dbd54 Pafer treadmill #2985 2025-09-05 09:18:53 +02:00
Roberto Viola
bc20ec0d8f Option to Enable/Disable Haptic Feedback on Zwift Play Controllers (Issue #3669) 2025-09-05 08:44:45 +02:00
Roberto Viola
278add7a11 Setting resistance for skandika nordlys (Issue #3667) 2025-09-04 16:11:18 +02:00
Roberto Viola
6e90091883 Support for LSG Treadmills (Issue #3665) 2025-09-04 09:29:16 +02:00
Roberto Viola
ebda22d7b4 skandika nordlys 2025-09-03 16:20:01 +02:00
Roberto Viola
625ffb3932 skandika nordlys 2025-09-03 16:17:28 +02:00
Roberto Viola
fe6868911e Controlar intensidad de un workout aumentando o disminuyendo el porcentaje de FTP objetivo (Discussion #3664) 2025-09-03 10:28:57 +02:00
Roberto Viola
1c73d15377 webserverinfosender disconnect crash (#3661) 2025-09-01 15:20:50 +02:00
Roberto Viola
c33ee55efb Update homeform.cpp 2025-09-01 11:50:25 +02:00
Roberto Viola
56979a2122 Update homeform.cpp 2025-09-01 11:07:31 +02:00
Roberto Viola
3e1db8bfdf Support ROWING device type in writeProcess
mail "Question re QZ App" from Michael M. of 31/8/2025

Extended the writeProcess method to handle the ROWING device type in addition to BIKE. This allows the processor to support additional device types for characteristic writes.
2025-09-01 11:05:57 +02:00
Roberto Viola
10fdc52446 QZ & Peloton Sync Drift in Tread Classes (Issue #3624) 2025-09-01 10:56:41 +02:00
Roberto Viola
23d23c40a5 QZ & Peloton Sync Drift in Tread Classes (Issue #3624) 2025-09-01 09:54:59 +02:00
Roberto Viola
90e8eeb983 Update main.yml 2025-08-31 06:47:17 +02:00
Roberto Viola
dcf395ec46 Update main.yml 2025-08-29 16:06:13 +02:00
Roberto Viola
d55cb553d3 Add chart display mode setting with zoom controls (#3627)
Introduces a new 'chart_display_mode' setting allowing users to select between both charts, heart rate only, or power only in the chart footer. Updates QML and settings UI to support this option, and adds zoom buttons to each chart for focused time-range viewing. JavaScript logic is enhanced to handle dynamic chart display and zooming, including interval-based updates to the visible time window.
2025-08-29 15:32:52 +02:00
Roberto Viola
b862d26bc3 Multiple files in different instances Other Folder in Training Program List (#3651)
* Update TrainingProgramsList.qml

* Update TrainingProgramsList.qml

* did for gpx, profiles and settings
2025-08-29 10:35:37 +02:00
Roberto Viola
d5e4f11849 Active Calories (#3630)
* first commit

* Update AppDelegate.swift

* watchkit

* apex bike cadence updated

* adding something for debug

* Update project.pbxproj

* removing basal

* fixing

* build 1145

* Update project.pbxproj

* Add option to calculate calories from heart rate

Introduces a new setting to calculate calories based on heart rate data instead of power. Updates the bluetoothdevice logic to support HR-based calorie calculation, adds a new metric for HR calories, and exposes the option in the settings UI. Also updates QZSettings to include the new configuration key and default.

* build 1149

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Zwift erg mode workouts not functioning #3643

* Update project.pbxproj
2025-08-29 08:43:29 +02:00
Roberto Viola
5e9679f6c3 Merach W50 FTMS Treadmill 2025-08-29 08:40:25 +02:00
Roberto Viola
8799c447fb QZ not working with Taurus FX9.9 elliptical (Issue #3618) 2025-08-27 14:57:18 +02:00
Roberto Viola
bcdb767b7e adding compensation when there is a power sensor and an ergModeSupported bike (PR #3388) 2025-08-27 11:29:22 +02:00
Roberto Viola
15e208d34c Zwift erg mode workouts not functioning (Discussion #3643)
Improves logic for routing power requests to the bike, including handling of virtual bikes, ZwiftPlay, and external power sensors. Updates FTMS characteristic change handling to block simulation commands in resistance level mode and only allow power commands when no external power sensor is present.
2025-08-27 10:48:48 +02:00
Roberto Viola
f16c41e6dd Zwift erg mode workouts not functioning (Discussion #3643)
Refines the logic for routing FTMS power commands to the bike by considering the presence of an external power sensor and erg mode support. Now allows power commands through when no external power sensor is configured and erg mode is supported, even if resistance level mode is active. Adds more detailed debug output for easier troubleshooting.
2025-08-27 10:43:57 +02:00
Roberto Viola
9110c55cb1 Zwift erg mode workouts not functioning (Discussion #3643) 2025-08-27 10:39:17 +02:00
Roberto Viola
881e155cbc QZ & Peloton Sync Drift in Tread Classes #3624 2025-08-27 09:39:26 +02:00
Roberto Viola
e2d187a7bd Zwift erg mode workouts not functioning (Discussion #3643) 2025-08-26 14:57:11 +02:00
Roberto Viola
66821d884a Update main.yml 2025-08-26 13:54:32 +02:00
Roberto Viola
73ad1dc46c Add support for KS-HDSY-X21C treadmill variant
Introduces detection and handling for KS-HDSY-X21C devices, including new flags and GATT service/characteristic UUIDs. This improves compatibility with additional Kingsmith treadmill models.
2025-08-26 08:22:20 +02:00
Roberto Viola
c91a2d3ee5 Update main.yml 2025-08-25 14:13:00 +02:00
Roberto Viola
87c0e95b01 Update main.yml 2025-08-25 14:10:59 +02:00
Roberto Viola
174da2ac14 Update main.yml 2025-08-25 14:05:45 +02:00
Roberto Viola
b61ba37b8f Update main.yml 2025-08-25 14:05:07 +02:00
Roberto Viola
27333e7836 Constant low wattage regardless of resistance #3641 2025-08-25 10:48:32 +02:00
Roberto Viola
58a9e81bd8 peloton bike setting removed 2025-08-25 10:10:38 +02:00
Roberto Viola
d78e92f42f Connected JTX Fitness elliptical trainer but no data in QZ fitness panel (Issue #3638) 2025-08-23 16:18:15 +02:00
Roberto Viola
2a5eb7b057 Connected JTX Fitness elliptical trainer but no data in QZ fitness panel (Issue #3638) 2025-08-23 14:58:48 +02:00
Roberto Viola
ae5f70645a Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-08-23 14:48:33 +02:00
Roberto Viola
d26b14276e How to Make 10s Intervals Work with Virtual Shifting (Discussion #3603) 2025-08-23 14:47:42 +02:00
Roberto Viola
9166ce7218 removing tester android 14 2025-08-23 14:39:10 +02:00
Roberto Viola
5f0ec98b0c Update main.yml 2025-08-23 12:17:15 +02:00
Roberto Viola
1bc7af0a88 fix ios github actions (#3637) 2025-08-23 09:25:42 +02:00
Roberto Viola
df75d33ca6 Fix SQL linking in GitHub Actions by adding sql module to defaults.pri (#3635) 2025-08-22 15:20:51 +02:00
Roberto Viola
34f7df6bfb Kettler HOI Frame Connectivity (Issue #3636) 2025-08-22 15:06:48 +02:00
Roberto Viola
1208b439fa Concept2 RowERG PM5 and QZ not getting metrocs (Issue #3625) (#3626) 2025-08-21 06:58:33 +02:00
Roberto Viola
14a9faa2ee apex bike cadence updated 2025-08-20 15:47:34 +02:00
61 changed files with 2534 additions and 1508 deletions

View File

@@ -416,7 +416,7 @@ jobs:
with:
version: '5.15.2'
host: 'linux'
modules: 'qtnetworkauth qtcharts qtsql'
modules: 'qtnetworkauth qtcharts'
cache: 'true'
cache-key-prefix: 'install-qt-action-linux'
@@ -655,7 +655,7 @@ jobs:
strategy:
fail-fast: false
matrix:
api-level: [24, 26, 28, 29, 30, 31, 33, 34, 35, 36]
api-level: [24, 26, 28, 29, 30, 31, 33, 35, 36]
include:
- api-level: 24
target: default
@@ -685,10 +685,6 @@ jobs:
target: google_apis
arch: x86_64
android-version: "Android 13"
- api-level: 34
target: google_apis
arch: x86_64
android-version: "Android 14"
- api-level: 35
target: google_apis
arch: x86_64
@@ -781,7 +777,7 @@ jobs:
adb shell am start -n org.cagnulen.qdomyoszwift/org.cagnulen.qdomyoszwift.CustomQtActivity
# Wait for app to start
sleep 60
sleep 90
# Verify the app is running
echo "Checking if app is running..."
@@ -842,7 +838,7 @@ jobs:
ios-build:
# The type of runner that the job will run on
runs-on: macos-latest
runs-on: macos-14
permissions:
contents: write
@@ -905,7 +901,7 @@ jobs:
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
cd ..
qmake CONFIG+=debug && make -j4
qmake CONFIG+=debug CONFIG+=iphonesimulator && make -j4
# causes iOS build on Mac to fail
# - name: Commit moc files
@@ -1578,6 +1574,16 @@ jobs:
# The type of runner that the job will run on
runs-on: ubuntu-22.04
if: github.event_name == 'schedule'
strategy:
matrix:
variant:
- name: treadmill
setting_key: nordictrack_2950_ip
setting_value: localhost
- name: bike
setting_key: tdf_10_ip
setting_value: localhost
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
@@ -1631,6 +1637,111 @@ jobs:
- name: download 3rd party files for qthttpserver
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
- name: Set Android NDK 21 && build
run: |
# Install NDK 21 after GitHub update
# https://github.com/actions/virtual-environments/issues/5595
ANDROID_ROOT="/usr/local/lib/android"
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
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 PELOTON_SECRET_KEY ${{ secrets.peloton_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 "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
# Set variant-specific IP setting
sed -i 's/property string ${{ matrix.variant.setting_key }}: ""/property string ${{ matrix.variant.setting_key }}: "${{ matrix.variant.setting_value }}"/' settings.qml
cd ..
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
# QTHTTPSERVER must use the same NDK
cd src/qthttpserver
qmake
make -j8
make install
cd ../..
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
sed -i 's/"android-debug"/"android-nordictrack-${{ matrix.variant.name }}"/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
sed -i 's/android-debug\.apk/android-debug-nordictrack-${{ matrix.variant.name }}.apk/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
cat src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
- name: Build APK (not usable for production due to unpatched QT library)
run: cd src; androiddeployqt --input android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
- name: Archive nordictrack binary
uses: actions/upload-artifact@v4
with:
name: nordictrack-${{ matrix.variant.name }}-android-trial
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
peloton-bike-plus-build:
# The type of runner that the job will run on
runs-on: ubuntu-22.04
if: github.event_name == 'schedule'
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Xvfb install and run
run: |
sudo apt-get install -y xvfb
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
- name: Checkout PR code
uses: actions/checkout@v3
with:
ref: refs/pull/3632/head
token: ${{ secrets.GITHUB_TOKEN }}
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
- name: Checkout submodules with specific branches
run: |
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
- 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
- name: Install Qt Android
uses: jdpurcell/install-qt-action@v5
with:
version: '5.15.0'
host: 'linux'
target: 'android'
arch: 'android'
modules: 'qtcharts qtnetworkauth'
dir: '${{ github.workspace }}/output/android/'
cache: 'true'
cache-key-prefix: 'install-qt-action-android'
- name: Install Java
uses: actions/setup-java@v3
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '11.0.23+9'
- 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: Set Android NDK 21 && build
run: |
# Install NDK 21 after GitHub update
@@ -1662,31 +1773,137 @@ jobs:
cd ../..
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-nordictrack-deployment-settings.json
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
sed -i 's/"android-debug"/"android-nordictrack"/g' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
sed -i 's/android-debug\.apk/android-debug-nordictrack.apk/g' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
cat src/android-qdomyos-zwift-nordictrack-deployment-settings.json
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
sed -i 's/"android-debug"/"android-peloton-bike-plus"/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
sed -i 's/android-debug\.apk/android-debug-peloton-bike-plus.apk/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
cat src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
- name: Build APK (not usable for production due to unpatched QT library)
run: cd src; androiddeployqt --input android-qdomyos-zwift-nordictrack-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack.apk
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
- name: Archive nordictrack binary
- name: Archive peloton-bike-plus binary
uses: actions/upload-artifact@v4
with:
name: nordictrack-android-trial
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack.apk
name: peloton-bike-plus-android-trial
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
peloton-bike-build:
# The type of runner that the job will run on
runs-on: ubuntu-22.04
if: github.event_name == 'schedule'
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Xvfb install and run
run: |
sudo apt-get install -y xvfb
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
- name: Checkout PR code
uses: actions/checkout@v3
with:
ref: refs/pull/3639/head
token: ${{ secrets.GITHUB_TOKEN }}
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
- name: Checkout submodules with specific branches
run: |
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
- 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
- name: Install Qt Android
uses: jdpurcell/install-qt-action@v5
with:
version: '5.15.0'
host: 'linux'
target: 'android'
arch: 'android'
modules: 'qtcharts qtnetworkauth'
dir: '${{ github.workspace }}/output/android/'
cache: 'true'
cache-key-prefix: 'install-qt-action-android'
- name: Install Java
uses: actions/setup-java@v3
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '11.0.23+9'
- 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: Set Android NDK 21 && build
run: |
# Install NDK 21 after GitHub update
# https://github.com/actions/virtual-environments/issues/5595
ANDROID_ROOT="/usr/local/lib/android"
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
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 PELOTON_SECRET_KEY ${{ secrets.peloton_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 "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
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
# QTHTTPSERVER must use the same NDK
cd src/qthttpserver
qmake
make -j8
make install
cd ../..
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
sed -i 's/"android-debug"/"android-peloton-bike"/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
sed -i 's/android-debug\.apk/android-debug-peloton-bike.apk/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
cat src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
- name: Build APK (not usable for production due to unpatched QT library)
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
- name: Archive peloton-bike binary
uses: actions/upload-artifact@v4
with:
name: peloton-bike-android-trial
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
upload_to_release:
permissions: write-all
runs-on: ubuntu-22.04
#if: github.event_name == 'schedule'
if: always()
needs: [linux-x86-build, window-msvc2019-build, window-msvc2022-build, ios-build, window-build, android-build, raspberry-pi-build]
if: github.event_name == 'schedule'
needs: [window-msvc2019-build, window-msvc2022-build, window-build, android-build, raspberry-pi-build, nordictrack-build, peloton-bike-plus-build, peloton-bike-build, raspberry-pi-build-and-image-64bit]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Download artifacts
uses: actions/download-artifact@v4
@@ -1695,9 +1912,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: nightly
tag_name: nightly-${{ steps.date.outputs.date }}
prerelease: false
name: 'QZ nightly build $$'
name: 'QZ nightly build - ${{ steps.date.outputs.date }}'
body: |
This is a nightly build of QZ.
@@ -1714,7 +1931,10 @@ jobs:
## Other Platforms:
- **fdroid-android-trial**: Android build
- **nordictrack-android-trial**: Nordictrack build for iFIT2 Tablets
- **nordictrack-treadmill-android-trial**: Nordictrack Treadmill build for iFIT2 Tablets
- **nordictrack-bike-android-trial**: Nordictrack Bike build for iFIT2 Tablets
- **peloton-bike-plus-android-trial**: Peloton Bike Plus build with Grupetto backend
- **peloton-bike-android-trial**: Peloton Bike build with Grupetto backend
- **raspberry-pi-binary**: Raspberry Pi build
__Please help us improve QZ by reporting any issues you encounter!__ :wink:
@@ -1727,6 +1947,9 @@ jobs:
windows-binary-no-python/*
windows-binary/*
fdroid-android-trial/android-debug.apk
nordictrack-android-trial/android-debug-nordictrack.apk
nordictrack-treadmill-android-trial/android-debug-nordictrack-treadmill.apk
nordictrack-bike-android-trial/android-debug-nordictrack-bike.apk
peloton-bike-plus-android-trial/android-debug-peloton-bike-plus.apk
peloton-bike-android-trial/android-debug-peloton-bike.apk
raspberry-pi-binary/qdomyos-zwift-32bit
raspberry-pi-binary/qdomyos-zwift-64bit

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
src/qdomyos-zwift.pro.user
.idea/
src/Makefile

View File

@@ -342,6 +342,8 @@
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9927BE35C4001D7645 /* bowflext216treadmill.cpp */; };
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */; };
876BFCA127BE35D8001D7645 /* moc_bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */; };
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */; };
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */; };
876E4E142594748000BD5714 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 876E4E132594748000BD5714 /* Assets.xcassets */; };
876E4E1B2594748000BD5714 /* watchkit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
876E4E202594748000BD5714 /* qdomyoszwiftApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876E4E1F2594748000BD5714 /* qdomyoszwiftApp.swift */; };
@@ -1298,6 +1300,9 @@
876BFC9B27BE35C5001D7645 /* proformelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformelliptical.h; path = ../src/devices/proformelliptical/proformelliptical.h; sourceTree = "<group>"; };
876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformelliptical.cpp; sourceTree = "<group>"; };
876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bowflext216treadmill.cpp; sourceTree = "<group>"; };
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitbackupwriter.h; path = ../src/fitbackupwriter.h; sourceTree = SOURCE_ROOT; };
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitbackupwriter.cpp; path = ../src/fitbackupwriter.cpp; sourceTree = SOURCE_ROOT; };
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitbackupwriter.cpp; sourceTree = "<group>"; };
876E4E112594747F00BD5714 /* watchkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = watchkit.app; sourceTree = BUILT_PRODUCTS_DIR; };
876E4E132594748000BD5714 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
876E4E152594748000BD5714 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -2273,6 +2278,9 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
8755E5D12E4E260B006A12E4 /* fontmanager.h */,
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */,
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */,
@@ -3944,6 +3952,8 @@
2B42755BF45173E11E2110CB /* FitFieldDefinition.mm in Compile Sources */,
873824AE27E64706004F1B46 /* moc_browser.cpp in Compile Sources */,
8738249727E646E3004F1B46 /* characteristicnotifier2a53.cpp in Compile Sources */,
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */,
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */,
DF373364C5474D877506CB26 /* FitMesg.mm in Compile Sources */,
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */,
872261F0289EA887006A6F75 /* moc_nordictrackelliptical.cpp in Compile Sources */,
@@ -4445,7 +4455,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1142;
CURRENT_PROJECT_VERSION = 1161;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4623,6 +4634,9 @@
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = "";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_INSTALL_OBJC_HEADER = YES;
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
@@ -4641,8 +4655,9 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1142;
CURRENT_PROJECT_VERSION = 1161;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = 0;
@@ -4822,6 +4837,9 @@
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = _debug;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_INSTALL_OBJC_HEADER = YES;
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
@@ -4873,7 +4891,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1142;
CURRENT_PROJECT_VERSION = 1161;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4969,7 +4987,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1142;
CURRENT_PROJECT_VERSION = 1161;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5061,7 +5079,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1142;
CURRENT_PROJECT_VERSION = 1161;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5177,7 +5195,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1142;
CURRENT_PROJECT_VERSION = 1161;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;

View File

@@ -23,6 +23,7 @@ class WatchKitConnection: NSObject {
static let shared = WatchKitConnection()
public static var distance = 0.0
public static var kcal = 0.0
public static var totalKcal = 0.0
public static var stepCadence = 0
public static var speed = 0.0
public static var cadence = 0.0
@@ -70,6 +71,9 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
WatchKitConnection.distance = dDistance
let dKcal = Double(result["kcal"] as! Double)
WatchKitConnection.kcal = dKcal
if let totalKcalDouble = result["totalKcal"] as? Double {
WatchKitConnection.totalKcal = totalKcalDouble
}
let dSpeed = Double(result["speed"] as! Double)
WatchKitConnection.speed = dSpeed

View File

@@ -28,6 +28,7 @@ class WorkoutTracking: NSObject {
static let shared = WorkoutTracking()
public static var distance = Double()
public static var kcal = Double()
public static var totalKcal = Double()
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
public static var cadenceLastSteps = Double()
public static var cadenceSteps = 0
@@ -166,6 +167,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
@@ -185,6 +187,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.workoutType()
])
}
@@ -223,25 +226,30 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
workoutSession.stopActivity(with: Date())
workoutSession.end()
guard let quantityType = HKQuantityType.quantityType(
// Write active calories
guard let activeQuantityType = HKQuantityType.quantityType(
forIdentifier: .activeEnergyBurned) else {
return
}
let unit = HKUnit.kilocalorie()
let totalEnergyBurned = WorkoutTracking.kcal
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let activeEnergyBurned = WorkoutTracking.kcal
let activeQuantity = HKQuantity(unit: unit,
doubleValue: activeEnergyBurned)
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: startDate,
end: Date())
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
quantity: activeQuantity,
start: startDate,
end: Date())
workoutBuilder.add([sample]) {(success, error) in}
workoutBuilder.add([activeSample]) {(success, error) in
if let error = error {
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
}
}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance
let quantityMiles = HKQuantity(unit: unitDistance,
@@ -273,6 +281,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}
@@ -334,6 +346,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}
@@ -399,6 +415,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}

View File

@@ -1,4 +1,4 @@
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia sql
QTPLUGIN += qavfmediaplayer
QT+= charts

View File

@@ -9,6 +9,7 @@ ColumnLayout {
anchors.fill: parent
Settings {
id: settings
property int chart_display_mode: 0
}
WebView {
id: webView
@@ -19,6 +20,9 @@ ColumnLayout {
if (loadRequest.errorString) {
console.error(loadRequest.errorString);
console.error("port " + settings.value("template_inner_QZWS_port"));
} else if (loadRequest.status === WebView.LoadSucceededStatus) {
// Send chart display mode to the web view
sendDisplayModeToWebView();
}
}
onVisibleChanged: {
@@ -28,4 +32,22 @@ ColumnLayout {
}
}
}
// Watch for changes in chart display mode setting
Connections {
target: settings
function onChart_display_modeChanged() {
sendDisplayModeToWebView();
}
}
function sendDisplayModeToWebView() {
if (webView.loading === false) {
webView.runJavaScript("
if (window.setChartDisplayMode) {
window.setChartDisplayMode(" + settings.chart_display_mode + ");
}
");
}
}
}

View File

@@ -13,22 +13,32 @@ ColumnLayout {
signal trainprogram_open_clicked(url name)
signal trainprogram_open_other_folder(url name)
signal trainprogram_preview(url name)
FileDialog {
id: fileDialogTrainProgram
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
} else {
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialog {
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
console.log("You chose: " + fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileUrl)
} else {
trainprogram_open_clicked(fileUrl)
}
close()
// Destroy and recreate the dialog for next use
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
// Destroy the dialog
fileDialogLoader.active = false
}
}
fileDialogTrainProgram.close()
}
onRejected: {
console.log("Canceled")
fileDialogTrainProgram.close()
}
}
@@ -263,7 +273,8 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
console.log("folder is " + rootItem.getWritableAppDir() + 'gpx')
fileDialogTrainProgram.visible = true
// Create a fresh FileDialog instance
fileDialogLoader.active = true
}
anchors {
bottom: parent.bottom

View File

@@ -7,18 +7,28 @@ import QtQuick.Dialogs 1.0
ColumnLayout {
signal loadSettings(url name)
FileDialog {
id: fileDialogSettings
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialogSettings.fileUrl)
loadSettings(fileDialogSettings.fileUrl)
fileDialogSettings.close()
}
onRejected: {
console.log("Canceled")
fileDialogSettings.close()
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialog {
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
console.log("You chose: " + fileUrl)
loadSettings(fileUrl)
close()
// Destroy and recreate the dialog for next use
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
// Destroy the dialog
fileDialogLoader.active = false
}
}
}
}
@@ -106,7 +116,8 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
console.log("folder is " + rootItem.getWritableAppDir() + 'settings')
fileDialogSettings.visible = true
// Create a fresh FileDialog instance
fileDialogLoader.active = true
}
anchors {
bottom: parent.bottom

View File

@@ -11,22 +11,32 @@ ColumnLayout {
signal trainprogram_open_clicked(url name)
signal trainprogram_open_other_folder(url name)
signal trainprogram_preview(url name)
FileDialog {
id: fileDialogTrainProgram
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
} else {
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialog {
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
console.log("You chose: " + fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileUrl)
} else {
trainprogram_open_clicked(fileUrl)
}
close()
// Destroy and recreate the dialog for next use
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
// Destroy the dialog
fileDialogLoader.active = false
}
}
fileDialogTrainProgram.close()
}
onRejected: {
console.log("Canceled")
fileDialogTrainProgram.close()
}
}
@@ -296,7 +306,8 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
console.log("folder is " + rootItem.getWritableAppDir() + 'training')
fileDialogTrainProgram.visible = true
// Create a fresh FileDialog instance
fileDialogLoader.active = true
}
anchors {
bottom: parent.bottom

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.20.8" android:versionCode="1145" 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.20.11" android:versionCode="1155" 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 -->

View File

@@ -25,6 +25,7 @@ import android.widget.Toast;
import org.cagnulen.qdomyoszwift.QLog;
import android.content.Intent;
public class Ant {
private ChannelService.ChannelServiceComm mChannelService = null;
private boolean mChannelServiceBound = false;
@@ -33,23 +34,27 @@ public class Ant {
static boolean speedRequest = false;
static boolean heartRequest = false;
static boolean bikeRequest = false; // Added bike request flag
static boolean remoteControlRequest = false; // Added remote control request flag
static boolean garminKey = false;
static boolean treadmill = false;
static boolean technoGymGroupCycle = false;
static int antBikeDeviceNumber = 0;
static int antHeartDeviceNumber = 0;
static int antRemoteControlDeviceNumber = 0; // Added remote control device number
// Updated antStart method with BikeRequest parameter at the end
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber) {
// Updated antStart method with RemoteControlRequest parameter at the end
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber, boolean RemoteControlRequest, int AntRemoteControlDeviceNumber) {
QLog.v(TAG, "antStart");
speedRequest = SpeedRequest;
heartRequest = HeartRequest;
treadmill = Treadmill;
garminKey = GarminKey;
bikeRequest = BikeRequest; // Set bike request flag
remoteControlRequest = RemoteControlRequest; // Set remote control request flag
technoGymGroupCycle = TechnoGymGroupCycle;
antBikeDeviceNumber = AntBikeDeviceNumber;
antHeartDeviceNumber = AntHeartDeviceNumber;
antRemoteControlDeviceNumber = AntRemoteControlDeviceNumber;
activity = a;
if(a != null)
QLog.v(TAG, "antStart activity is valid");
@@ -159,14 +164,29 @@ public class Ant {
return mChannelService.isBikeConnected();
}
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
if(mChannelService == null)
return;
QLog.v(TAG, "updateBikeTransmitterExtendedMetrics");
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
elapsedTimeSeconds, resistance,
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
elapsedTimeSeconds, resistance,
inclination);
}
// Remote Control methods
public boolean isRemoteControlConnected() {
if(mChannelService == null)
return false;
QLog.v(TAG, "isRemoteControlConnected");
return mChannelService.isRemoteControlConnected();
}
public void setRemoteControlDeviceNumber(int deviceNumber) {
if(mChannelService == null)
return;
QLog.v(TAG, "setRemoteControlDeviceNumber: " + deviceNumber);
mChannelService.setRemoteControlDeviceNumber(deviceNumber);
}
}

View File

@@ -0,0 +1,438 @@
/*
* ANT+ Remote Control implementation for QDomyos-Zwift
* Based on Golden Cheetah RemoteControl implementation
* Maps ANT+ remote control commands to workout controls
*/
package org.cagnulen.qdomyoszwift;
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.AcknowledgedDataMessage;
import com.dsi.ant.message.fromant.BroadcastDataMessage;
import com.dsi.ant.message.fromant.ChannelEventMessage;
import com.dsi.ant.message.fromant.MessageFromAntType;
import com.dsi.ant.message.ipc.AntMessageParcel;
import android.os.RemoteException;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
/**
* ANT+ Remote Control Channel Controller
* Handles ANT+ Control Device Profile (Device Type 0x10)
*/
public class AntRemoteControl {
private static final String TAG = "AntRemoteControl";
// ANT+ Control Device Profile constants
private static final int CONTROL_DEVICE_TYPE = 0x10;
private static final int CONTROL_TRANSMISSION_TYPE = 0x05;
private static final short CONTROL_PERIOD = 8192; // 4 Hz
private static final int CONTROL_FREQUENCY = 57; // 2457 MHz
// ANT+ Control Generic Command Page
private static final byte ANT_CONTROL_GENERIC_CMD_PAGE = 0x49;
// ANT+ Generic Commands (from Golden Cheetah)
private static final int ANT_CONTROL_GENERIC_CMD_MENU_UP = 0x00;
private static final int ANT_CONTROL_GENERIC_CMD_MENU_DOWN = 0x01;
private static final int ANT_CONTROL_GENERIC_CMD_MENU_SELECT = 0x02;
private static final int ANT_CONTROL_GENERIC_CMD_MENU_BACK = 0x03;
private static final int ANT_CONTROL_GENERIC_CMD_HOME = 0x04;
private static final int ANT_CONTROL_GENERIC_CMD_START = 0x20;
private static final int ANT_CONTROL_GENERIC_CMD_STOP = 0x21;
private static final int ANT_CONTROL_GENERIC_CMD_RESET = 0x22;
private static final int ANT_CONTROL_GENERIC_CMD_LENGTH = 0x23;
private static final int ANT_CONTROL_GENERIC_CMD_LAP = 0x24;
private static final int ANT_CONTROL_GENERIC_CMD_USER_1 = 0x8000;
private static final int ANT_CONTROL_GENERIC_CMD_USER_2 = 0x8001;
private static final int ANT_CONTROL_GENERIC_CMD_USER_3 = 0x8002;
private AntChannel mAntChannel;
private ChannelController mChannelController;
private boolean isChannelOpen = false;
private int deviceNumber = 0; // 0 means wildcard - accept any remote
// Native methods for communicating with Qt layer
public static native void nativeOnRemoteCommand(int command);
public static native void nativeGearUp();
public static native void nativeGearDown();
/**
* Channel Controller for handling ANT+ events
*/
private class ChannelController implements IAntChannelEventHandler {
@Override
public void onChannelDeath() {
QLog.w(TAG, "onChannelDeath: Remote Control Channel Death - cleaning up");
isChannelOpen = false;
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.v(TAG, "onReceiveMessage: messageType=" + messageType + ", parcel=" + antParcel);
switch(messageType) {
case ACKNOWLEDGED_DATA:
QLog.d(TAG, "onReceiveMessage: Received ACKNOWLEDGED_DATA");
AcknowledgedDataMessage ackMsg = new AcknowledgedDataMessage(antParcel);
byte[] ackPayload = ackMsg.getPayload();
QLog.v(TAG, "onReceiveMessage: ACKNOWLEDGED_DATA payload length=" + (ackPayload != null ? ackPayload.length : 0));
handleDataMessage(ackPayload);
break;
case BROADCAST_DATA:
QLog.d(TAG, "onReceiveMessage: Received BROADCAST_DATA");
BroadcastDataMessage broadcastMsg = new BroadcastDataMessage(antParcel);
byte[] broadcastPayload = broadcastMsg.getPayload();
QLog.v(TAG, "onReceiveMessage: BROADCAST_DATA payload length=" + (broadcastPayload != null ? broadcastPayload.length : 0));
handleDataMessage(broadcastPayload);
break;
case CHANNEL_EVENT:
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "onReceiveMessage: CHANNEL_EVENT - eventCode=" + code);
switch(code) {
/* case CHANNEL_IN_WRONG_STATE:
QLog.w(TAG, "onReceiveMessage: CHANNEL_IN_WRONG_STATE error");
break;*/
case CHANNEL_COLLISION:
QLog.w(TAG, "onReceiveMessage: CHANNEL_COLLISION error");
break;
case TRANSFER_TX_FAILED:
QLog.w(TAG, "onReceiveMessage: TRANSFER_TX_FAILED error");
break;
case RX_SEARCH_TIMEOUT:
QLog.i(TAG, "onReceiveMessage: RX_SEARCH_TIMEOUT - no remote control found");
break;
case CHANNEL_CLOSED:
QLog.i(TAG, "onReceiveMessage: CHANNEL_CLOSED");
isChannelOpen = false;
break;
default:
QLog.v(TAG, "onReceiveMessage: Other channel event=" + code);
break;
}
break;
default:
QLog.d(TAG, "onReceiveMessage: Unhandled messageType=" + messageType + ", parcel=" + antParcel);
break;
}
}
/**
* Handle incoming data messages from ANT+ remote control
*/
private void handleDataMessage(byte[] payload) {
QLog.v(TAG, "handleDataMessage: called with payload=" + (payload != null ? "length=" + payload.length : "null"));
if (payload == null) {
QLog.w(TAG, "handleDataMessage: payload is null, ignoring");
return;
}
if (payload.length < 8) {
QLog.w(TAG, "handleDataMessage: payload too short (length=" + payload.length + "), expected 8 bytes");
return;
}
// Log raw payload for debugging
StringBuilder payloadHex = new StringBuilder();
for (int i = 0; i < payload.length; i++) {
payloadHex.append(String.format("%02X ", payload[i]));
}
QLog.v(TAG, "handleDataMessage: raw payload: " + payloadHex.toString());
// Check if this is a Generic Command Page
byte pageNumber = payload[0];
QLog.d(TAG, "handleDataMessage: pageNumber=0x" + Integer.toHexString(pageNumber & 0xFF) +
" (expected=0x" + Integer.toHexString(ANT_CONTROL_GENERIC_CMD_PAGE & 0xFF) + ")");
if (pageNumber != ANT_CONTROL_GENERIC_CMD_PAGE) {
QLog.w(TAG, "handleDataMessage: not a Generic Command Page, ignoring (pageNumber=0x" +
Integer.toHexString(pageNumber & 0xFF) + ")");
return;
}
// Extract command from payload
// Command is in bytes 1-2 (little endian)
int command = ((payload[2] & 0xFF) << 8) | (payload[1] & 0xFF);
QLog.i(TAG, "handleDataMessage: extracted ANT+ Remote Command=0x" + Integer.toHexString(command) +
" from bytes[1]=0x" + Integer.toHexString(payload[1] & 0xFF) +
", bytes[2]=0x" + Integer.toHexString(payload[2] & 0xFF));
// Map commands to actions
handleRemoteCommand(command);
}
/**
* Handle remote control commands and map them to QDomyos-Zwift actions
*/
private void handleRemoteCommand(int command) {
QLog.d(TAG, "handleRemoteCommand: processing command=0x" + Integer.toHexString(command));
switch(command) {
case ANT_CONTROL_GENERIC_CMD_MENU_UP:
QLog.i(TAG, "handleRemoteCommand: MENU_UP -> Gear Up (like Zwift Click)");
try {
nativeGearUp();
QLog.d(TAG, "handleRemoteCommand: nativeGearUp() called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeGearUp()", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_MENU_DOWN:
QLog.i(TAG, "handleRemoteCommand: MENU_DOWN -> Gear Down (like Zwift Click)");
try {
nativeGearDown();
QLog.d(TAG, "handleRemoteCommand: nativeGearDown() called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeGearDown()", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_MENU_SELECT:
QLog.i(TAG, "handleRemoteCommand: MENU_SELECT -> Select action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(SELECT) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(SELECT)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_MENU_BACK:
QLog.i(TAG, "handleRemoteCommand: MENU_BACK -> Back action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(BACK) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(BACK)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_HOME:
QLog.i(TAG, "handleRemoteCommand: HOME -> Home action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(HOME) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(HOME)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_START:
QLog.i(TAG, "handleRemoteCommand: START -> Start workout");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(START) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(START)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_STOP:
QLog.i(TAG, "handleRemoteCommand: STOP -> Stop workout");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(STOP) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(STOP)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_LAP:
QLog.i(TAG, "handleRemoteCommand: LAP -> Lap marker");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(LAP) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(LAP)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_RESET:
QLog.i(TAG, "handleRemoteCommand: RESET -> Reset action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(RESET) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(RESET)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_USER_1:
case ANT_CONTROL_GENERIC_CMD_USER_2:
case ANT_CONTROL_GENERIC_CMD_USER_3:
QLog.i(TAG, "handleRemoteCommand: USER_" + (command - ANT_CONTROL_GENERIC_CMD_USER_1 + 1) + " -> User action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(USER) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(USER)", e);
}
break;
default:
QLog.w(TAG, "handleRemoteCommand: Unknown/unmapped command=0x" + Integer.toHexString(command));
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(UNKNOWN) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(UNKNOWN)", e);
}
break;
}
}
}
/**
* Constructor
*/
public AntRemoteControl() {
QLog.i(TAG, "AntRemoteControl: constructor - initializing ANT+ Remote Control");
mChannelController = new ChannelController();
QLog.d(TAG, "AntRemoteControl: constructor completed, channel controller created");
}
/**
* Open ANT+ remote control channel
*/
public boolean openChannel(AntChannel antChannel) {
QLog.i(TAG, "openChannel: request to open ANT+ Remote Control channel");
if (isChannelOpen) {
QLog.w(TAG, "openChannel: Remote control channel already open, ignoring request");
return false;
}
if (antChannel == null) {
QLog.e(TAG, "openChannel: antChannel is null, cannot proceed");
return false;
}
mAntChannel = antChannel;
QLog.d(TAG, "openChannel: antChannel assigned");
try {
// Configure the channel
ChannelId channelId = new ChannelId(deviceNumber, CONTROL_DEVICE_TYPE, CONTROL_TRANSMISSION_TYPE);
QLog.d(TAG, "openChannel: created ChannelId - deviceNumber=" + deviceNumber +
", deviceType=0x" + Integer.toHexString(CONTROL_DEVICE_TYPE) +
", transmissionType=0x" + Integer.toHexString(CONTROL_TRANSMISSION_TYPE));
QLog.i(TAG, "openChannel: configuring ANT+ Remote Control channel" +
" (deviceNumber=" + deviceNumber + ", frequency=" + CONTROL_FREQUENCY +
"MHz, period=" + CONTROL_PERIOD + ")");
// Assign the channel with slave configuration
QLog.d(TAG, "openChannel: assigning channel as SLAVE_RECEIVE_ONLY");
mAntChannel.assign(ChannelType.SLAVE_RECEIVE_ONLY);
// Set the channel ID
QLog.d(TAG, "openChannel: setting channel ID");
mAntChannel.setChannelId(channelId);
// Set the period
QLog.d(TAG, "openChannel: setting period=" + CONTROL_PERIOD);
mAntChannel.setPeriod(CONTROL_PERIOD);
// Set the RF frequency
QLog.d(TAG, "openChannel: setting RF frequency=" + CONTROL_FREQUENCY);
mAntChannel.setRfFrequency(CONTROL_FREQUENCY);
// Register event handler
QLog.d(TAG, "openChannel: registering channel event handler");
mAntChannel.setChannelEventHandler(mChannelController);
// Open the channel
QLog.d(TAG, "openChannel: opening the channel");
mAntChannel.open();
isChannelOpen = true;
QLog.i(TAG, "openChannel: ANT+ Remote Control channel opened successfully, now listening for commands");
return true;
} catch (RemoteException e) {
QLog.e(TAG, "openChannel: RemoteException while opening ANT+ Remote Control channel", e);
isChannelOpen = false;
return false;
} catch (AntCommandFailedException e) {
QLog.e(TAG, "openChannel: AntCommandFailedException while opening ANT+ Remote Control channel", e);
isChannelOpen = false;
return false;
}
}
/**
* Close ANT+ remote control channel
*/
public void closeChannel() {
QLog.i(TAG, "closeChannel: request to close ANT+ Remote Control channel");
if (!isChannelOpen && mAntChannel == null) {
QLog.d(TAG, "closeChannel: channel not open and null, nothing to do");
return;
}
if (mAntChannel == null) {
QLog.w(TAG, "closeChannel: channel marked as open but mAntChannel is null, clearing flag");
isChannelOpen = false;
return;
}
try {
QLog.d(TAG, "closeChannel: closing ANT+ Remote Control channel");
mAntChannel.close();
QLog.d(TAG, "closeChannel: channel closed, releasing resources");
mAntChannel.release();
QLog.i(TAG, "closeChannel: ANT+ Remote Control channel closed and released successfully");
} catch (RemoteException e) {
QLog.e(TAG, "closeChannel: RemoteException while closing ANT+ Remote Control channel", e);
} catch (AntCommandFailedException e) {
QLog.e(TAG, "closeChannel: AntCommandFailedException while closing ANT+ Remote Control channel", e);
}
isChannelOpen = false;
mAntChannel = null;
QLog.d(TAG, "closeChannel: cleanup completed");
}
/**
* Check if channel is open
*/
public boolean isChannelOpen() {
boolean channelOpen = isChannelOpen;
QLog.v(TAG, "isChannelOpen: returning " + channelOpen);
return channelOpen;
}
/**
* Set the device number to search for (0 = wildcard)
*/
public void setDeviceNumber(int deviceNumber) {
QLog.d(TAG, "setDeviceNumber: changing deviceNumber from " + this.deviceNumber + " to " + deviceNumber);
if (deviceNumber == 0) {
QLog.i(TAG, "setDeviceNumber: using wildcard (0) to accept any remote control");
} else {
QLog.i(TAG, "setDeviceNumber: will only accept remote control with device number " + deviceNumber);
}
this.deviceNumber = deviceNumber;
}
/**
* Get the current device number
*/
public int getDeviceNumber() {
QLog.v(TAG, "getDeviceNumber: returning " + deviceNumber);
return deviceNumber;
}
}

View File

@@ -59,6 +59,7 @@ public class ChannelService extends Service {
SDMChannelController sdmChannelController = null;
BikeChannelController bikeChannelController = null; // Added BikeChannelController reference
BikeTransmitterController bikeTransmitterController = null; // Added BikeTransmitterController reference
AntRemoteControl antRemoteControl = null; // Added AntRemoteControl reference
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
@Override
@@ -307,6 +308,41 @@ public class ChannelService extends Service {
return "Bike transmitter not initialized";
}
// ========== REMOTE CONTROL METHODS ==========
/**
* Check if remote control is connected
*/
boolean isRemoteControlConnected() {
QLog.v(TAG, "ChannelServiceComm.isRemoteControlConnected");
return (antRemoteControl != null && antRemoteControl.isChannelOpen());
}
/**
* Set the remote control device number (0 = wildcard)
*/
void setRemoteControlDeviceNumber(int deviceNumber) {
QLog.v(TAG, "ChannelServiceComm.setRemoteControlDeviceNumber: " + deviceNumber);
if (antRemoteControl != null) {
antRemoteControl.setDeviceNumber(deviceNumber);
QLog.d(TAG, "Remote control device number updated to: " + deviceNumber);
} else {
QLog.w(TAG, "Remote control not initialized, cannot set device number");
}
}
/**
* Get remote control status info for debugging
*/
String getRemoteControlInfo() {
if (antRemoteControl != null) {
return "Remote control initialized, channel open: " + antRemoteControl.isChannelOpen() +
", device number: " + antRemoteControl.getDeviceNumber();
}
return "Remote control not initialized";
}
/**
* Closes all channels currently added.
*/
@@ -393,6 +429,37 @@ public class ChannelService extends Service {
bikeTransmitterController = null;
}
}
// Add initialization for AntRemoteControl
if (Ant.remoteControlRequest && antRemoteControl == null) {
QLog.i(TAG, "Initializing AntRemoteControl");
try {
// Create remote control instance
antRemoteControl = new AntRemoteControl();
// Set device number (0 = wildcard for any remote)
antRemoteControl.setDeviceNumber(Ant.antRemoteControlDeviceNumber);
// Acquire channel like other controllers
AntChannel remoteChannel = acquireChannel();
if (remoteChannel != null) {
boolean channelOpened = antRemoteControl.openChannel(remoteChannel);
if (channelOpened) {
QLog.i(TAG, "AntRemoteControl initialized and channel opened successfully");
} else {
QLog.e(TAG, "Failed to open AntRemoteControl channel");
antRemoteControl = null;
}
} else {
QLog.e(TAG, "Failed to acquire channel for AntRemoteControl");
antRemoteControl = null;
}
} catch (Exception e) {
QLog.e(TAG, "Failed to initialize AntRemoteControl: " + e.getMessage(), e);
antRemoteControl = null;
}
}
}
private void closeAllChannels() {
@@ -409,6 +476,9 @@ public class ChannelService extends Service {
if (bikeTransmitterController != null) { // Added closing bikeTransmitterController
bikeTransmitterController.close(); // Use close() method like other controllers
}
if (antRemoteControl != null) { // Added closing antRemoteControl
antRemoteControl.closeChannel();
}
heartChannelController = null;
powerChannelController = null;
@@ -416,6 +486,7 @@ public class ChannelService extends Service {
sdmChannelController = null;
bikeChannelController = null; // Added nullifying bikeChannelController
bikeTransmitterController = null; // Added nullifying bikeTransmitterController
antRemoteControl = null; // Added nullifying antRemoteControl
}
AntChannel acquireChannel() throws ChannelNotAvailableException {

View File

@@ -14,7 +14,7 @@ CharacteristicWriteProcessor2AD9::CharacteristicWriteProcessor2AD9(double bikeRe
int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
if (data.size()) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::BIKE) {
if (dt == bluetoothdevice::BIKE || dt == bluetoothdevice::ROWING) {
QSettings settings;
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)

View File

@@ -152,7 +152,15 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
emit resistanceRead(Resistance.value());
m_pelotonResistance = Resistance.value();
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value());
// Parse cadence from 5th byte (index 4) and multiply by 2
uint8_t rawCadence = newValue.at(4);
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = rawCadence * 2;
}
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value()) + QStringLiteral(", Raw cadence: ") + QString::number(rawCadence) + QStringLiteral(", Final cadence: ") + QString::number(Cadence.value());
}
if (newValue.length() != 10 || newValue.at(2) != 0x31) {
@@ -162,41 +170,13 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
uint16_t distanceData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
double distance = ((double)distanceData);
if(distance != lastDistance) {
if(lastDistance != 0) {
double deltaDistance = distance - lastDistance;
double deltaTime = fabs(now.msecsTo(lastTS));
double timeHours = deltaTime / (1000.0 * 60.0 * 60.0);
double k = 0.005333;
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = (deltaDistance *k) / timeHours; // Speed in distance units per hour
} else {
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = Speed.value() / 0.37497622;
}
}
lastDistance = distance;
lastTS = now;
qDebug() << "lastDistance" << lastDistance << "lastTS" << lastTS;
// Calculate speed using the same method as echelon bike
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = 0.37497622 * ((double)Cadence.value());
} else {
// Check if speed and cadence should be reset due to timeout (2 seconds)
if (lastTS.msecsTo(now) > 2000) {
if (Speed.value() > 0) {
Speed = 0;
qDebug() << "Speed reset to 0 due to timeout";
}
if (Cadence.value() > 0) {
Cadence = 0;
qDebug() << "Cadence reset to 0 due to timeout";
}
lastTS = now;
}
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
if (watts())

View File

@@ -70,6 +70,11 @@ void bike::changePower(int32_t power) {
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
double erg_filter_lower =
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
// Apply bike power offset
int bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toInt();
power += bike_power_offset;
qDebug() << QStringLiteral("changePower: original power with offset applied: ") + QString::number(power) + QStringLiteral(" (offset: ") + QString::number(bike_power_offset) + QStringLiteral(")");
requestPower = power; // used by some bikes that have ERG mode builtin

View File

@@ -1489,10 +1489,14 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
(b.name().toUpper().startsWith("SCHWINN 810")) ||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
b.name().toUpper().startsWith(QStringLiteral("FOCUS M3")) ||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
b.name().toUpper().startsWith(QStringLiteral("KICKR RUN")) ||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
(b.name().toUpper().startsWith(QStringLiteral("KS-AP-"))) || // Kingsmith WalkingPad R3 Hybrid+
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("TT8")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().toUpper().startsWith(QStringLiteral("ST90")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
@@ -1747,6 +1751,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("SL010-")) ||
(b.name().toUpper().startsWith("EXPERT-SX9")) ||
(b.name().toUpper().startsWith("MRK-S26S-")) ||
(b.name().toUpper().startsWith("MRK-S26C-")) ||
(b.name().toUpper().startsWith("ROBX")) ||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
(b.name().toUpper().startsWith("XCX-")) ||
@@ -1778,6 +1783,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith("KICKR ROLLR") ||
b.name().toUpper().startsWith("KICKR CORE") ||
(b.name().toUpper().startsWith("KICKR MOVE ")) ||
(b.name().toUpper().startsWith("HOI FRAME ")) ||
(b.name().toUpper().startsWith("HAMMER ") && saris_trainer) ||
(b.name().toUpper().startsWith("WAHOO KICKR"))) &&
!wahooKickrSnapBike && !ftmsBike && filter) {
@@ -1900,6 +1906,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) ||
b.name().toUpper().startsWith(QStringLiteral("YOROTO-RW-")) ||
b.name().toUpper().startsWith(QStringLiteral("SF-RW")) ||
b.name().toUpper().startsWith(QStringLiteral("NORDLYS")) ||
b.name().toUpper().startsWith(QStringLiteral("ROWER ")) ||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
b.name().toUpper().startsWith(QStringLiteral("DFIT-L-R")) ||

View File

@@ -109,7 +109,52 @@ QTime bluetoothdevice::maxPace() {
double bluetoothdevice::odometerFromStartup() { return Distance.valueRaw(); }
double bluetoothdevice::odometer() { return Distance.value(); }
double bluetoothdevice::lapOdometer() { return Distance.lapValue(); }
metric bluetoothdevice::calories() { return KCal; }
metric bluetoothdevice::calories() {
QSettings settings;
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
if (fromHR && Heart.value() > 0) {
// Calculate calories based on heart rate
double totalHRKCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value());
hrKCal.setValue(totalHRKCal);
if (activeOnly) {
activeKCal.setValue(metric::calculateActiveKCal(hrKCal.value(), elapsed.value()));
return activeKCal;
} else {
return hrKCal;
}
} else {
// Power-based calculation (current behavior)
if (activeOnly) {
activeKCal.setValue(metric::calculateActiveKCal(KCal.value(), elapsed.value()));
return activeKCal;
} else {
return KCal;
}
}
}
metric bluetoothdevice::totalCalories() {
QSettings settings;
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
if (fromHR && Heart.value() > 0) {
return hrKCal; // Return HR-based total calories
} else {
return KCal; // Return power-based total calories
}
}
metric bluetoothdevice::activeCalories() {
return activeKCal;
}
metric bluetoothdevice::hrCalories() {
return hrKCal;
}
metric bluetoothdevice::jouls() { return m_jouls; }
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
@@ -254,7 +299,17 @@ void bluetoothdevice::update_hr_from_external() {
#ifndef IO_UNDER_QT
lockscreen h;
long appleWatchHeartRate = h.heartRate();
h.setKcal(KCal.value());
QSettings settings;
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
if (activeOnly) {
// When active calories setting is enabled, send both total and active calories
h.setKcal(calories().value()); // This will be active calories
h.setTotalKcal(totalCalories().value()); // This will be total calories
} else {
// When disabled, send total calories as before
h.setKcal(calories().value()); // This will be total calories
}
h.setDistance(Distance.value());
h.setSpeed(Speed.value());
h.setPower(m_watt.value());
@@ -277,7 +332,7 @@ void bluetoothdevice::update_hr_from_external() {
double kcal = calories().value();
if(kcal < 0)
kcal = 0;
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0);
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value());
#endif
#endif
}
@@ -288,6 +343,8 @@ void bluetoothdevice::clearStats() {
moving.clear(true);
Speed.clear(false);
KCal.clear(true);
hrKCal.clear(true);
activeKCal.clear(true);
Distance.clear(true);
Distance1s.clear(true);
Heart.clear(false);
@@ -313,6 +370,8 @@ void bluetoothdevice::setPaused(bool p) {
elapsed.setPaused(p);
Speed.setPaused(p);
KCal.setPaused(p);
hrKCal.setPaused(p);
activeKCal.setPaused(p);
Distance.setPaused(p);
Distance1s.setPaused(p);
Heart.setPaused(p);
@@ -336,6 +395,8 @@ void bluetoothdevice::setLap() {
elapsed.setLap(true);
Speed.setLap(false);
KCal.setLap(true);
hrKCal.setLap(true);
activeKCal.setLap(true);
Distance.setLap(true);
Distance1s.setLap(true);
Heart.setLap(false);

View File

@@ -108,11 +108,19 @@ class bluetoothdevice : public QObject {
/**
* @brief calories Gets a metric object to get and set the amount of energy expended.
* Default implementation returns the protected KCal property. Units: kcal
* Default implementation returns the protected KCal property, potentially adjusted for active calories. Units: kcal
* Other implementations could have different units.
* @return
*/
virtual metric calories();
virtual metric activeCalories();
virtual metric hrCalories();
/**
* @brief totalCalories Gets total calories (including BMR) regardless of active calories setting.
* @return Total calories metric
*/
virtual metric totalCalories();
/**
* @brief jouls Gets a metric object to get and set the number of joules expended. Units: joules
@@ -548,6 +556,8 @@ class bluetoothdevice : public QObject {
* @brief KCal The number of kilocalories expended in the session. Units: kcal
*/
metric KCal;
metric activeKCal;
metric hrKCal;
/**
* @brief Speed The simulated speed of the device. Units: km/h

View File

@@ -150,6 +150,11 @@ void echelonconnectsport::update() {
initDone) {
update_metrics(true, watts());
// Continuous ERG mode support - recalculate resistance as cadence changes when using power zone tiles
if (RequestedPower.value() > 0) {
changePower(RequestedPower.value());
}
// sending poll every 2 seconds
if (sec1Update++ >= (2000 / refresh->interval())) {
sec1Update = 0;

View File

@@ -130,7 +130,7 @@ void ftmsbike::init() {
if (initDone)
return;
if(ICSE) {
if(ICSE || HAMMER) {
uint8_t write[] = {FTMS_REQUEST_CONTROL};
bool ret = writeCharacteristic(write, sizeof(write), "requestControl", false, true);
write[0] = {FTMS_RESET};
@@ -194,7 +194,7 @@ void ftmsbike::zwiftPlayInit() {
}
void ftmsbike::forcePower(int16_t requestPower) {
if(resistance_lvl_mode || TITAN_7000) {
if((resistance_lvl_mode || TITAN_7000) && !MAGNUS) {
forceResistance(resistanceFromPowerRequest(requestPower));
} else {
uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00};
@@ -401,8 +401,15 @@ void ftmsbike::update() {
lastGearValue = gears();
// if a classic request of power from zwift or any other platform is coming, will be transfereed on the ftmsCharacteristicChanged applying the gear mod too
if (requestPower != -1 && (!virtualBike || !virtualBike->ftmsDeviceConnected() || (zwiftPlayService != nullptr && gears_zwift_ratio))) {
// Power request routing logic:
// 1. No virtualBike: route directly to bike
// 2. VirtualBike not connected to FTMS: route directly to bike
// 3. ZwiftPlay with gear ratio: route directly to bike
// 4. ErgMode supported + power sensor: use delta power system (bypass FTMS routing)
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"));
if (requestPower != -1 && (!virtualBike || !virtualBike->ftmsDeviceConnected() || (zwiftPlayService != nullptr && gears_zwift_ratio) || (ergModeSupported && power_sensor))) {
qDebug() << QStringLiteral("writing power") << requestPower;
init();
forcePower(requestPower);
@@ -656,14 +663,22 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
if(DU30_bike) {
m_watt = wattsFromResistance(Resistance.value());
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
} else if (MRK_S26C) {
m_watt = Cadence.value() * (Resistance.value() * 1.16);
emit debug(QStringLiteral("Current Watt (MRK-S26C formula): ") + QString::number(m_watt.value()));
} else if (LYDSTO && watt_ignore_builtin) {
m_watt = wattFromHR(true);
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
} else if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled")))
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
} else {
double ftms_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
m_watt = ftms_watt; // Only update watt if no external power sensor
}
}
index += 2;
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
} else if(DOMYOS) {
@@ -741,14 +756,15 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
uint16_t time_division = 1024;
uint8_t index = 4;
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
if (newValue.length() > 3) {
m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
if (newValue.length() > 3) {
double ftms_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
m_watt = ftms_watt; // Only update watt if no external power sensor
emit powerChanged(m_watt.value());
}
emit powerChanged(m_watt.value());
emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value()));
}
@@ -1036,11 +1052,14 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
}
if (Flags.instantPower) {
double ftms_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled")))
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
.startsWith(QStringLiteral("Disabled"))) {
m_watt = ftms_watt; // Only update watt if no external power sensor
}
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
index += 2;
}
@@ -1317,15 +1336,27 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
QSettings settings;
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"));
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
if (!autoResistance() || resistance_lvl_mode || ergModeNotSupported) {
bool isPowerCommand = (newValue.length() > 0 && (uint8_t)newValue.at(0) == FTMS_SET_TARGET_POWER);
// FTMS routing filter logic:
// - Block simulation commands (0x11) when resistance_lvl_mode=true
// - Allow power commands (0x05) only when no external power sensor (delta power system handles external sensors)
bool allowPowerRouting = (!power_sensor && ergModeSupported && isPowerCommand);
if (!autoResistance() || (resistance_lvl_mode && !allowPowerRouting) || ergModeNotSupported) {
qDebug() << "ignoring routing FTMS packet to the bike from virtualbike because of auto resistance OFF or resistance lvl mode is on or ergModeNotSupported"
<< characteristic.uuid() << newValue.toHex(' ') << ergModeNotSupported << resistance_lvl_mode;
<< characteristic.uuid() << newValue.toHex(' ') << "ergModeNotSupported:" << ergModeNotSupported
<< "resistance_lvl_mode:" << resistance_lvl_mode << "power_sensor:" << power_sensor << "isPowerCommand:" << isPowerCommand;
return;
}
QByteArray b = newValue;
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
if (gattWriteCharControlPointId.isValid()) {
@@ -1500,7 +1531,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
max_resistance = 16;
} else if ((bluetoothDevice.name().toUpper().startsWith("MAGNUS "))) {
qDebug() << QStringLiteral("MAGNUS found");
resistance_lvl_mode = true;
MAGNUS = true;
resistance_lvl_mode = true;
ergModeSupported = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("DU30-"))) {
qDebug() << QStringLiteral("DU30 found");
max_resistance = 32;
@@ -1511,6 +1544,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((bluetoothDevice.name().toUpper().startsWith("DOMYOS"))) {
qDebug() << QStringLiteral("DOMYOS found");
resistance_lvl_mode = true;
ergModeSupported = false;
DOMYOS = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("3G Cardio RB"))) {
qDebug() << QStringLiteral("_3G_Cardio_RB found");
@@ -1544,6 +1578,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((bluetoothDevice.name().toUpper().startsWith("SPAX-BK-"))) {
qDebug() << QStringLiteral("SPAX-BK found");
resistance_lvl_mode = true;
ergModeSupported = false;
} else if ((bluetoothDevice.name().toUpper().startsWith("LYDSTO"))) {
qDebug() << QStringLiteral("LYDSTO found");
LYDSTO = true;
@@ -1552,11 +1587,13 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
SL010 = true;
max_resistance = 25;
resistance_lvl_mode = true;
ergModeSupported = false;
} else if ((bluetoothDevice.name().toUpper().startsWith("REEBOK"))) {
qDebug() << QStringLiteral("REEBOK found");
REEBOK = true;
max_resistance = 32;
resistance_lvl_mode = true;
ergModeSupported = false;
} else if ((bluetoothDevice.name().toUpper().startsWith("TITAN 7000"))) {
qDebug() << QStringLiteral("Titan 7000 found");
TITAN_7000 = true;
@@ -1590,7 +1627,14 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if(device.name().toUpper().startsWith("VANRYSEL-HT")) {
qDebug() << QStringLiteral("VANRYSEL-HT found");
VANRYSEL_HT = true;
} else if(device.name().toUpper().startsWith("MRK-S26C-")) {
qDebug() << QStringLiteral("MRK-S26C found");
MRK_S26C = true;
} else if(device.name().toUpper().startsWith("HAMMER")) {
qDebug() << QStringLiteral("HAMMER found");
HAMMER = true;
}
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {
resistance_lvl_mode = true;

View File

@@ -158,6 +158,9 @@ class ftmsbike : public bike {
bool THINK_X = false;
bool WLT8828 = false;
bool VANRYSEL_HT = false;
bool MAGNUS = false;
bool MRK_S26C = false;
bool HAMMER = false;
int16_t T2_lastGear = 0;

View File

@@ -62,12 +62,11 @@ void ftmsrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
void ftmsrower::forceResistance(resistance_t requestResistance) {
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
write[3] = ((uint16_t)requestResistance * 100) & 0xFF;
write[4] = ((uint16_t)requestResistance * 100) >> 8;
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00};
write[1] = ((uint8_t)(requestResistance * 10));
writeCharacteristic(write, sizeof(write), QStringLiteral("forceResistance ") + QString::number(requestResistance));
if(NORDLYS)
Resistance = requestResistance; // Nordlys does not report back the resistance so we set it here
}
void ftmsrower::update() {
@@ -143,6 +142,123 @@ void ftmsrower::serviceDiscovered(const QBluetoothUuid &gatt) {
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
}
void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
QDateTime now = QDateTime::currentDateTime();
QSettings settings;
QString charUuid = characteristic.uuid().toString();
if (charUuid == QStringLiteral("{ce060031-43e5-11e4-916c-0800200c9a66}")) {
// Parse characteristic CE060031 - Based on go-row implementation
if (newValue.length() >= 10) {
// Extract RowState from byte 9 - this indicates if user is actively rowing
pm5RowState = (uint8_t)newValue.at(9);
emit debug(QStringLiteral("PM5 CE060031 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
}
}
else if (charUuid == QStringLiteral("{ce060032-43e5-11e4-916c-0800200c9a66}")) {
// Parse characteristic CE060032 - Based on go-row implementation
if (newValue.length() >= 7) {
// Extract cadence (SPM) from byte 5
uint8_t spm = (uint8_t)newValue.at(5);
if (spm > 0) {
Cadence = spm;
lastStroke = now;
}
// Extract speed from bytes 3-4 (little endian) in 0.001m/s
uint16_t speedRaw = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
if (speedRaw > 0) {
Speed = (speedRaw * 0.001) * 3.6; // Convert m/s to km/h
}
emit debug(QStringLiteral("PM5 CE060032 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" Cadence: ") + QString::number(Cadence.value()) +
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
}
}
else if (charUuid == QStringLiteral("{ce060033-43e5-11e4-916c-0800200c9a66}")) {
// Parse characteristic CE060033 - Additional data
if (newValue.length() >= 20) {
emit debug(QStringLiteral("PM5 CE060033 RAW: ") + newValue.toHex(' '));
}
}
else if (charUuid == QStringLiteral("{ce060036-43e5-11e4-916c-0800200c9a66}")) {
// Parse characteristic CE060036 - Power and stroke count (based on go-row implementation)
if (newValue.length() >= 9) {
// Extract stroke count from bytes 7-8 (little endian)
uint16_t strokeCount = ((uint8_t)newValue.at(8) << 8) | (uint8_t)newValue.at(7);
if (strokeCount != StrokesCount.value()) {
StrokesCount = strokeCount;
lastStroke = now;
}
// Extract power from bytes 3-4 (little endian)
uint16_t power = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
if (power > 0) {
m_watt = power;
}
emit debug(QStringLiteral("PM5 CE060036 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
QStringLiteral(" Stroke Count: ") + QString::number(StrokesCount.value()) +
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
}
}
else if (charUuid == QStringLiteral("{ce060035-43e5-11e4-916c-0800200c9a66}")) {
// Parse characteristic CE060035 - Stroke data including drive length (stroke length)
if (newValue.length() >= 7) {
// Extract drive length (stroke length) from byte 6 - 0.01 meters LSB, max 2.55m
uint8_t driveLengthRaw = (uint8_t)newValue.at(6);
if (driveLengthRaw > 0) {
// Convert from 0.01m units to meters
double strokeLengthMeters = driveLengthRaw * 0.01;
StrokesLength = strokeLengthMeters;
}
emit debug(QStringLiteral("PM5 CE060035 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" Stroke Length: ") + QString::number(StrokesLength.value()) +
QStringLiteral("m RowState: ") + QString::number(pm5RowState));
}
}
// Update calories based on power if available
if (m_watt.value() > 0) {
KCal += ((((0.048 * ((double)m_watt.value()) + 1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(now))));
}
// Update crank revolutions for virtual device compatibility
if (Cadence.value() > 0) {
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
lastRefreshCharacteristicChanged = now;
// Apply RowState logic after all characteristics processing
if (PM5 && pm5RowState == 0) {
m_watt = 0;
Cadence = 0;
Speed = 0;
}
// Update metrics for virtual device
update_metrics(false, m_watt.value());
emit debug(QStringLiteral("PM5 Metrics - Cadence: ") + QString::number(Cadence.value()) +
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
QStringLiteral(" Distance: ") + QString::number(Distance.value()) +
QStringLiteral(" Calories: ") + QString::number(KCal.value()) +
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
}
void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
QDateTime now = QDateTime::currentDateTime();
@@ -156,6 +272,17 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
qDebug() << QStringLiteral(" << ") << characteristic.uuid() << " " << newValue.toHex(' ');
// Handle Concept2 PM5 characteristics as fallback when FTMS is not available
if (PM5 && (characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060031-43e5-11e4-916c-0800200c9a66")) ||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060032-43e5-11e4-916c-0800200c9a66")) ||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060033-43e5-11e4-916c-0800200c9a66")) ||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060035-43e5-11e4-916c-0800200c9a66")) ||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060036-43e5-11e4-916c-0800200c9a66")))) {
parseConcept2Data(characteristic, newValue);
return;
}
if (characteristic.uuid() != QBluetoothUuid((quint16)0x2AD1)) {
return;
}
@@ -569,6 +696,36 @@ void ftmsrower::serviceScanDone(void) {
#endif
auto services_list = m_control->services();
bool hasFTMSService = false;
bool hasConcept2Services = false;
// Check if FTMS service (0x1826) is available
QBluetoothUuid ftmsService((quint16)0x1826);
for (const QBluetoothUuid &s : qAsConst(services_list)) {
if (s == ftmsService) {
hasFTMSService = true;
break;
}
}
// If no FTMS service, check for Concept2 PM5 services
if (!hasFTMSService && PM5) {
QBluetoothUuid concept2InfoService(QStringLiteral("ce060010-43e5-11e4-916c-0800200c9a66"));
QBluetoothUuid concept2ControlService(QStringLiteral("ce060020-43e5-11e4-916c-0800200c9a66"));
QBluetoothUuid concept2RowingService(QStringLiteral("ce060030-43e5-11e4-916c-0800200c9a66"));
for (const QBluetoothUuid &s : qAsConst(services_list)) {
if (s == concept2InfoService || s == concept2ControlService || s == concept2RowingService) {
hasConcept2Services = true;
break;
}
}
if (hasConcept2Services) {
emit debug(QStringLiteral("PM5 without FTMS service detected, using Concept2 protocol"));
}
}
for (const QBluetoothUuid &s : qAsConst(services_list)) {
gattCommunicationChannelService.append(m_control->createServiceObject(s));
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
@@ -619,6 +776,9 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) {
PM5 = true;
qDebug() << "PM5 found!";
} else if (device.name().toUpper().startsWith(QStringLiteral("NORDLYS"))) {
NORDLYS = true;
qDebug() << "NORDLYS found!";
}
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);

View File

@@ -46,6 +46,7 @@ class ftmsrower : public rower {
void startDiscover();
uint16_t watts() override;
void forceResistance(resistance_t requestResistance);
void parseConcept2Data(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
QTimer *refresh;
@@ -69,6 +70,7 @@ class ftmsrower : public rower {
bool WHIPR = false;
bool KINGSMITH = false;
bool PM5 = false;
bool NORDLYS = false;
bool WATER_ROWER = false;
bool DFIT_L_R = false;
@@ -76,6 +78,9 @@ class ftmsrower : public rower {
bool ROWER = false;
QDateTime lastStroke = QDateTime::currentDateTime();
double lastStrokesCount = 0;
// PM5 specific variables
uint8_t pm5RowState = 0;
#ifdef Q_OS_IOS
lockscreen *h = 0;

View File

@@ -972,7 +972,7 @@ void horizontreadmill::update() {
forceIncline(requestInclination);
// this treadmill doesn't send the incline, so i'm forcing it manually
if(SW_TREADMILL) {
if(SW_TREADMILL || mobvoi_treadmill) {
Inclination = requestInclination;
}
}

View File

@@ -448,7 +448,7 @@ void kingsmithr2treadmill::stateChanged(QLowEnergyService::ServiceState state) {
QBluetoothUuid _gattWriteCharacteristicId((quint16)0xFED7);
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xFED8);
if (KS_NACH_X21C || KS_NGCH_G1C_2) {
if (KS_NACH_X21C || KS_NGCH_G1C_2 || KS_HDSY_X21C_2) {
_gattWriteCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED7-0000-1000-8000-00805f9b34fb"));
_gattNotifyCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED8-0000-1000-8000-00805f9b34fb"));
} else if (KS_NGCH_G1C || KS_NACH_MXG || KS_NACH_X21C_2) {
@@ -517,6 +517,12 @@ void kingsmithr2treadmill::serviceScanDone(void) {
qDebug() << "KS_NACH_X21C default service id not found";
_gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("00011234-0000-1000-8000-00805f9b34fb"));
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
} else if(gattCommunicationChannelService == nullptr && KS_HDSY_X21C) {
KS_HDSY_X21C_2 = true;
KS_HDSY_X21C = false;
qDebug() << "KS_HDSY_X21C default service id not found";
_gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("00021234-0000-1000-8000-00805f9b34fb"));
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
} else if(gattCommunicationChannelService == nullptr && KS_NGCH_G1C) {
KS_NGCH_G1C_2 = true;
KS_NGCH_G1C = false;
@@ -550,6 +556,9 @@ void kingsmithr2treadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
if (device.name().toUpper().startsWith(QStringLiteral("KS-NACH-X21C"))) {
qDebug() << "KS-NACH-X21C workaround!";
KS_NACH_X21C = true;
} else if (device.name().toUpper().startsWith(QStringLiteral("KS-HDSY-X21C"))) {
qDebug() << "KS-HDSY-X21C workaround!";
KS_HDSY_X21C = true;
} else if (device.name().toUpper().startsWith(QStringLiteral("KS-NGCH-G1C"))) {
qDebug() << "KS-NGCH-G1C workaround!";
KS_NGCH_G1C = true;

View File

@@ -98,6 +98,8 @@ class kingsmithr2treadmill : public treadmill {
bool KS_NACH_X21C = false;
bool KS_NACH_X21C_2 = false;
bool KS_HDSY_X21C = false;
bool KS_HDSY_X21C_2 = false;
bool KS_NGCH_G1C = false;
bool KS_NGCH_G1C_2 = false;
bool KS_NACH_MXG = false;

View File

@@ -345,14 +345,15 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris
uint16_t time_division = 1024;
uint8_t index = 4;
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
if (newValue.length() > 3) {
m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
if (newValue.length() > 3) {
double tacx_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
m_rawWatt = tacx_watt; // Always update rawWatt from TACX bike data
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
m_watt = tacx_watt; // Only update watt if no external power sensor
emit powerChanged(m_watt.value());
}
emit powerChanged(m_watt.value());
emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value()));
}
@@ -635,11 +636,14 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris
if (Flags.instantPower) {
// power table from an user
double tacx_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
m_rawWatt = tacx_watt; // Always update rawWatt from TACX bike data
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled")))
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
.startsWith(QStringLiteral("Disabled"))) {
m_watt = tacx_watt; // Only update watt if no external power sensor
}
index += 2;
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
}

View File

@@ -33,9 +33,16 @@ void toorxtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this,
&toorxtreadmill::serviceDiscovered);
// Start a discovery
qDebug() << QStringLiteral("toorxtreadmill::deviceDiscovered");
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
// Start a discovery - use FullDiscovery only if not done before
QSettings settings;
bool discoveryCompleted = settings.value(QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed).toBool();
qDebug() << QStringLiteral("toorxtreadmill::deviceDiscovered - discoveryCompleted:") << discoveryCompleted;
if (discoveryCompleted) {
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::MinimalDiscovery);
} else {
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
}
return;
}
}
@@ -382,6 +389,14 @@ void toorxtreadmill::rfCommConnected() {
qDebug() << QStringLiteral(" init1 write");
socket->write((char *)init2, sizeof(init2));
qDebug() << QStringLiteral(" init2 write");
// Mark discovery as completed for future connections
QSettings settings;
if (!settings.value(QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed).toBool()) {
settings.setValue(QZSettings::toorxtreadmill_discovery_completed, true);
qDebug() << QStringLiteral("toorxtreadmill discovery marked as completed");
}
initDone = true;
// requestStart = 1;
emit connectedAndDiscovered();
@@ -426,8 +441,8 @@ uint16_t toorxtreadmill::GetCaloriesFromPacket(const QByteArray &packet) {
return convertedData;
}
uint16_t toorxtreadmill::GetDistanceFromPacket(const QByteArray &packet) {
uint16_t convertedData = (packet.at(9) << 8) | packet.at(10);
double toorxtreadmill::GetDistanceFromPacket(const QByteArray &packet) {
double convertedData = (double)((packet.at(9) << 8) | packet.at(10)) / 100.0;
return convertedData;
}

View File

@@ -55,7 +55,7 @@ class toorxtreadmill : public treadmill {
bool MASTERT409 = false;
uint16_t GetElapsedTimeFromPacket(const QByteArray &packet);
uint16_t GetDistanceFromPacket(const QByteArray &packet);
double GetDistanceFromPacket(const QByteArray &packet);
uint16_t GetCaloriesFromPacket(const QByteArray &packet);
double GetSpeedFromPacket(const QByteArray &packet);
uint8_t GetInclinationFromPacket(const QByteArray &packet);

View File

@@ -56,7 +56,7 @@ void trxappgateusbelliptical::writeCharacteristic(uint8_t *data, uint8_t data_le
}
void trxappgateusbelliptical::forceResistance(resistance_t requestResistance) {
if (elliptical_type == TYPE::DCT2000I) {
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
uint8_t noOpData1[] = {0xf0, 0xa6, 0x01, 0x01, 0x03, 0x9b};
noOpData1[4] = requestResistance + 1;
noOpData1[5] = noOpData1[4] + 0x98;
@@ -95,7 +95,7 @@ void trxappgateusbelliptical::update() {
}
requestResistance = -1;
} else {
if (elliptical_type == TYPE::DCT2000I) {
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
uint8_t noOpData1[] = {0xf0, 0xa2, 0x01, 0x01, 0x94};
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp"));
} else {
@@ -139,22 +139,41 @@ void trxappgateusbelliptical::serviceDiscovered(const QBluetoothUuid &gatt) {
double trxappgateusbelliptical::GetSpeedFromPacket(const QByteArray &packet) {
uint16_t convertedData = (packet.at(7) - 1) + ((packet.at(6) - 1) * 100);
double data = (double)(convertedData) / 10.0f;
return data;
if (elliptical_type == TYPE::JTX_FITNESS) {
// JTX Fitness doesn't send speed via bluetooth, calculate from cadence using settings ratio
QSettings settings;
double cadence_speed_ratio = settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio).toDouble();
double cadence = GetCadenceFromPacket(packet);
return cadence * cadence_speed_ratio;
} else {
uint16_t convertedData = (packet.at(7) - 1) + ((packet.at(6) - 1) * 100);
double data = (double)(convertedData) / 10.0f;
return data;
}
}
double trxappgateusbelliptical::GetCadenceFromPacket(const QByteArray &packet) {
uint16_t convertedData = ((uint16_t)packet.at(9)) + ((uint16_t)packet.at(8) * 100);
uint16_t convertedData;
if (elliptical_type == TYPE::JTX_FITNESS) {
// JTX Fitness uses only byte 5 for cadence
convertedData = packet.at(5);
} else {
convertedData = ((uint16_t)packet.at(9)) + ((uint16_t)packet.at(8) * 100);
}
return convertedData;
}
double trxappgateusbelliptical::GetWattFromPacket(const QByteArray &packet) {
uint16_t convertedData = ((packet.at(16) - 1) * 100) + (packet.at(17) - 1);
double data = ((double)(convertedData)) / 10.0f;
return data;
if (elliptical_type == TYPE::JTX_FITNESS) {
// JTX Fitness doesn't send watts via bluetooth, use classic elliptical calculation
return 0; // Will be calculated in characteristicChanged using wattsFromResistance
} else {
uint16_t convertedData = ((packet.at(16) - 1) * 100) + (packet.at(17) - 1);
double data = ((double)(convertedData)) / 10.0f;
return data;
}
}
void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
@@ -216,7 +235,7 @@ void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacterist
void trxappgateusbelliptical::btinit() {
if (elliptical_type == TYPE::DCT2000I) {
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x00, 0x91};
uint8_t initData2[] = {0xf0, 0xa0, 0x01, 0x01, 0x92};
uint8_t initData3[] = {0xf0, 0xa1, 0x01, 0x01, 0x93};
@@ -277,11 +296,12 @@ void trxappgateusbelliptical::stateChanged(QLowEnergyService::ServiceState state
QString uuidNotify1 = QStringLiteral("0000fff1-0000-1000-8000-00805f9b34fb");
QString uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e");
if (elliptical_type == TYPE::DCT2000I) {
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS) {
uuidWrite = QStringLiteral("49535343-8841-43f4-a8d4-ecbe34729bb3");
uuidNotify1 = QStringLiteral("49535343-1E4D-4BD9-BA61-23C647249616");
uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e");
}
// TAURUS_FX99 uses standard 0000fff0 characteristics
QBluetoothUuid _gattWriteCharacteristicId(uuidWrite);
QBluetoothUuid _gattNotify1CharacteristicId(uuidNotify1);
@@ -364,6 +384,41 @@ void trxappgateusbelliptical::serviceScanDone(void) {
uuid = uuid2;
}
// Fallback logic: try to find the service in discovered services
bool found = false;
foreach (QBluetoothUuid s, m_control->services()) {
if (s == QBluetoothUuid::fromString(uuid)) {
found = true;
break;
}
}
// If primary service not found, try fallback service
if (!found) {
if (elliptical_type == TYPE::DCT2000I) {
// I-CONSOLE+ device but DCT2000I service not found, try 0000fff0 service (Taurus FX9.9)
bool found_fff0 = false;
foreach (QBluetoothUuid s, m_control->services()) {
if (s == QBluetoothUuid::fromString(uuid3)) {
found_fff0 = true;
break;
}
}
if (found_fff0) {
uuid = uuid3;
elliptical_type = TYPE::TAURUS_FX99;
qDebug() << QStringLiteral("I-CONSOLE+ device detected as Taurus FX9.9 with 0000fff0 service");
} else {
qDebug() << QStringLiteral("DCT2000I service not found");
}
} else {
// Try DCT2000I/JTX Fitness service as fallback
uuid = uuid2;
elliptical_type = TYPE::JTX_FITNESS;
qDebug() << QStringLiteral("Standard service not found, trying JTX Fitness service as fallback");
}
}
QBluetoothUuid _gattCommunicationChannelServiceId(uuid);
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
@@ -448,7 +503,13 @@ void trxappgateusbelliptical::controllerStateChanged(QLowEnergyController::Contr
}
}
uint16_t trxappgateusbelliptical::watts() { return m_watt.value(); }
uint16_t trxappgateusbelliptical::watts() {
if (elliptical_type == TYPE::JTX_FITNESS) {
// For JTX Fitness, always use the elliptical class generic calculation
return elliptical::watts();
}
return m_watt.value();
}
void trxappgateusbelliptical::searchingStop() { searchStopped = true; }

View File

@@ -78,6 +78,8 @@ class trxappgateusbelliptical : public elliptical {
typedef enum TYPE {
ELLIPTICAL_GENERIC = 0,
DCT2000I = 1,
JTX_FITNESS = 2,
TAURUS_FX99 = 3,
} TYPE;
TYPE elliptical_type = ELLIPTICAL_GENERIC;

View File

@@ -323,7 +323,7 @@ void wahookickrsnapbike::update() {
} else if (lastGearValue != gears()) {
inclinationChanged(lastGrade, lastGrade);
}
} else if (requestResistance != -1 && KICKR_BIKE == false) {
} else if ((requestResistance != -1 || lastGearValue != gears()) && KICKR_BIKE == false) {
if (requestResistance > 100) {
requestResistance = 100;
} else if (requestResistance == 0) {

27
src/fitbackupwriter.cpp Normal file
View File

@@ -0,0 +1,27 @@
#include "fitbackupwriter.h"
#include <QDebug>
FitBackupWriter::FitBackupWriter(QObject *parent) : QObject(parent) {
}
FitBackupWriter::~FitBackupWriter() {
}
void FitBackupWriter::writeFitBackup(const QString &filename,
const QList<SessionLine> &session,
bluetoothdevice::BLUETOOTH_TYPE deviceType,
uint32_t processType,
FIT_SPORT workoutType,
const QString &workoutName,
const QString &deviceName) {
qDebug() << QStringLiteral("Writing FIT backup file in background thread: ") << filename;
// Remove existing file
QFile::remove(filename);
// Save FIT file using the same logic as the original backup() method
qfit::save(filename, session, deviceType, processType, workoutType,
workoutName, deviceName, "", "", "", "");
qDebug() << QStringLiteral("FIT backup file written successfully: ") << filename;
}

28
src/fitbackupwriter.h Normal file
View File

@@ -0,0 +1,28 @@
// fitbackupwriter.h
#ifndef FITBACKUPWRITER_H
#define FITBACKUPWRITER_H
#include <QObject>
#include <QFile>
#include "sessionline.h"
#include "fit_profile.hpp"
#include "qfit.h"
#include "bluetoothdevice.h"
class FitBackupWriter : public QObject {
Q_OBJECT
public:
explicit FitBackupWriter(QObject *parent = nullptr);
virtual ~FitBackupWriter();
public slots:
void writeFitBackup(const QString &filename,
const QList<SessionLine> &session,
bluetoothdevice::BLUETOOTH_TYPE deviceType,
uint32_t processType,
FIT_SPORT workoutType,
const QString &workoutName,
const QString &deviceName);
};
#endif // FITBACKUPWRITER_H

View File

@@ -215,7 +215,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
target_power = new DataObject(QStringLiteral("T.Power(W)"), QStringLiteral("icons/icons/watt.png"),
QStringLiteral("0"), true, QStringLiteral("target_power"), 48, labelFontSize);
target_zone = new DataObject(QStringLiteral("T.Zone"), QStringLiteral("icons/icons/watt.png"), QStringLiteral("1"),
false, QStringLiteral("target_zone"), 48, labelFontSize);
true, QStringLiteral("target_zone"), 48, labelFontSize);
target_speed = new DataObject(QStringLiteral("T.Speed (") + unit + QStringLiteral("/h)"),
QStringLiteral("icons/icons/speed.png"), QStringLiteral("0.0"), true,
QStringLiteral("target_speed"), 48, labelFontSize);
@@ -572,7 +572,16 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
automaticShiftingTimer = new QTimer(this);
connect(automaticShiftingTimer, &QTimer::timeout, this, &homeform::ten_hz);
automaticShiftingTimer->start(100); // 100ms = 10Hz
if (settings.value(QZSettings::automatic_virtual_shifting_enabled, QZSettings::default_automatic_virtual_shifting_enabled).toBool()) {
automaticShiftingTimer->start(100); // 100ms = 10Hz
}
// Initialize FIT backup thread
fitBackupThread = new QThread(this);
fitBackupWriter = new FitBackupWriter();
fitBackupWriter->moveToThread(fitBackupThread);
fitBackupThread->start();
QObject *rootObject = engine->rootObjects().constFirst();
QObject *home = rootObject->findChild<QObject *>(QStringLiteral("home"));
@@ -1167,18 +1176,24 @@ QString homeform::getWritableAppDir() {
void homeform::backup() {
static uint8_t index = 0;
qDebug() << QStringLiteral("saving fit file backup...");
qDebug() << QStringLiteral("scheduling fit file backup...");
QString path = getWritableAppDir();
bluetoothdevice *dev = bluetoothManager->device();
if (dev) {
QString filename = path + QString::number(index) + backupFitFileName;
QFile::remove(filename);
qfit::save(filename, Session, dev->deviceType(),
qobject_cast<m3ibike *>(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE,
stravaPelotonWorkoutType, workoutName(), dev->bluetoothDevice.name(),
"", "", "", "");
// Use thread to write FIT backup file
QMetaObject::invokeMethod(fitBackupWriter, "writeFitBackup",
Qt::QueuedConnection,
Q_ARG(QString, filename),
Q_ARG(QList<SessionLine>, Session),
Q_ARG(bluetoothdevice::BLUETOOTH_TYPE, dev->deviceType()),
Q_ARG(uint32_t, qobject_cast<m3ibike *>(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE),
Q_ARG(FIT_SPORT, stravaPelotonWorkoutType),
Q_ARG(QString, workoutName()),
Q_ARG(QString, dev->bluetoothDevice.name()));
index++;
if (index > 1) {
@@ -1301,6 +1316,15 @@ void homeform::refresh_bluetooth_devices_clicked() {
}
homeform::~homeform() {
// Cleanup FIT backup thread
if (fitBackupThread && fitBackupThread->isRunning()) {
fitBackupThread->quit();
fitBackupThread->wait();
}
if (fitBackupWriter) {
delete fitBackupWriter;
}
gpx_save_clicked();
fit_save_clicked();
}
@@ -3976,23 +4000,32 @@ void homeform::LargeButton(const QString &name) {
double zoneValue = settings.value(zoneSetting, zoneNum).toDouble();
// Calculate target watts based on FTP and zone value
// Each zone represents a percentage of FTP
double targetWatts;
// Map zoneValue to the correct percentage within each power zone
double targetPerc;
if (zoneValue <= 1.9) {
targetWatts = ftp * 0.55 * (zoneValue);
// Zone 1: 0-55% FTP range
targetPerc = zoneValue * 0.50; // zoneValue 1.0->50%, safely within Zone 1 boundary
if (targetPerc > 0.55) targetPerc = 0.55;
} else if (zoneValue <= 2.9) {
targetWatts = ftp * 0.75 * (zoneValue - 1);
// Zone 2: 56-75% FTP range
targetPerc = 0.56 + (zoneValue - 2.0) * 0.19; // zoneValue 2.0->56%, 3.0->75%
} else if (zoneValue <= 3.9) {
targetWatts = ftp * 0.90 * (zoneValue - 2);
// Zone 3: 76-90% FTP range
targetPerc = 0.76 + (zoneValue - 3.0) * 0.14; // zoneValue 3.0->76%, 4.0->90%
} else if (zoneValue <= 4.9) {
targetWatts = ftp * 1.05 * (zoneValue - 3);
// Zone 4: 91-105% FTP range
targetPerc = 0.91 + (zoneValue - 4.0) * 0.14; // zoneValue 4.0->91%, 5.0->105%
} else if (zoneValue <= 5.9) {
targetWatts = ftp * 1.20 * (zoneValue - 4);
// Zone 5: 106-120% FTP range
targetPerc = 1.06 + (zoneValue - 5.0) * 0.14; // zoneValue 5.0->106%, 6.0->120%
} else if (zoneValue <= 6.9) {
targetWatts = ftp * 1.50 * (zoneValue - 5);
// Zone 6: 121-150% FTP range
targetPerc = 1.21 + (zoneValue - 6.0) * 0.29; // zoneValue 6.0->121%, 7.0->150%
} else {
targetWatts = ftp * 1.70 * (zoneValue - 6);
// Zone 7: 151%+ FTP range
targetPerc = 1.51 + (zoneValue - 7.0) * 0.19; // zoneValue 7.0->151%, 8.0->170%
}
double targetWatts = ftp * targetPerc;
bluetoothManager->device()->changePower(targetWatts);
} else if (name.contains(QStringLiteral("erg_mode"))) {
settings.setValue(QZSettings::zwift_erg, !settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool());
@@ -4244,6 +4277,8 @@ void homeform::LargeButton(const QString &name) {
}
}
void homeform::Plus(const QString &name) {
QSettings settings;
@@ -4429,15 +4464,34 @@ void homeform::Plus(const QString &name) {
}
} else if (name.contains(QStringLiteral("resistance")) || name.contains(QStringLiteral("peloton_resistance"))) {
if (bluetoothManager->device()) {
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
((bike *)bluetoothManager->device())
->changeResistance(((bike *)bluetoothManager->device())->currentResistance().value() + 1);
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) {
((rower *)bluetoothManager->device())
->changeResistance(((rower *)bluetoothManager->device())->currentResistance().value() + 1);
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
((elliptical *)bluetoothManager->device())
->changeResistance(((elliptical *)bluetoothManager->device())->currentResistance().value() + 1);
auto dev = bluetoothManager->device();
double current = dev->currentResistance().value();
double diff = dev->difficult();
if (diff == 0) diff = 1.0; // safety
resistance_t maxRes = dev->maxResistance();
if (dev->deviceType() == bluetoothdevice::BIKE) {
double g = ((bike *)dev)->gears();
double target = current + 1; // device-space target
int raw = qRound((target - g) / diff);
if (raw < 1) raw = 1;
if (raw > maxRes) raw = maxRes;
((bike *)dev)->changeResistance(raw);
} else if (dev->deviceType() == bluetoothdevice::ROWING) {
double g = ((rower *)dev)->gears();
double target = current + 1; // device-space target
int raw = qRound((target - g) / diff);
if (raw < 1) raw = 1;
if (raw > maxRes) raw = maxRes;
((rower *)dev)->changeResistance(raw);
} else if (dev->deviceType() == bluetoothdevice::ELLIPTICAL) {
double g = ((elliptical *)dev)->gears();
double target = current + 1; // device-space target
// elliptical::changeResistance does not use difficult(), but keep formula consistent
int raw = qRound((target - g) / diff);
if (raw < 1) raw = 1;
if (raw > maxRes) raw = maxRes;
((elliptical *)dev)->changeResistance(raw);
}
}
} else if (name.contains(QStringLiteral("target_power"))) {
@@ -4483,6 +4537,17 @@ void homeform::Plus(const QString &name) {
if (bluetoothManager->device() && trainProgram) {
trainProgram->increaseElapsedTime(1);
}
} else if (name.contains(QStringLiteral("target_zone"))) {
QSettings settings;
double currentFtp = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble();
if (currentFtp > 0 && bluetoothManager->device() &&
bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
double currentTargetPower = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
double powerIncrement = currentFtp * 0.01; // 1% of FTP
double newTargetPower = currentTargetPower + powerIncrement;
((bike *)bluetoothManager->device())->changePower(newTargetPower);
qDebug() << "Target power increased by" << powerIncrement << "W (1% of FTP) from" << currentTargetPower << "to" << newTargetPower;
}
} else {
qDebug() << name << QStringLiteral("not handled");
@@ -4689,15 +4754,34 @@ void homeform::Minus(const QString &name) {
}
} else if (name.contains(QStringLiteral("resistance")) || name.contains(QStringLiteral("peloton_resistance"))) {
if (bluetoothManager->device()) {
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
((bike *)bluetoothManager->device())
->changeResistance(((bike *)bluetoothManager->device())->currentResistance().value() - 1);
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) {
((rower *)bluetoothManager->device())
->changeResistance(((rower *)bluetoothManager->device())->currentResistance().value() - 1);
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
((elliptical *)bluetoothManager->device())
->changeResistance(((elliptical *)bluetoothManager->device())->currentResistance().value() - 1);
auto dev = bluetoothManager->device();
double current = dev->currentResistance().value();
double diff = dev->difficult();
if (diff == 0) diff = 1.0; // safety
resistance_t maxRes = dev->maxResistance();
if (dev->deviceType() == bluetoothdevice::BIKE) {
double g = ((bike *)dev)->gears();
double target = current - 1; // device-space target
int raw = qRound((target - g) / diff);
if (raw < 1) raw = 1;
if (raw > maxRes) raw = maxRes;
((bike *)dev)->changeResistance(raw);
} else if (dev->deviceType() == bluetoothdevice::ROWING) {
double g = ((rower *)dev)->gears();
double target = current - 1; // device-space target
int raw = qRound((target - g) / diff);
if (raw < 1) raw = 1;
if (raw > maxRes) raw = maxRes;
((rower *)dev)->changeResistance(raw);
} else if (dev->deviceType() == bluetoothdevice::ELLIPTICAL) {
double g = ((elliptical *)dev)->gears();
double target = current - 1; // device-space target
// elliptical::changeResistance does not use difficult(), but keep formula consistent
int raw = qRound((target - g) / diff);
if (raw < 1) raw = 1;
if (raw > maxRes) raw = maxRes;
((elliptical *)dev)->changeResistance(raw);
}
}
} else if (name.contains(QStringLiteral("target_power"))) {
@@ -4747,6 +4831,18 @@ void homeform::Minus(const QString &name) {
if (bluetoothManager->device() && trainProgram) {
trainProgram->decreaseElapsedTime(1);
}
} else if (name.contains(QStringLiteral("target_zone"))) {
QSettings settings;
double currentFtp = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble();
if (currentFtp > 0 && bluetoothManager->device() &&
bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
double currentTargetPower = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
double powerDecrement = currentFtp * 0.01; // 1% of FTP
double newTargetPower = currentTargetPower - powerDecrement;
if (newTargetPower < 0) newTargetPower = 0; // Prevent negative power
((bike *)bluetoothManager->device())->changePower(newTargetPower);
qDebug() << "Target power decreased by" << powerDecrement << "W (1% of FTP) from" << currentTargetPower << "to" << newTargetPower;
}
} else {
qDebug() << name << QStringLiteral("not handled");
qDebug() << "Minus" << name;
@@ -5054,7 +5150,19 @@ void homeform::update() {
double currentHRZone = 1;
double ftpZone = 1;
qDebug() << "homeform::update fired!";
// Timer jitter detection (same logic as trainprogram::scheduler)
QDateTime now = QDateTime::currentDateTime();
qint64 msecsElapsed = lastUpdateCall.msecsTo(now);
// Reset jitter if it's getting too large
if (qAbs(currentUpdateJitter) > 5000) {
currentUpdateJitter = 0;
}
currentUpdateJitter += msecsElapsed - 1000;
lastUpdateCall = now;
qDebug() << "homeform::update fired!" << "elapsed:" << msecsElapsed << "jitter:" << currentUpdateJitter;
if (settings.status() != QSettings::NoError) {
qDebug() << "!!!!QSETTINGS ERROR!" << settings.status();
@@ -5204,8 +5312,9 @@ void homeform::update() {
QString::number((bluetoothManager->device())->currentSpeed().max() * unit_conversion, 'f', 1));
heart->setValue(QString::number(bluetoothManager->device()->currentHeart().value(), 'f', 0));
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
calories->setValue(QString::number(bluetoothManager->device()->calories().value(), 'f', 0));
calories->setSecondLine(QString::number(bluetoothManager->device()->calories().rate1s() * 60.0, 'f', 1) +
calories->setSecondLine(QString::number((activeOnly ? bluetoothManager->device()->activeCalories().rate1s() : bluetoothManager->device()->calories().rate1s()) * 60.0, 'f', 1) +
" /min");
if (!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool())
fan->setValue(QString::number(bluetoothManager->device()->fanSpeed()));
@@ -5886,7 +5995,7 @@ void homeform::update() {
this->strokesCount->setValue(
QString::number(((rower *)bluetoothManager->device())->currentStrokesCount().value(), 'f', 0));
this->strokesLength->setValue(
QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().value(), 'f', 1));
QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().value(), 'f', 2));
this->target_speed->setValue(QString::number(
((rower *)bluetoothManager->device())->lastRequestedSpeed().value() * unit_conversion, 'f', 1));
@@ -7184,14 +7293,65 @@ void homeform::update() {
qDebug() << "Current Distance 1s:" << bluetoothManager->device()->currentDistance1s().value() << bluetoothManager->device()->currentSpeed().value() << watts;
// Calculate current elapsed time in seconds
uint32_t currentElapsedSeconds = bluetoothManager->device()->elapsedTime().second() +
(bluetoothManager->device()->elapsedTime().minute() * 60) +
(bluetoothManager->device()->elapsedTime().hour() * 3600);
if (Session.empty()) {
currentUpdateJitter = 0;
}
// Check for timer jitter gaps and fill missing SessionLine records (same logic as trainprogram)
if (!Session.empty() && qAbs(currentUpdateJitter) > 1000) {
if (currentUpdateJitter > 1000) {
// We are late... fill the missing seconds with SessionLine records
int missedSeconds = currentUpdateJitter / 1000;
qDebug() << "Timer jitter detected: filling" << missedSeconds << "missing SessionLine records";
// Create SessionLine records for each missed second using current device values
uint32_t lastRecordedTime = Session.last().elapsedTime;
for (int i = 1; i <= missedSeconds; i++) {
SessionLine gapFill(
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
pace, cadence, bluetoothManager->device()->calories().value(),
bluetoothManager->device()->elevationGain().value(),
lastRecordedTime + i, // Fill each missing second
lapTrigger, totalStrokes, avgStrokesRate, maxStrokesRate, avgStrokesLength,
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
target_cadence->value().toDouble(), target_power->value().toDouble(), target_resistance->value().toDouble(),
target_incline->value().toDouble(), target_speed->value().toDouble(),
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(), bluetoothManager->device()->HeatStrainIndex.value());
Session.append(gapFill);
qDebug() << "Added gap-filling SessionLine for elapsed time:" << (lastRecordedTime + i);
}
// Adjust jitter counter (same as trainprogram)
currentUpdateJitter -= (missedSeconds * 1000);
} else if (currentUpdateJitter < -1000) {
// We are early (negative jitter)... remove excess SessionLine records
int excessSeconds = (-currentUpdateJitter) / 1000;
qDebug() << "Negative timer jitter detected: removing" << excessSeconds << "excess SessionLine records";
// Remove excess SessionLine records from the end
for (int i = 0; i < excessSeconds && !Session.empty(); i++) {
Session.removeLast();
qDebug() << "Removed excess SessionLine record";
}
// Adjust jitter counter (same as trainprogram)
currentUpdateJitter += (excessSeconds * 1000);
}
}
SessionLine s(
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
pace, cadence, bluetoothManager->device()->calories().value(),
bluetoothManager->device()->elevationGain().value(),
bluetoothManager->device()->elapsedTime().second() +
(bluetoothManager->device()->elapsedTime().minute() * 60) +
(bluetoothManager->device()->elapsedTime().hour() * 3600),
currentElapsedSeconds,
lapTrigger, totalStrokes, avgStrokesRate, maxStrokesRate, avgStrokesLength,
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
@@ -7207,9 +7367,13 @@ void homeform::update() {
#ifndef Q_OS_IOS
if (iphone_socket && iphone_socket->state() == QAbstractSocket::ConnectedState) {
QSettings mdns_settings;
bool activeOnly = mdns_settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
QString toSend =
"SENDER=PAD#HR=" + QString::number(bluetoothManager->device()->currentHeart().value()) +
"#KCAL=" + QString::number(bluetoothManager->device()->calories().value()) +
(activeOnly ? "#TOTALKCAL=" + QString::number(bluetoothManager->device()->totalCalories().value()) : "") +
"#BCAD=" + QString::number(bluetoothManager->device()->currentCadence().value()) +
"#SPD=" + QString::number(bluetoothManager->device()->currentSpeed().value()) +
"#PWR=" + QString::number(bluetoothManager->device()->wattsMetric().value()) +

View File

@@ -16,6 +16,7 @@
#include "smtpclient/src/SmtpMime"
#include "trainprogram.h"
#include "workoutmodel.h"
#include "fitbackupwriter.h"
#include <QChart>
#include <QColor>
#include <QGraphicsScene>
@@ -26,6 +27,7 @@
#include <QQuickItem>
#include <QQuickItemGrabResult>
#include <QTextToSpeech>
#include <QThread>
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
@@ -753,6 +755,10 @@ class homeform : public QObject {
QDateTime automaticShiftingGearUpStartTime = QDateTime::currentDateTime();
QDateTime automaticShiftingGearDownStartTime = QDateTime::currentDateTime();
// Timer jitter detection variables (same logic as trainprogram::scheduler)
QDateTime lastUpdateCall = QDateTime::currentDateTime();
qint64 currentUpdateJitter = 0;
peloton *pelotonHandler = nullptr;
bool m_pelotonAskStart = false;
QString m_pelotonProvider = "";
@@ -788,6 +794,10 @@ class homeform : public QObject {
QTimer *backupTimer;
QTimer *automaticShiftingTimer;
// FIT backup threading
QThread *fitBackupThread;
FitBackupWriter *fitBackupWriter;
QString strava_code;
QOAuth2AuthorizationCodeFlow *strava_connect();
void strava_refreshtoken();

View File

@@ -33,24 +33,122 @@
overflow-x: none;
margin: 0px;
}
.zoom-button {
position: absolute;
top: 10px;
right: 10px;
background: rgba(156, 39, 176, 0.8);
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
z-index: 1000;
font-size: 12px;
}
.zoom-button:hover {
background: rgba(156, 39, 176, 1);
}
.chart-container {
position: relative;
}
</style>
</head>
<body style="background-color:#1d2330">
<table style="border-spacing: 0px">
<tr>
<td>
<div id="divcanvas" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
<canvas id="canvas"></canvas>
<div id="chartContainer">
<table id="bothChartsTable" style="border-spacing: 0px; display: table;">
<tr>
<td>
<div id="divcanvas" class="chart-container" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
<button class="zoom-button" onclick="toggleZoom('power')">📊</button>
<canvas id="canvas"></canvas>
</div>
</td>
<td>
<div id="divcanvasheart" class="chart-container" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
<button class="zoom-button" onclick="toggleZoom('heart')">❤️</button>
<canvas id="canvasheart"></canvas>
</div>
</td>
</tr>
</table>
<div id="powerChartOnly" style="display: none;">
<div id="divcanvasFull" class="chart-container" style="width:100vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
<button class="zoom-button" onclick="toggleZoom('power')">📊</button>
<canvas id="canvasFull"></canvas>
</div>
</td>
<td>
<div id="divcanvasheart" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
<canvas id="canvasheart"></canvas>
</div>
</td>
</tr>
</table>
</div>
<div id="heartChartOnly" style="display: none;">
<div id="divcanvasheartFull" class="chart-container" style="width:100vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
<button class="zoom-button" onclick="toggleZoom('heart')">❤️</button>
<canvas id="canvasheartFull"></canvas>
</div>
</div>
</div>
<script>
let currentZoomMode = {power: false, heart: false};
let originalTimeRange = null;
function setChartDisplayMode(mode) {
const bothChartsTable = document.getElementById('bothChartsTable');
const powerChartOnly = document.getElementById('powerChartOnly');
const heartChartOnly = document.getElementById('heartChartOnly');
// Hide all containers
bothChartsTable.style.display = 'none';
powerChartOnly.style.display = 'none';
heartChartOnly.style.display = 'none';
// Show selected mode
switch(mode) {
case 0: // Both charts
bothChartsTable.style.display = 'table';
break;
case 1: // Heart rate only
heartChartOnly.style.display = 'block';
break;
case 2: // Power only
powerChartOnly.style.display = 'block';
break;
}
}
function toggleZoom(chartType) {
currentZoomMode[chartType] = !currentZoomMode[chartType];
if (currentZoomMode[chartType]) {
// Enable zoom mode (-30s to +2min from now)
setZoomMode(chartType, true);
} else {
// Disable zoom mode (show all data)
setZoomMode(chartType, false);
}
}
function setZoomMode(chartType, enabled) {
// This function will be extended to work with the actual chart instances
// For now, it's a placeholder that the chart scripts can override
console.log(`Zoom ${enabled ? 'enabled' : 'disabled'} for ${chartType} chart`);
if (window.toggleChartZoom) {
window.toggleChartZoom(chartType, enabled);
}
}
// Listen for messages from the QML WebView to change display mode
window.addEventListener('message', function(event) {
if (event.data && event.data.action === 'setChartDisplayMode') {
setChartDisplayMode(event.data.mode);
}
});
// Initialize with both charts visible
setChartDisplayMode(0);
</script>
</body>
</html>

View File

@@ -57,8 +57,19 @@ function process_trainprogram(arr) {
}
function process_arr(arr) {
let ctx = document.getElementById('canvas').getContext('2d');
let div = document.getElementById('divcanvas');
// Try to get the active canvas - check all possible canvas IDs
let ctx, div;
if (document.getElementById('canvas') && document.getElementById('canvas').offsetParent !== null) {
ctx = document.getElementById('canvas').getContext('2d');
div = document.getElementById('divcanvas');
} else if (document.getElementById('canvasFull') && document.getElementById('canvasFull').offsetParent !== null) {
ctx = document.getElementById('canvasFull').getContext('2d');
div = document.getElementById('divcanvasFull');
} else {
// Fallback to the first available canvas
ctx = (document.getElementById('canvas') || document.getElementById('canvasFull')).getContext('2d');
div = document.getElementById('divcanvas') || document.getElementById('divcanvasFull');
}
let reqpower = [];
let reqcadence = [];
@@ -383,6 +394,122 @@ function process_arr(arr) {
refresh();
}
// Global variables for zoom functionality
var isZoomedPower = false;
var isZoomedHeart = false;
var currentTime = 0;
var zoomUpdateIntervalPower = null;
var zoomUpdateIntervalHeart = null;
// Function to toggle zoom mode
window.toggleChartZoom = function(chartType, enabled) {
if (chartType === 'power' && powerChart) {
isZoomedPower = enabled;
if (enabled) {
startZoomMode('power');
} else {
stopZoomMode('power');
}
} else if (chartType === 'heart' && window.heartChart) {
isZoomedHeart = enabled;
if (enabled) {
startZoomMode('heart');
} else {
stopZoomMode('heart');
}
}
};
function startZoomMode(chartType) {
if (chartType === 'power' && powerChart) {
// Update zoom range every 1 second to follow "now"
zoomUpdateIntervalPower = setInterval(function() {
updateZoomRange('power');
}, 1000);
// Initial zoom setup
updateZoomRange('power');
} else if (chartType === 'heart' && window.heartChart) {
// Update zoom range every 1 second to follow "now"
zoomUpdateIntervalHeart = setInterval(function() {
updateZoomRange('heart');
}, 1000);
// Initial zoom setup
updateZoomRange('heart');
}
}
function stopZoomMode(chartType) {
if (chartType === 'power' && powerChart) {
// Clear the interval
if (zoomUpdateIntervalPower) {
clearInterval(zoomUpdateIntervalPower);
zoomUpdateIntervalPower = null;
}
// Reset to show all data and restore original tick settings
powerChart.options.scales.x.min = undefined;
powerChart.options.scales.x.max = undefined;
powerChart.options.scales.x.ticks.stepSize = undefined;
powerChart.options.scales.x.ticks.maxTicksLimit = undefined;
powerChart.update('none');
} else if (chartType === 'heart' && window.heartChart) {
// Clear the interval
if (zoomUpdateIntervalHeart) {
clearInterval(zoomUpdateIntervalHeart);
zoomUpdateIntervalHeart = null;
}
// Reset to show all data and restore original tick settings
window.heartChart.options.scales.x.min = undefined;
window.heartChart.options.scales.x.max = undefined;
window.heartChart.options.scales.x.ticks.stepSize = undefined;
window.heartChart.options.scales.x.ticks.maxTicksLimit = undefined;
window.heartChart.update('none');
}
}
function updateZoomRange(chartType) {
if (chartType === 'power' && powerChart && powerChart.data.datasets[0] && powerChart.data.datasets[0].data) {
// Get the latest data point time (current time)
let latestDataPoint = powerChart.data.datasets[0].data[powerChart.data.datasets[0].data.length - 1];
if (!latestDataPoint) return;
currentTime = latestDataPoint.x;
// Set zoom range: -30s to +2min from current time
let zoomStart = Math.max(0, currentTime - 30); // -30 seconds, but not below 0
let zoomEnd = currentTime + 120; // +2 minutes
// Update chart scale with proper tick configuration for zoom
powerChart.options.scales.x.min = zoomStart;
powerChart.options.scales.x.max = zoomEnd;
powerChart.options.scales.x.ticks.stepSize = 30; // 30 second intervals in zoom mode
powerChart.options.scales.x.ticks.maxTicksLimit = 6; // Limit number of ticks
powerChart.update('none');
} else if (chartType === 'heart' && window.heartChart && window.heartChart.data.datasets[0] && window.heartChart.data.datasets[0].data) {
// Get the latest data point time (current time)
let latestDataPoint = window.heartChart.data.datasets[0].data[window.heartChart.data.datasets[0].data.length - 1];
if (!latestDataPoint) return;
currentTime = latestDataPoint.x;
// Set zoom range: -30s to +2min from current time
let zoomStart = Math.max(0, currentTime - 30); // -30 seconds, but not below 0
let zoomEnd = currentTime + 120; // +2 minutes
// Update chart scale with proper tick configuration for zoom
window.heartChart.options.scales.x.min = zoomStart;
window.heartChart.options.scales.x.max = zoomEnd;
window.heartChart.options.scales.x.ticks.stepSize = 30; // 30 second intervals in zoom mode
window.heartChart.options.scales.x.ticks.maxTicksLimit = 6; // Limit number of ticks
window.heartChart.update('none');
}
}
function refresh() {
el = new MainWSQueueElement({
msg: null

View File

@@ -55,8 +55,19 @@ function process_trainprogram_heart(arr) {
}
function process_arr_heart(arr) {
let ctx = document.getElementById('canvasheart').getContext('2d');
let div = document.getElementById('divcanvasheart');
// Try to get the active canvas - check all possible canvas IDs
let ctx, div;
if (document.getElementById('canvasheart') && document.getElementById('canvasheart').offsetParent !== null) {
ctx = document.getElementById('canvasheart').getContext('2d');
div = document.getElementById('divcanvasheart');
} else if (document.getElementById('canvasheartFull') && document.getElementById('canvasheartFull').offsetParent !== null) {
ctx = document.getElementById('canvasheartFull').getContext('2d');
div = document.getElementById('divcanvasheartFull');
} else {
// Fallback to the first available canvas
ctx = (document.getElementById('canvasheart') || document.getElementById('canvasheartFull')).getContext('2d');
div = document.getElementById('divcanvasheart') || document.getElementById('divcanvasheartFull');
}
let reqpower = [];
let reqcadence = [];

View File

@@ -92,6 +92,18 @@ var pedometer = CMPedometer()
Server.server?.send(createString(sender: sender))
}
@objc public func setTotalKcal(totalKcal: Double) -> Void
{
var sender: String
if UIDevice.current.userInterfaceIdiom == .pad {
sender = "PAD"
} else {
sender = "PHONE"
}
WatchKitConnection.totalKcal = totalKcal;
Server.server?.send(createString(sender: sender))
}
@objc public func setCadence(cadence: Double) -> Void
{
var sender: String
@@ -129,7 +141,7 @@ var pedometer = CMPedometer()
}
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)#";
return "SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(WatchKitConnection.kcal)#TOTALKCAL=\(WatchKitConnection.totalKcal)#BCAD=\(WatchKitConnection.cadence)#SPD=\(WatchKitConnection.speed)#PWR=\(WatchKitConnection.power)#CAD=\(WatchKitConnection.stepCadence)#ODO=\(WatchKitConnection.distance)#";
}
@objc func updateHeartRate() {

View File

@@ -26,6 +26,7 @@ class WatchKitConnection: NSObject {
static var distance = 0.0
static var stepCadence = 0
static var kcal = 0.0
static var totalKcal = 0.0
static var speed = 0.0
static var power = 0.0
static var cadence = 0.0
@@ -55,6 +56,11 @@ class WatchKitConnection: NSObject {
WatchKitConnection.kcal = Kcal;
}
public func setTotalKCal(TotalKcal: Double) -> Void
{
WatchKitConnection.totalKcal = TotalKcal;
}
public func setDistance(Distance: Double) -> Void
{
WatchKitConnection.distance = Distance;

View File

@@ -31,6 +31,7 @@ protocol WorkoutTrackingProtocol {
public static var lastDateMetric = Date()
public static var distance = Double()
public static var kcal = Double()
public static var totalKcal = Double()
public static var steps = Double()
var sport: Int = 0
let healthStore = HKHealthStore()
@@ -100,6 +101,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
@@ -119,6 +121,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.workoutType()
])
}
@@ -166,22 +169,27 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
return
}
guard let quantityType = HKQuantityType.quantityType(
// Write active calories
guard let activeQuantityType = HKQuantityType.quantityType(
forIdentifier: .activeEnergyBurned) else {
return
}
let unit = HKUnit.kilocalorie()
let totalEnergyBurned = WorkoutTracking.kcal
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let activeEnergyBurned = WorkoutTracking.kcal
let activeQuantity = HKQuantity(unit: unit,
doubleValue: activeEnergyBurned)
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: startDate,
end: Date())
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
quantity: activeQuantity,
start: startDate,
end: Date())
workoutBuilder.add([sample]) {(success, error) in}
workoutBuilder.add([activeSample]) {(success, error) in
if let error = error {
SwiftDebug.qtDebug("WorkoutTracking active calories: " + error.localizedDescription)
}
}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance * 0.000621371
@@ -215,6 +223,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
SwiftDebug.qtDebug("WorkoutTracking: " + error.localizedDescription)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
} else {
@@ -270,14 +282,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
return
}
// No need to manually set values - the builder has added the samples
// and the workout now has steps and distance metrics built in
// You can access the data if needed:
if let workout = workout {
// Here you can use the workout as needed
// The steps and distance are now part of the workout's statistics
}
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}
@@ -286,7 +294,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
workoutInProgress = false;
}
@objc func addMetrics(power: Double, cadence: Double, speed: Double, kcal: Double, steps: Double, deviceType: UInt8, distance: Double) {
@objc func addMetrics(power: Double, cadence: Double, speed: Double, kcal: Double, steps: Double, deviceType: UInt8, distance: Double, totalKcal: Double) {
SwiftDebug.qtDebug("WorkoutTracking: GET DATA: \(Date())")
if(workoutInProgress == false && power > 0) {
@@ -298,6 +306,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let Speed = speed / 100;
WorkoutTracking.kcal = kcal
WorkoutTracking.totalKcal = totalKcal
WorkoutTracking.steps = steps
WorkoutTracking.distance = distance

View File

@@ -10,6 +10,7 @@ class lockscreen {
long heartRate();
long stepCadence();
void setKcal(double kcal);
void setTotalKcal(double totalKcal);
void setDistance(double distance);
void setSteps(double steps);
void setSpeed(double speed);
@@ -17,7 +18,7 @@ class lockscreen {
void setCadence(double cadence);
void startWorkout(unsigned short deviceType);
void stopWorkout();
void workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance);
void workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance, double totalKcal);
bool appleWatchAppInstalled();
// virtualbike

View File

@@ -129,6 +129,11 @@ void lockscreen::setKcal(double kcal)
[h setKcalWithKcal:kcal];
}
void lockscreen::setTotalKcal(double totalKcal)
{
[h setTotalKcalWithTotalKcal:totalKcal];
}
void lockscreen::setDistance(double distance)
{
[h setDistanceWithDistance:distance * 0.621371];
@@ -172,9 +177,9 @@ void lockscreen::virtualbike_setCadence(unsigned short crankRevolutions, unsigne
[_virtualbike updateCadenceWithCrankRevolutions:crankRevolutions LastCrankEventTime:lastCrankEventTime];
}
void lockscreen::workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance) {
void lockscreen::workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance, double totalKcal) {
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:watt cadence:cadence*2 speed:speed * 100 kcal:currentCalories steps:currentSteps deviceType:deviceType distance:currentDistance];
[workoutTracking addMetricsWithPower:watt cadence:cadence*2 speed:speed * 100 kcal:currentCalories steps:currentSteps deviceType:deviceType distance:currentDistance totalKcal:totalKcal];
}
void lockscreen::virtualbike_zwift_ios(bool disable_hr, bool garmin_bluetooth_compatibility, bool zwift_play_emulator, bool watt_bike_emulator)
@@ -226,7 +231,7 @@ double lockscreen::virtualbike_getPowerRequested()
bool lockscreen::virtualbike_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 CrankRevolutions, UInt16 LastCrankEventTime, signed short Gears, UInt16 currentCalories, UInt32 Distance)
{
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::BIKE distance:Distance];
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::BIKE distance:Distance totalKcal:0];
if(_virtualbike_zwift != nil)
return [_virtualbike_zwift updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt CrankRevolutions:CrankRevolutions LastCrankEventTime:LastCrankEventTime Gears:Gears];
@@ -236,7 +241,7 @@ bool lockscreen::virtualbike_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResi
bool lockscreen::virtualrower_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 CrankRevolutions, UInt16 LastCrankEventTime, UInt16 StrokesCount, UInt32 Distance, UInt16 KCal, UInt16 Pace)
{
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:KCal steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::ROWING distance:Distance];
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:KCal steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::ROWING distance:Distance totalKcal:0];
if(_virtualrower != nil)
return [_virtualrower updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt CrankRevolutions:CrankRevolutions LastCrankEventTime:LastCrankEventTime StrokesCount:StrokesCount Distance:Distance KCal:KCal Pace:Pace];
@@ -301,7 +306,7 @@ double lockscreen::virtualtreadmill_getRequestedSpeed()
bool lockscreen::virtualtreadmill_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 currentInclination, UInt64 currentDistance, unsigned short currentCalories, qint32 currentSteps, unsigned short elapsedSeconds)
{
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:currentSteps deviceType:bluetoothdevice::BLUETOOTH_TYPE::TREADMILL distance:currentDistance];
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:currentSteps deviceType:bluetoothdevice::BLUETOOTH_TYPE::TREADMILL distance:currentDistance totalKcal:0];
if(_virtualtreadmill_zwift != nil)
return [_virtualtreadmill_zwift updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt currentInclination:currentInclination currentDistance:currentDistance elapsedTimeSeconds:elapsedSeconds];

View File

@@ -22,6 +22,7 @@
#include <QQmlApplicationEngine>
#include <QSettings>
#include <QStandardPaths>
#include <QList>
#ifdef CHARTJS
#include <QtWebView/QtWebView>
#endif
@@ -604,6 +605,13 @@ int main(int argc, char *argv[]) {
}
#endif
// Register custom meta types used in queued invocations
qRegisterMetaType<SessionLine>("SessionLine");
qRegisterMetaType<QList<SessionLine>>("QList<SessionLine>");
qRegisterMetaType<bluetoothdevice::BLUETOOTH_TYPE>("bluetoothdevice::BLUETOOTH_TYPE");
qRegisterMetaType<uint32_t>("uint32_t");
qRegisterMetaType<FIT_SPORT>("FIT_SPORT");
qInstallMessageHandler(myMessageOutput);
qDebug() << QStringLiteral("version ") << app->applicationVersion();
foreach (QString s, settings.allKeys()) {

View File

@@ -899,7 +899,7 @@ ApplicationWindow {
}
ItemDelegate {
text: "version 2.20.8"
text: "version 2.20.11"
width: parent.width
}

View File

@@ -423,3 +423,44 @@ Women:
return (T * ((0.6309 * H) + (0.1988 * W) + (0.2017 * A) - 55.0969) / 4.184);
}
}
double metric::calculateBMR() {
// Calculate Basal Metabolic Rate using Mifflin-St Jeor equation
// BMR (kcal/day) for males: 10 * weight(kg) + 6.25 * height(cm) - 5 * age + 5
// BMR (kcal/day) for females: 10 * weight(kg) + 6.25 * height(cm) - 5 * age - 161
QSettings settings;
QString sex = settings.value(QZSettings::sex, QZSettings::default_sex).toString();
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
double age = settings.value(QZSettings::age, QZSettings::default_age).toDouble();
double height = settings.value(QZSettings::height, QZSettings::default_height).toDouble();
// Full Mifflin-St Jeor equation with height
if (sex.toLower().contains("female")) {
return (10 * weight) + (6.25 * height) - (5 * age) - 161;
} else {
return (10 * weight) + (6.25 * height) - (5 * age) + 5;
}
}
double metric::calculateActiveKCal(double totalKCal, double elapsed) {
QSettings settings;
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
if (!activeOnly) {
return totalKCal; // Return total calories if active-only mode is disabled
}
// Calculate BMR in calories per second
double bmrPerDay = calculateBMR();
double bmrPerSecond = bmrPerDay / (24.0 * 60.0 * 60.0); // Convert from calories/day to calories/second
// Calculate BMR calories for the elapsed time
double bmrForElapsed = bmrPerSecond * elapsed;
// Active calories = Total calories - BMR calories for the elapsed time
double activeKCal = totalKCal - bmrForElapsed;
// Ensure we don't return negative calories
return activeKCal > 0 ? activeKCal : 0;
}

View File

@@ -52,6 +52,8 @@ class metric {
static double calculateWeightLoss(double kcal);
static double calculateVO2Max(QList<SessionLine> *session);
static double calculateKCalfromHR(double HR_AVG, double elapsed);
static double calculateBMR();
static double calculateActiveKCal(double totalKCal, double elapsed);
static double powerPeak(QList<SessionLine> *session, int seconds);

View File

@@ -19,18 +19,28 @@ ColumnLayout {
property string profile_name: "default"
}
FileDialogClass.FileDialog {
id: fileDialogTrainProgram
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
profile_open_clicked(fileDialogTrainProgram.fileUrl)
fileDialogTrainProgram.close()
}
onRejected: {
console.log("Canceled")
fileDialogTrainProgram.close()
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialogClass.FileDialog {
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
console.log("You chose: " + fileUrl)
profile_open_clicked(fileUrl)
close()
// Destroy and recreate the dialog for next use
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
// Destroy the dialog
fileDialogLoader.active = false
}
}
}
}
@@ -257,7 +267,8 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
console.log("folder is " + rootItem.getWritableAppDir() + 'training')
fileDialogTrainProgram.visible = true
// Create a fresh FileDialog instance
fileDialogLoader.active = true
}
anchors {
bottom: parent.bottom

View File

@@ -110,6 +110,7 @@ SOURCES += \
$$PWD/fitdatabaseprocessor.cpp \
$$PWD/devices/trxappgateusbrower/trxappgateusbrower.cpp \
$$PWD/logwriter.cpp \
$$PWD/fitbackupwriter.cpp \
$$PWD/mqtt/qmqttauthenticationproperties.cpp \
$$PWD/mqtt/qmqttclient.cpp \
$$PWD/mqtt/qmqttconnection.cpp \
@@ -384,6 +385,7 @@ HEADERS += \
$$PWD/fitdatabaseprocessor.h \
$$PWD/inclinationresistancetable.h \
$$PWD/logwriter.h \
$$PWD/fitbackupwriter.h \
$$PWD/osc.h \
$$PWD/oscpp/client.hpp \
$$PWD/oscpp/detail/endian.hpp \
@@ -992,4 +994,4 @@ INCLUDEPATH += purchasing/inapp
WINRT_MANIFEST = AppxManifest.xml
VERSION = 2.20.8
VERSION = 2.20.11

File diff suppressed because it is too large Load Diff

View File

@@ -73,6 +73,7 @@ const QString QZSettings::bike_cadence_sensor = QStringLiteral("bike_cadence_sen
const QString QZSettings::run_cadence_sensor = QStringLiteral("run_cadence_sensor");
const QString QZSettings::rogue_echo_bike = QStringLiteral("rogue_echo_bike");
const QString QZSettings::bike_power_sensor = QStringLiteral("bike_power_sensor");
const QString QZSettings::bike_power_offset = QStringLiteral("bike_power_offset");
const QString QZSettings::heart_rate_belt_name = QStringLiteral("heart_rate_belt_name");
const QString QZSettings::default_heart_rate_belt_name = QStringLiteral("Disabled");
const QString QZSettings::heart_ignore_builtin = QStringLiteral("heart_ignore_builtin");
@@ -684,6 +685,8 @@ 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::ant_remote_control = QStringLiteral("ant_remote_control");
const QString QZSettings::ant_remote_control_device_number = QStringLiteral("ant_remote_control_device_number");
const QString QZSettings::proform_rower_sport_rl = QStringLiteral("proform_rower_sport_rl");
const QString QZSettings::strava_date_prefix = QStringLiteral("strava_date_prefix");
const QString QZSettings::race_mode = QStringLiteral("race_mode");
@@ -726,6 +729,7 @@ const QString QZSettings::proform_treadmill_705_cst = QStringLiteral("proform_tr
const QString QZSettings::zwift_click = QStringLiteral("zwift_click");
const QString QZSettings::hop_sport_hs_090h_bike = QStringLiteral("hop_sport_hs_090h_bike");
const QString QZSettings::zwift_play = QStringLiteral("zwift_play");
const QString QZSettings::zwift_play_vibration = QStringLiteral("zwift_play_vibration");
const QString QZSettings::nordictrack_treadmill_x14i = QStringLiteral("nordictrack_treadmill_x14i");
const QString QZSettings::zwift_api_poll = QStringLiteral("zwift_api_poll");
const QString QZSettings::tile_step_count_enabled = QStringLiteral("tile_step_count_enabled");
@@ -755,6 +759,7 @@ const QString QZSettings::default_strava_upload_mode = QStringLiteral("Always");
const QString QZSettings::proform_treadmill_705_cst_V78_239 = QStringLiteral("proform_treadmill_705_cst_V78_239");
const QString QZSettings::stryd_add_inclination_gain = QStringLiteral("stryd_add_inclination_gain");
const QString QZSettings::toorx_bike_srx_500 = QStringLiteral("toorx_bike_srx_500");
const QString QZSettings::toorxtreadmill_discovery_completed = QStringLiteral("toorxtreadmill_discovery_completed");
const QString QZSettings::atletica_lightspeed_treadmill = QStringLiteral("atletica_lightspeed_treadmill");
const QString QZSettings::peloton_treadmill_level = QStringLiteral("peloton_treadmill_level");
const QString QZSettings::peloton_treadmill_walk_level = QStringLiteral("peloton_treadmill_walk_level");
@@ -974,9 +979,13 @@ const QString QZSettings::tile_auto_virtual_shifting_climb_enabled = QStringLite
const QString QZSettings::tile_auto_virtual_shifting_climb_order = QStringLiteral("tile_auto_virtual_shifting_climb_order");
const QString QZSettings::tile_auto_virtual_shifting_sprint_enabled = QStringLiteral("tile_auto_virtual_shifting_sprint_enabled");
const QString QZSettings::tile_auto_virtual_shifting_sprint_order = QStringLiteral("tile_auto_virtual_shifting_sprint_order");
const QString QZSettings::chart_display_mode = QStringLiteral("chart_display_mode");
const QString QZSettings::calories_active_only = QStringLiteral("calories_active_only");
const QString QZSettings::calories_from_hr = QStringLiteral("calories_from_hr");
const QString QZSettings::height = QStringLiteral("height");
const uint32_t allSettingsCount = 798;
const uint32_t allSettingsCount = 807;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1027,6 +1036,7 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor},
{QZSettings::run_cadence_sensor, QZSettings::default_run_cadence_sensor},
{QZSettings::bike_power_sensor, QZSettings::default_bike_power_sensor},
{QZSettings::bike_power_offset, QZSettings::default_bike_power_offset},
{QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name},
{QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin},
{QZSettings::kcal_ignore_builtin, QZSettings::default_kcal_ignore_builtin},
@@ -1554,6 +1564,8 @@ QVariant allSettings[allSettingsCount][2] = {
{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::ant_remote_control, QZSettings::default_ant_remote_control},
{QZSettings::ant_remote_control_device_number, QZSettings::default_ant_remote_control_device_number},
{QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl},
{QZSettings::strava_date_prefix, QZSettings::default_strava_date_prefix},
{QZSettings::race_mode, QZSettings::default_race_mode},
@@ -1593,6 +1605,7 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::zwift_click, QZSettings::default_zwift_click},
{QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike},
{QZSettings::zwift_play, QZSettings::default_zwift_play},
{QZSettings::zwift_play_vibration, QZSettings::default_zwift_play_vibration},
{QZSettings::nordictrack_treadmill_x14i, QZSettings::default_nordictrack_treadmill_x14i},
{QZSettings::zwift_api_poll, QZSettings::default_zwift_api_poll},
{QZSettings::tile_step_count_enabled, QZSettings::default_tile_step_count_enabled},
@@ -1795,7 +1808,12 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::tile_auto_virtual_shifting_climb_order, QZSettings::default_tile_auto_virtual_shifting_climb_order},
{QZSettings::tile_auto_virtual_shifting_sprint_enabled, QZSettings::default_tile_auto_virtual_shifting_sprint_enabled},
{QZSettings::tile_auto_virtual_shifting_sprint_order, QZSettings::default_tile_auto_virtual_shifting_sprint_order},
{QZSettings::chart_display_mode, QZSettings::default_chart_display_mode},
{QZSettings::rogue_echo_bike, QZSettings::default_rogue_echo_bike},
{QZSettings::calories_active_only, QZSettings::default_calories_active_only},
{QZSettings::calories_from_hr, QZSettings::default_calories_from_hr},
{QZSettings::height, QZSettings::default_height},
{QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed},
};
void QZSettings::qDebugAllSettings(bool showDefaults) {

View File

@@ -257,6 +257,9 @@ class QZSettings {
static const QString bike_power_sensor;
static constexpr bool default_bike_power_sensor = false;
static const QString bike_power_offset;
static constexpr int default_bike_power_offset = 0;
static const QString heart_rate_belt_name;
static const QString default_heart_rate_belt_name;
@@ -1927,6 +1930,12 @@ class QZSettings {
static const QString ant_speed_gain;
static constexpr float default_ant_speed_gain = 1;
static const QString ant_remote_control;
static constexpr bool default_ant_remote_control = false;
static const QString ant_remote_control_device_number;
static constexpr int default_ant_remote_control_device_number = 0;
static const QString race_mode;
static constexpr bool default_race_mode = false;
@@ -2038,6 +2047,9 @@ class QZSettings {
static const QString zwift_play;
static constexpr bool default_zwift_play = false;
static const QString zwift_play_vibration;
static constexpr bool default_zwift_play_vibration = true;
static const QString nordictrack_treadmill_x14i;
static constexpr bool default_nordictrack_treadmill_x14i = false;
@@ -2115,6 +2127,9 @@ class QZSettings {
static const QString toorx_bike_srx_500;
static constexpr bool default_toorx_bike_srx_500 = false;
static const QString toorxtreadmill_discovery_completed;
static constexpr bool default_toorxtreadmill_discovery_completed = false;
static const QString atletica_lightspeed_treadmill;
static constexpr bool default_atletica_lightspeed_treadmill = false;
@@ -2660,6 +2675,30 @@ class QZSettings {
static const QString tile_auto_virtual_shifting_sprint_order;
static constexpr int default_tile_auto_virtual_shifting_sprint_order = 57;
/**
* @brief Chart display mode: 0 = both charts, 1 = heart rate only, 2 = power only
*/
static const QString chart_display_mode;
static constexpr int default_chart_display_mode = 0;
/**
* @brief Calculate only active calories (exclude basal metabolic rate)
*/
static const QString calories_active_only;
static constexpr bool default_calories_active_only = false;
/**
* @brief Calculate calories from heart rate instead of power
*/
static const QString calories_from_hr;
static constexpr bool default_calories_from_hr = false;
/**
* @brief User height in centimeters for BMR calculation
*/
static const QString height;
static constexpr double default_height = 175.0;
/**
* @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

@@ -4,6 +4,7 @@
#include <QDateTime>
#include <QGeoCoordinate>
#include <QTimer>
#include <QMetaType>
#include "definitions.h"
@@ -54,4 +55,7 @@ class SessionLine {
const QDateTime &time = QDateTime::currentDateTime());
};
// Make SessionLine known to Qt's meta-type system (required for queued connections/Q_ARG usage)
Q_DECLARE_METATYPE(SessionLine)
#endif // SESSIONLINE_H

View File

@@ -1198,6 +1198,15 @@ import Qt.labs.platform 1.1
property int tile_auto_virtual_shifting_sprint_order: 57
property string proform_rower_ip: ""
property string ftms_elliptical: "Disabled"
property bool calories_active_only: false
property real height: 175.0
property bool calories_from_hr: false
property int bike_power_offset: 0
property int chart_display_mode: 0
property bool zwift_play_vibration: true
property bool toorxtreadmill_discovery_completed: false
property bool ant_remote_control: false
property int ant_remote_control_device_number: 0
}
@@ -1317,6 +1326,62 @@ import Qt.labs.platform 1.1
color: Material.color(Material.Lime)
}
RowLayout {
spacing: 10
Label {
id: labelHeight
text: qsTr("Player Height") + "(" + (settings.miles_unit?"ft/in":"cm") + ")"
Layout.fillWidth: true
}
TextField {
id: heightTextField
text: settings.miles_unit ? Math.floor(settings.height / 30.48) + "'" + Math.round((settings.height % 30.48) / 2.54) + '"' : settings.height
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
//inputMethodHints: Qt.ImhFormattedNumbersOnly
onAccepted: {
if (settings.miles_unit) {
var parts = text.match(/(\d+)'(\d+)"/);
if (parts) {
settings.height = parseInt(parts[1]) * 30.48 + parseInt(parts[2]) * 2.54;
}
} else {
settings.height = text;
}
}
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
id: okHeightButton
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: {
if (settings.miles_unit) {
var parts = heightTextField.text.match(/(\d+)'(\d+)"/);
if (parts) {
settings.height = parseInt(parts[1]) * 30.48 + parseInt(parts[2]) * 2.54;
}
} else {
settings.height = heightTextField.text;
}
toast.show("Setting saved!");
}
}
}
Label {
text: qsTr("Enter your height for more accurate BMR and active calories calculation. Use centimeters for metric or feet'inches\" format (e.g., 5'10\") for imperial units.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
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 {
@@ -1721,7 +1786,63 @@ import Qt.labs.platform 1.1
}
Label {
text: qsTr("This prevents your bike or treadmill from sending its calories-burned calculation to QZ and defaults to QZs more accurate calculation.")
text: qsTr("This prevents your bike or treadmill from sending its calories-burned calculation to QZ and defaults to QZ's more accurate calculation.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
id: switchActiveCaloriesOnlyDelegate
text: qsTr("Calculate Active Calories Only")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.calories_active_only
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.calories_active_only = checked
}
Label {
text: qsTr("Enable to calculate only active calories (excluding basal metabolic rate) similar to Apple Watch. When disabled, total calories including BMR are calculated. This affects both display and Apple Health integration.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
id: switchCaloriesFromHRDelegate
text: qsTr("Calculate Calories from Heart Rate")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.calories_from_hr
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.calories_from_hr = checked
}
Label {
text: qsTr("Enable to calculate calories based on heart rate data instead of power. Requires heart rate sensor connection for accurate calorie estimation.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
@@ -2418,6 +2539,44 @@ import Qt.labs.platform 1.1
color: Material.color(Material.Lime)
}
RowLayout {
spacing: 10
Label {
id: labelBikePowerOffset
text: qsTr("Zwift Power Offset (W):")
Layout.fillWidth: true
}
TextField {
id: bikePowerOffsetTextField
text: settings.bike_power_offset
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
inputMethodHints: Qt.ImhDigitsOnly
onAccepted: settings.bike_power_offset = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
id: okBikePowerOffsetButton
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.bike_power_offset = bikePowerOffsetTextField.text; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("Add an offset in watts to the requested power from apps like Zwift. Positive values increase power, negative values decrease it. Default is 0.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
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 {
@@ -4475,6 +4634,69 @@ import Qt.labs.platform 1.1
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
text: qsTr("ANT+ Remote Control")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.ant_remote_control
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: { settings.ant_remote_control = checked; window.settings_restart_to_apply = true; }
}
Label {
text: qsTr("Enable ANT+ Remote Control support (like Zwift Click). Menu Up/Down buttons control gear shifting. Works with standard ANT+ Control Device remotes. Default: Disabled")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
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+ Remote Control Device Number (0 = any):")
Layout.fillWidth: true
}
TextField {
id: antRemoteControlDeviceNumberTextField
text: settings.ant_remote_control_device_number
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
inputMethodHints: Qt.ImhDigitsOnly
onAccepted: settings.ant_remote_control_device_number = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.ant_remote_control_device_number = antRemoteControlDeviceNumberTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("Set device number to 0 to accept any ANT+ remote control, or specify a specific device number to pair with only that remote.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
}
}
/*
@@ -4724,6 +4946,44 @@ import Qt.labs.platform 1.1
onClicked: openFloatingWindowBrowser();
}
RowLayout {
spacing: 10
Label {
id: labelChartDisplayMode
text: qsTr("Chart Display Mode:")
Layout.fillWidth: true
}
ComboBox {
id: chartDisplayModeComboBox
model: ["Both Charts", "Heart Rate Only", "Power Only"]
currentIndex: settings.chart_display_mode
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onActivated: {
console.log("chart_display_mode activated" + chartDisplayModeComboBox.currentIndex)
}
}
Button {
id: okChartDisplayModeButton
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.chart_display_mode = chartDisplayModeComboBox.currentIndex; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("Choose which charts to display in the footer: both heart rate and power charts, only heart rate chart, or only power chart.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
AccordionElement {
id: themesOptionsAccordion
title: qsTr("UI Themes")
@@ -5653,7 +5913,7 @@ import Qt.labs.platform 1.1
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
/*
IndicatorOnlySwitch {
id: pelotonBikeOCRDelegate
text: qsTr("Peloton Bike/Bike+ (Experimental)")
@@ -5680,7 +5940,7 @@ import Qt.labs.platform 1.1
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
}*/
}
}
@@ -8660,6 +8920,19 @@ import Qt.labs.platform 1.1
Layout.fillWidth: true
onClicked: { settings.iconsole_rower = checked; window.settings_restart_to_apply = true; }
}
IndicatorOnlySwitch {
text: qsTr("Toorx Treadmill Discovery Completed")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.toorxtreadmill_discovery_completed
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: { settings.toorxtreadmill_discovery_completed = checked; }
}
}
}
@@ -11409,6 +11682,33 @@ import Qt.labs.platform 1.1
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
text: qsTr("Zwift Play Vibration")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.zwift_play_vibration
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: { settings.zwift_play_vibration = checked; }
}
Label {
text: qsTr("Enable vibration feedback on Zwift Play controllers when changing gears. Default: enabled.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
text: qsTr("Buttons debouncing")
spacing: 0

View File

@@ -847,16 +847,10 @@ void trainprogram::scheduler() {
qDebug() << QStringLiteral("trainprogram ticks") << ticks << QStringLiteral("currentTimerJitter") << currentTimerJitter;
if(qAbs(currentTimerJitter) > 1000) {
// we are late...
if (currentTimerJitter > 1000) {
int seconds = currentTimerJitter / 1000;
ticks += seconds;
currentTimerJitter -= (seconds * 1000);
qDebug() << QStringLiteral("fixing jitter!") << seconds << ticks << currentTimerJitter;
} else {
// negative jitter, reset the counter without touching the ticks
currentTimerJitter = 0;
}
int seconds = currentTimerJitter / 1000;
ticks += seconds;
currentTimerJitter -= (seconds * 1000);
qDebug() << QStringLiteral("fixing jitter!") << seconds << ticks << currentTimerJitter;
}
double odometerFromTheDevice = bluetoothManager->device()->odometer();

View File

@@ -45,11 +45,21 @@ void WebServerInfoSender::acceptError(QAbstractSocket::SocketError socketError)
bool WebServerInfoSender::isRunning() const { return innerTcpServer && innerTcpServer->isListening(); }
bool WebServerInfoSender::send(const QString &data) {
if (isRunning() && !data.isEmpty()) {
QMutexLocker locker(&clientsMutex);
bool rv = true, oldrv = false;
for (QWebSocket *client : sendToClients) {
rv = client->sendTextMessage(data) > 0;
if (!oldrv)
oldrv = rv;
for (auto it = sendToClients.begin(); it != sendToClients.end();) {
if (it->isNull()) {
// Remove null pointers
it = sendToClients.erase(it);
} else {
QWebSocket *client = it->data();
if (client) {
rv = client->sendTextMessage(data) > 0;
if (!oldrv)
oldrv = rv;
}
++it;
}
}
return rv;
} else
@@ -67,6 +77,12 @@ void WebServerInfoSender::innerStop() {
innerTcpServer = 0;
httpServer = 0;
}
// Clear all collections
QMutexLocker locker(&clientsMutex);
clients.clear();
sendToClients.clear();
reply2Req.clear();
}
bool WebServerInfoSender::init() {
@@ -223,35 +239,62 @@ void WebServerInfoSender::onNewConnection() {
QWebSocket *pSocket = httpServer->nextPendingWebSocketConnection();
QUrl requestUrl = pSocket->requestUrl();
qDebug() << QStringLiteral("WebSocket connection") << requestUrl;
QMutexLocker locker(&clientsMutex);
// Handle different types of WebSocket connections based on the path
if (requestUrl.path() == QStringLiteral("/fetcher")) {
connect(pSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(processFetcherRequest(QString)));
connect(pSocket, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(processFetcherRawRequest(QByteArray)));
} else {
connect(pSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(processTextMessage(QString)));
connect(pSocket, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(processBinaryMessage(QByteArray)));
sendToClients << pSocket;
sendToClients << QPointer<QWebSocket>(pSocket);
}
connect(pSocket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()));
clients << pSocket;
// Store the WebSocket connection
clients << QPointer<QWebSocket>(pSocket);
}
void WebServerInfoSender::socketDisconnected() {
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
qDebug() << QStringLiteral("socketDisconnected:") << pClient;
if (pClient) {
clients.removeAll(pClient);
if (!sendToClients.removeAll(pClient)) {
QMutableHashIterator<QNetworkReply *, QPair<QJsonObject, QWebSocket *>> i(reply2Req);
while (i.hasNext()) {
i.next();
if (i.value().second == pClient) {
i.remove();
break;
}
QMutexLocker locker(&clientsMutex);
qDebug() << QStringLiteral("socketDisconnected:") << clients.size();
// Remove from sendToClients (QPointer)
for (auto it = sendToClients.begin(); it != sendToClients.end();) {
if (it->isNull() || it->data() == pClient) {
it = sendToClients.erase(it);
} else {
++it;
}
}
pClient->deleteLater();
qDebug() << QStringLiteral("socketDisconnected: sendToClients removed");
// Remove from reply2Req map
QMutableHashIterator<QNetworkReply *, QPair<QJsonObject, QWebSocket *>> i(reply2Req);
while (i.hasNext()) {
i.next();
if (i.value().second == pClient) {
qDebug() << QStringLiteral("socketDisconnected: reply2Req remove");
i.remove();
break;
}
}
// Remove from clients (QPointer)
for (auto it = clients.begin(); it != clients.end();) {
if (it->isNull() || it->data() == pClient) {
it = clients.erase(it);
} else {
++it;
}
}
qDebug() << QStringLiteral("socketDisconnected: cleanup completed");
}
}

View File

@@ -5,6 +5,8 @@
#include <QNetworkAccessManager>
#include <QNetworkCookie>
#include <QNetworkCookieJar>
#include <QMutex>
#include <QPointer>
class QNoCookieJar : public QNetworkCookieJar {
Q_OBJECT
@@ -36,11 +38,12 @@ class WebServerInfoSender : public TemplateInfoSender {
int port = 0;
QTcpServer *innerTcpServer = 0;
virtual bool init();
QList<QWebSocket *> clients;
QNetworkAccessManager *fetcher = 0;
QList<QWebSocket *> sendToClients;
QList<QPointer<QWebSocket>> clients;
QNetworkAccessManager *fetcher = nullptr;
QList<QPointer<QWebSocket>> sendToClients;
QHash<QString, QString> relative2Absolute;
QHash<QNetworkReply *, QPair<QJsonObject, QWebSocket *>> reply2Req;
mutable QMutex clientsMutex;
private slots:
void acceptError(QAbstractSocket::SocketError socketError);
void watchdogEvent();

View File

@@ -352,6 +352,11 @@ void zwiftclickremote::controllerStateChanged(QLowEnergyController::ControllerSt
void zwiftclickremote::vibrate(uint8_t pattern) {
if(!initDone) return;
QSettings settings;
bool zwift_play_vibration = settings.value(QZSettings::zwift_play_vibration, QZSettings::default_zwift_play_vibration).toBool();
if(!zwift_play_vibration) return;
QByteArray s = QByteArray::fromHex("1212080A060802100018");
s.append(pattern);
writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, (uint8_t *) s.data(), s.length(), "vibrate", false, false);