mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
73 Commits
JRNY-virtu
...
ant-remote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebe975321d | ||
|
|
397fe4f4de | ||
|
|
d28fc0753f | ||
|
|
597ef2259b | ||
|
|
0394e56cd6 | ||
|
|
704c7f1f80 | ||
|
|
678ac9d466 | ||
|
|
a8a6c5d736 | ||
|
|
e8408710df | ||
|
|
47825f0783 | ||
|
|
f7ce518812 | ||
|
|
f887a068b9 | ||
|
|
6ecbce4b87 | ||
|
|
9454d75f55 | ||
|
|
4063321418 | ||
|
|
bb88d58e47 | ||
|
|
7bc2f065c0 | ||
|
|
c773b45ddf | ||
|
|
eaf7db7813 | ||
|
|
a29f6350d0 | ||
|
|
65ad925d37 | ||
|
|
8fd486d582 | ||
|
|
8fa5dcadcb | ||
|
|
6abf6c9cfd | ||
|
|
b4603da714 | ||
|
|
b27e84de69 | ||
|
|
49337cbbc6 | ||
|
|
fe2f5e923c | ||
|
|
69f54dbd54 | ||
|
|
bc20ec0d8f | ||
|
|
278add7a11 | ||
|
|
6e90091883 | ||
|
|
ebda22d7b4 | ||
|
|
625ffb3932 | ||
|
|
fe6868911e | ||
|
|
1c73d15377 | ||
|
|
c33ee55efb | ||
|
|
56979a2122 | ||
|
|
3e1db8bfdf | ||
|
|
10fdc52446 | ||
|
|
23d23c40a5 | ||
|
|
90e8eeb983 | ||
|
|
dcf395ec46 | ||
|
|
d55cb553d3 | ||
|
|
b862d26bc3 | ||
|
|
d5e4f11849 | ||
|
|
5e9679f6c3 | ||
|
|
8799c447fb | ||
|
|
bcdb767b7e | ||
|
|
15e208d34c | ||
|
|
f16c41e6dd | ||
|
|
9110c55cb1 | ||
|
|
881e155cbc | ||
|
|
e2d187a7bd | ||
|
|
66821d884a | ||
|
|
73ad1dc46c | ||
|
|
c91a2d3ee5 | ||
|
|
87c0e95b01 | ||
|
|
174da2ac14 | ||
|
|
b61ba37b8f | ||
|
|
27333e7836 | ||
|
|
58a9e81bd8 | ||
|
|
d78e92f42f | ||
|
|
2a5eb7b057 | ||
|
|
ae5f70645a | ||
|
|
d26b14276e | ||
|
|
9166ce7218 | ||
|
|
5f0ec98b0c | ||
|
|
1bc7af0a88 | ||
|
|
df75d33ca6 | ||
|
|
34f7df6bfb | ||
|
|
1208b439fa | ||
|
|
14a9faa2ee |
273
.github/workflows/main.yml
vendored
273
.github/workflows/main.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
src/qdomyos-zwift.pro.user
|
||||
|
||||
.idea/
|
||||
|
||||
src/Makefile
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 + ");
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
438
src/android/src/AntRemoteControl.java
Normal file
438
src/android/src/AntRemoteControl.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")) ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
27
src/fitbackupwriter.cpp
Normal 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
28
src/fitbackupwriter.h
Normal 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
|
||||
246
src/homeform.cpp
246
src/homeform.cpp
@@ -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()) +
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -899,7 +899,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
text: "version 2.20.8"
|
||||
text: "version 2.20.11"
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
306
src/settings.qml
306
src/settings.qml
@@ -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 QZ’s 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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user