mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 23:41:50 +01:00
Compare commits
52 Commits
Mobi-Rower
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b28d17eb2d | ||
|
|
20e1d25ee2 | ||
|
|
a66efed67b | ||
|
|
f4d9f27566 | ||
|
|
8f7204df07 | ||
|
|
3ec146f53c | ||
|
|
e6b73efca0 | ||
|
|
d703eef4f4 | ||
|
|
6c3d00f123 | ||
|
|
1b75ee29c5 | ||
|
|
825d3a4d89 | ||
|
|
b3f1bbdd90 | ||
|
|
569036d855 | ||
|
|
f8ad368b45 | ||
|
|
1d58e3f3e6 | ||
|
|
4918b59911 | ||
|
|
faeec45119 | ||
|
|
1084e1529f | ||
|
|
7192733ace | ||
|
|
c4199ce9b6 | ||
|
|
4c0417c083 | ||
|
|
ee31c1a84f | ||
|
|
78523c3a5e | ||
|
|
9d7a6a8d2d | ||
|
|
de78308aba | ||
|
|
2213d3d9b1 | ||
|
|
181dbd6d64 | ||
|
|
c5b6236de7 | ||
|
|
b753296632 | ||
|
|
5aa2a310d3 | ||
|
|
8816fd105a | ||
|
|
4fb046d9dc | ||
|
|
1578e25aca | ||
|
|
bceabd916a | ||
|
|
009c806189 | ||
|
|
c22ee74ff4 | ||
|
|
0772026b7b | ||
|
|
5bd74260ab | ||
|
|
35f7ab636e | ||
|
|
4d8fd1ce1a | ||
|
|
b2a28d71e4 | ||
|
|
2db5683dd2 | ||
|
|
3b81b6d4ee | ||
|
|
2e0bd25a4a | ||
|
|
323c169067 | ||
|
|
1ff9da34db | ||
|
|
361874f1ea | ||
|
|
33b686bf3e | ||
|
|
74e1aba909 | ||
|
|
bf75b2bda0 | ||
|
|
80faa062e1 | ||
|
|
51808cc8a4 |
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(tshark:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git ls-tree:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
495
.github/workflows/main.yml
vendored
495
.github/workflows/main.yml
vendored
@@ -18,6 +18,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
window-build:
|
||||
@@ -29,41 +33,36 @@ jobs:
|
||||
- {python: false}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout googletest
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout MSIX-Toolkit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: microsoft/MSIX-Toolkit
|
||||
path: "src/MSIX-Toolkit/"
|
||||
ref: b82af826d29e93e4c85d34fad8a405b6c49905e7
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout qHttpServer
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: qt-labs/qthttpserver
|
||||
path: "src/qthttpserver"
|
||||
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.7'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: .github/workflows/main.yml
|
||||
- name: download python and paddleocr
|
||||
run: |
|
||||
python -VV
|
||||
@@ -103,15 +102,29 @@ jobs:
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-windows'
|
||||
|
||||
- name: Cache qthttpserver build
|
||||
id: cache-qthttpserver-mingw
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: src/qthttpserver
|
||||
key: qthttpserver-mingw-${{ runner.os }}-qt5.15.2-v1
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
if: steps.cache-qthttpserver-mingw.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Build qthttpserver
|
||||
if: steps.cache-qthttpserver-mingw.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
cd ../..
|
||||
|
||||
- name: Install qthttpserver
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
@@ -127,7 +140,7 @@ jobs:
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
cd ..
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -364,25 +377,21 @@ jobs:
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout googletest
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout qHttpServer
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: qt-labs/qthttpserver
|
||||
path: "src/qthttpserver"
|
||||
@@ -399,15 +408,29 @@ jobs:
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-linux'
|
||||
|
||||
- name: Cache qthttpserver build
|
||||
id: cache-qthttpserver-linux
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: src/qthttpserver
|
||||
key: qthttpserver-linux-${{ runner.os }}-qt5.15.2-v1
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
if: steps.cache-qthttpserver-linux.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Build qthttpserver
|
||||
if: steps.cache-qthttpserver-linux.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd src/qthttpserver
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
cd ../..
|
||||
|
||||
- name: Install qthttpserver
|
||||
run: |
|
||||
cd src/qthttpserver
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
@@ -588,16 +611,57 @@ 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
|
||||
- name: Cache Android NDK
|
||||
id: cache-ndk
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /usr/local/lib/android/sdk/ndk/21.4.7075529
|
||||
key: android-ndk-21.4.7075529-${{ runner.os }}
|
||||
|
||||
- name: Install NDK 21
|
||||
if: steps.cache-ndk.outputs.cache-hit != 'true'
|
||||
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"
|
||||
SDKMANAGER="/usr/local/lib/android/sdk/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}"
|
||||
|
||||
- name: Setup ccache
|
||||
run: |
|
||||
sudo apt-get install -y ccache
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV
|
||||
ccache --set-config=max_size=2G
|
||||
ccache --set-config=compression=true
|
||||
ccache --set-config=compression_level=6
|
||||
ccache --set-config=base_dir=${{ github.workspace }}
|
||||
ccache --set-config=sloppiness=pch_defines,time_macros,include_file_mtime,file_macro
|
||||
ccache --zero-stats
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .ccache
|
||||
key: ccache-android-ndk21-${{ hashFiles('src/**/*.cpp', 'src/**/*.h', 'src/*.pro') }}
|
||||
restore-keys: |
|
||||
ccache-android-ndk21-
|
||||
|
||||
- name: Setup NDK environment with ccache
|
||||
run: |
|
||||
NDK_PATH=/usr/local/lib/android/sdk/ndk/21.4.7075529
|
||||
NDK_BIN=$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/bin
|
||||
ln -sfn $NDK_PATH /usr/local/lib/android/sdk/ndk-bundle
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
echo "ANDROID_NDK=/usr/local/lib/android/sdk/ndk-bundle" >> $GITHUB_ENV
|
||||
echo "ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk-bundle" >> $GITHUB_ENV
|
||||
# Wrap NDK compilers with ccache for build caching
|
||||
if [ ! -f "$NDK_BIN/clang.real" ]; then
|
||||
mv $NDK_BIN/clang $NDK_BIN/clang.real
|
||||
mv $NDK_BIN/clang++ $NDK_BIN/clang++.real
|
||||
fi
|
||||
printf '#!/bin/bash\nexec /usr/bin/ccache %s/clang.real "$@"\n' "$NDK_BIN" > $NDK_BIN/clang
|
||||
printf '#!/bin/bash\nexec /usr/bin/ccache %s/clang++.real "$@"\n' "$NDK_BIN" > $NDK_BIN/clang++
|
||||
chmod +x $NDK_BIN/clang $NDK_BIN/clang++
|
||||
|
||||
- name: Generate secrets
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
@@ -608,25 +672,37 @@ jobs:
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> 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
|
||||
- name: Build qthttpserver for Android
|
||||
run: |
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
|
||||
- name: Build QZ for Android (4 ABIs)
|
||||
run: |
|
||||
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
|
||||
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-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-deployment-settings.json
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-android-${{ runner.os }}-v1
|
||||
restore-keys: |
|
||||
gradle-android-${{ runner.os }}-
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab
|
||||
|
||||
- name: Show ccache stats
|
||||
if: always()
|
||||
run: ccache --show-stats
|
||||
|
||||
- name: Archive apk binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -636,6 +712,7 @@ jobs:
|
||||
android-emulator-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: android-build
|
||||
continue-on-error: ${{ matrix.flaky || false }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -669,6 +746,7 @@ jobs:
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 13"
|
||||
flaky: true
|
||||
- api-level: 35
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
@@ -680,94 +758,78 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
|
||||
# Download the APK from the previous job
|
||||
- name: Download APK Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: fdroid-android-trial
|
||||
path: apk-debug
|
||||
|
||||
|
||||
- name: Setup Java for Android Emulator
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
# Use a smaller emulator configuration
|
||||
|
||||
- name: Run tests on emulator (${{ matrix.android-version }})
|
||||
uses: ReactiveCircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
arch: ${{ matrix.arch }}
|
||||
api-level: ${{ matrix.api-level }}
|
||||
profile: Nexus 6
|
||||
disable-animations: true
|
||||
script: |
|
||||
# Display available space
|
||||
df -h
|
||||
|
||||
|
||||
# List available files
|
||||
echo "Files in apk-debug directory:"
|
||||
ls -la apk-debug/
|
||||
|
||||
|
||||
# Install the APK
|
||||
adb install apk-debug/android-debug.apk
|
||||
|
||||
# Grant necessary permissions - comprehensive list for all Android APIs
|
||||
echo "Granting all required permissions..."
|
||||
|
||||
# Grant runtime (dangerous) permissions
|
||||
echo "Granting runtime permissions..."
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_FINE_LOCATION || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_COARSE_LOCATION || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_ADMIN || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_ADVERTISE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_CONNECT || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_SCAN || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.READ_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WRITE_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.MANAGE_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.CAMERA || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.RECORD_AUDIO || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.INTERNET || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_NETWORK_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_WIFI_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.CHANGE_WIFI_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WAKE_LOCK || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.VIBRATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.READ_PHONE_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.FOREGROUND_SERVICE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS || true
|
||||
|
||||
# Additional permissions for newer Android versions (12+)
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.POST_NOTIFICATIONS || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.SCHEDULE_EXACT_ALARM || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.USE_EXACT_ALARM || true
|
||||
|
||||
# Enable all app ops permissions
|
||||
|
||||
# Enable special app ops permissions
|
||||
adb shell appops set org.cagnulen.qdomyoszwift MANAGE_EXTERNAL_STORAGE allow || true
|
||||
adb shell appops set org.cagnulen.qdomyoszwift SYSTEM_ALERT_WINDOW allow || true
|
||||
adb shell appops set org.cagnulen.qdomyoszwift WRITE_SETTINGS allow || true
|
||||
|
||||
|
||||
echo "All permissions granted successfully"
|
||||
|
||||
|
||||
# Start the main activity
|
||||
adb shell am start -n org.cagnulen.qdomyoszwift/org.cagnulen.qdomyoszwift.CustomQtActivity
|
||||
|
||||
|
||||
# Wait for app to start
|
||||
sleep 90
|
||||
|
||||
|
||||
# Verify the app is running
|
||||
echo "Checking if app is running..."
|
||||
# Use different ps commands for different Android versions
|
||||
adb shell "ps -A 2>/dev/null || ps" > process_list.txt
|
||||
|
||||
|
||||
# Debug: show all processes to understand the format
|
||||
echo "=== All running processes ==="
|
||||
cat process_list.txt | head -20
|
||||
@@ -782,26 +844,26 @@ jobs:
|
||||
echo "=== Full recent logcat ==="
|
||||
adb logcat -d | tail -n 100
|
||||
echo "App is running successfully"
|
||||
|
||||
|
||||
# Take a screenshot for verification
|
||||
adb shell screencap -p /sdcard/screenshot.png
|
||||
adb pull /sdcard/screenshot.png
|
||||
|
||||
|
||||
# Test orientamento automatico con screenshot
|
||||
echo "Starting orientation test with automatic screenshots..."
|
||||
|
||||
# Screenshot iniziale (orientamento corrente)
|
||||
|
||||
# Screenshot iniziale (orientamento corrente)
|
||||
adb shell screencap -p /sdcard/screenshot_orientation_0.png
|
||||
adb pull /sdcard/screenshot_orientation_0.png
|
||||
|
||||
|
||||
# Loop per 3 rotazioni aggiuntive (90°, 180°, 270°)
|
||||
for i in 1 2 3; do echo "Rotating to orientation $i (90° * $i)"; adb shell settings put system user_rotation $i; sleep 5; echo "Taking screenshot for orientation $i"; adb shell screencap -p /sdcard/screenshot_orientation_$i.png; adb pull /sdcard/screenshot_orientation_$i.png; done
|
||||
|
||||
|
||||
echo "Orientation test completed - 4 screenshots captured"
|
||||
|
||||
|
||||
# Check if the package is installed
|
||||
adb shell pm list packages | grep org.cagnulen.qdomyoszwift
|
||||
|
||||
|
||||
# Save logcat for debugging
|
||||
echo "Saving logcat for analysis..."
|
||||
adb logcat -d > full_logcat.txt
|
||||
@@ -828,17 +890,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout googletest
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
@@ -867,6 +927,30 @@ jobs:
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/ios/5.15.2/binary/*.* ${{ github.workspace }}/output/ios/Qt/5.15.2/ios/lib/
|
||||
|
||||
- name: Setup ccache
|
||||
run: |
|
||||
brew install ccache
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV
|
||||
ccache --set-config=max_size=2G
|
||||
ccache --set-config=compression=true
|
||||
ccache --set-config=compression_level=6
|
||||
ccache --set-config=base_dir=${{ github.workspace }}
|
||||
ccache --set-config=sloppiness=pch_defines,time_macros,include_file_mtime,file_macro,clang_index_store
|
||||
ccache --zero-stats
|
||||
# Create wrapper scripts for Xcode (which uses absolute compiler paths, ignoring PATH)
|
||||
mkdir -p /tmp/ccache-wrappers
|
||||
printf '#!/bin/bash\nexec /opt/homebrew/bin/ccache clang "$@"\n' > /tmp/ccache-wrappers/clang
|
||||
printf '#!/bin/bash\nexec /opt/homebrew/bin/ccache clang++ "$@"\n' > /tmp/ccache-wrappers/clang++
|
||||
chmod +x /tmp/ccache-wrappers/clang /tmp/ccache-wrappers/clang++
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .ccache
|
||||
key: ccache-ios-${{ hashFiles('src/**/*.cpp', 'src/**/*.h', 'src/*.pro') }}
|
||||
restore-keys: |
|
||||
ccache-ios-
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd src
|
||||
@@ -879,15 +963,14 @@ jobs:
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
qmake CONFIG+=debug CONFIG+=iphonesimulator && make -j4
|
||||
qmake CONFIG+=debug CONFIG+=iphonesimulator \
|
||||
QMAKE_CC=/tmp/ccache-wrappers/clang \
|
||||
QMAKE_CXX=/tmp/ccache-wrappers/clang++ \
|
||||
&& make -j4
|
||||
|
||||
# causes iOS build on Mac to fail
|
||||
# - name: Commit moc files
|
||||
# uses: EndBug/add-and-commit@v9
|
||||
# with:
|
||||
# message: 'moc files added'
|
||||
# add: 'src/moc_*.cpp --force'
|
||||
# if: github.ref == 'refs/heads/master'
|
||||
- name: Show ccache stats
|
||||
if: always()
|
||||
run: ccache --show-stats
|
||||
|
||||
window-msvc2019-build:
|
||||
runs-on: windows-latest
|
||||
@@ -895,35 +978,33 @@ jobs:
|
||||
matrix:
|
||||
config:
|
||||
- {python: true}
|
||||
- {python: false}
|
||||
- {python: false}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout googletest
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout qHttpServer
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: qt-labs/qthttpserver
|
||||
path: "src/qthttpserver"
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.7'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: .github/workflows/main.yml
|
||||
- name: download python and paddleocr
|
||||
run: |
|
||||
python -VV
|
||||
@@ -937,7 +1018,7 @@ jobs:
|
||||
python -m pip install opencv-python
|
||||
python -m pip install numpy
|
||||
python -m pip install pywin32
|
||||
if: matrix.config.python
|
||||
if: matrix.config.python
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -959,17 +1040,31 @@ jobs:
|
||||
toolset: 14.2
|
||||
arch: x64
|
||||
|
||||
- name: Cache qthttpserver build
|
||||
id: cache-qthttpserver-msvc2019
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: src/qthttpserver
|
||||
key: qthttpserver-msvc2019-${{ runner.os }}-qt5.15.2-v1
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
if: steps.cache-qthttpserver-msvc2019.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Build qthttpserver
|
||||
if: steps.cache-qthttpserver-msvc2019.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
cd src\qthttpserver
|
||||
qmake
|
||||
nmake
|
||||
cd ../..
|
||||
|
||||
- name: Install qthttpserver
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
nmake install
|
||||
cd ../..
|
||||
cd ../..
|
||||
|
||||
- name: Secrets
|
||||
if: github.ref == 'refs/heads/master'
|
||||
@@ -983,17 +1078,27 @@ jobs:
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
cd ..
|
||||
|
||||
- name: Cache vcpkg
|
||||
id: cache-vcpkg-msvc2019
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ runner.workspace }}\vcpkg\installed
|
||||
key: vcpkg-msvc2019-x64-windows-protobuf-abseil-v1
|
||||
|
||||
- name: Clone vcpkg
|
||||
if: steps.cache-vcpkg-msvc2019.outputs.cache-hit != 'true'
|
||||
run: git clone https://github.com/microsoft/vcpkg.git
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Bootstrap vcpkg
|
||||
if: steps.cache-vcpkg-msvc2019.outputs.cache-hit != 'true'
|
||||
run: .\vcpkg\bootstrap-vcpkg.bat
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Create vcpkg.json
|
||||
if: steps.cache-vcpkg-msvc2019.outputs.cache-hit != 'true'
|
||||
working-directory: ${{ runner.workspace }}
|
||||
run: |
|
||||
echo '{
|
||||
@@ -1006,18 +1111,19 @@ jobs:
|
||||
],
|
||||
"builtin-baseline": "8c2fcacefba009d63672f9d137f192765e632c9f"
|
||||
}' > vcpkg.json
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-vcpkg-msvc2019.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
|
||||
qmake
|
||||
qmake
|
||||
nmake
|
||||
cd src/debug
|
||||
mkdir output
|
||||
@@ -1037,13 +1143,13 @@ jobs:
|
||||
cp ../../adb/* adb/
|
||||
cd ..
|
||||
cd appx
|
||||
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
|
||||
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
|
||||
if: matrix.config.python
|
||||
|
||||
|
||||
- name: Build without python
|
||||
run: |
|
||||
run: |
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
|
||||
qmake
|
||||
nmake
|
||||
@@ -1095,28 +1201,24 @@ jobs:
|
||||
window-msvc2019-aiserver-build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout googletest
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout qHttpServer
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: qt-labs/qthttpserver
|
||||
path: "src/qthttpserver"
|
||||
path: "src/qthttpserver"
|
||||
|
||||
- name: Install CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
@@ -1141,17 +1243,31 @@ jobs:
|
||||
toolset: 14.2
|
||||
arch: x64
|
||||
|
||||
- name: Cache qthttpserver build
|
||||
id: cache-qthttpserver-aiserver
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: src/qthttpserver
|
||||
key: qthttpserver-msvc2019-${{ runner.os }}-qt5.15.2-v1
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
if: steps.cache-qthttpserver-aiserver.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Build qthttpserver
|
||||
if: steps.cache-qthttpserver-aiserver.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
cd src\qthttpserver
|
||||
qmake
|
||||
nmake
|
||||
cd ../..
|
||||
|
||||
- name: Install qthttpserver
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
nmake install
|
||||
cd ../..
|
||||
cd ../..
|
||||
|
||||
- name: Secrets
|
||||
if: github.ref == 'refs/heads/master'
|
||||
@@ -1167,15 +1283,25 @@ jobs:
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
|
||||
- name: Cache vcpkg
|
||||
id: cache-vcpkg-aiserver
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ runner.workspace }}\vcpkg\installed
|
||||
key: vcpkg-msvc2019-x64-windows-protobuf-abseil-v1
|
||||
|
||||
- name: Clone vcpkg
|
||||
if: steps.cache-vcpkg-aiserver.outputs.cache-hit != 'true'
|
||||
run: git clone https://github.com/microsoft/vcpkg.git
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Bootstrap vcpkg
|
||||
if: steps.cache-vcpkg-aiserver.outputs.cache-hit != 'true'
|
||||
run: .\vcpkg\bootstrap-vcpkg.bat
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Create vcpkg.json
|
||||
if: steps.cache-vcpkg-aiserver.outputs.cache-hit != 'true'
|
||||
working-directory: ${{ runner.workspace }}
|
||||
run: |
|
||||
echo '{
|
||||
@@ -1188,8 +1314,9 @@ jobs:
|
||||
],
|
||||
"builtin-baseline": "8c2fcacefba009d63672f9d137f192765e632c9f"
|
||||
}' > vcpkg.json
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-vcpkg-aiserver.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
@@ -1197,7 +1324,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
|
||||
cd src
|
||||
echo "#define AISERVER" >> aiserver.h
|
||||
@@ -1237,19 +1364,19 @@ jobs:
|
||||
path: windows-msvc2019-ai-server-binary.zip
|
||||
|
||||
raspberry-pi-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
runs-on: ubuntu-22.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Secrets
|
||||
run: |
|
||||
@@ -1294,24 +1421,14 @@ jobs:
|
||||
path: src/qdomyos-zwift-32bit
|
||||
|
||||
raspberry-pi-build-and-image-64bit:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
runs-on: ubuntu-22.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:master
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.19.3
|
||||
|
||||
- name: Secrets
|
||||
run: |
|
||||
cd src
|
||||
@@ -1326,24 +1443,21 @@ jobs:
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
|
||||
- name: Install Qt5 build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
|
||||
- name: Build for Raspberry Pi 64-bit
|
||||
uses: docker://arm64v8/debian:bullseye-20241016
|
||||
with:
|
||||
args: >
|
||||
bash -c "
|
||||
set -ex &&
|
||||
for i in 1 2 3; do apt-get update && break || sleep 5; done &&
|
||||
for i in 1 2 3; do apt-get install -y --fix-missing build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql && break || sleep 5; done &&
|
||||
export QT_SELECT=qt5 &&
|
||||
export PATH=/usr/lib/qt5/bin:$PATH &&
|
||||
cd /github/workspace &&
|
||||
sed -i '/QtHttpServer/d' qdomyos-zwift.pro &&
|
||||
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/#include <QtHttpServer/\/\/#include <QtHttpServer/' {} + &&
|
||||
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/QHttpServer/\/\/QHttpServer/' {} + &&
|
||||
cat qdomyos-zwift.pro &&
|
||||
qmake &&
|
||||
make -j$(nproc)
|
||||
"
|
||||
run: |
|
||||
export QT_SELECT=qt5
|
||||
export PATH=/usr/lib/qt5/bin:$PATH
|
||||
sed -i '/QtHttpServer/d' qdomyos-zwift.pro
|
||||
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/#include <QtHttpServer/\/\/#include <QtHttpServer/' {} +
|
||||
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/QHttpServer/\/\/QHttpServer/' {} +
|
||||
cat qdomyos-zwift.pro
|
||||
qmake
|
||||
make -j$(nproc)
|
||||
|
||||
- name: Rename binary
|
||||
run: mv src/qdomyos-zwift src/qdomyos-zwift-64bit
|
||||
@@ -1352,7 +1466,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: raspberry-pi-binary-64bit
|
||||
path: src/qdomyos-zwift-64bit
|
||||
path: src/qdomyos-zwift-64bit
|
||||
|
||||
window-msvc2022-build:
|
||||
runs-on: windows-latest
|
||||
@@ -1370,9 +1484,11 @@ jobs:
|
||||
ref: refs/pull/1508/head
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.7'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: .github/workflows/main.yml
|
||||
- name: download python and paddleocr
|
||||
run: |
|
||||
python -VV
|
||||
@@ -1386,7 +1502,7 @@ jobs:
|
||||
python -m pip install opencv-python
|
||||
python -m pip install numpy
|
||||
python -m pip install pywin32
|
||||
if: matrix.config.python
|
||||
if: matrix.config.python
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -1432,17 +1548,27 @@ jobs:
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
cd ..
|
||||
|
||||
- name: Cache vcpkg
|
||||
id: cache-vcpkg-msvc2022
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ runner.workspace }}\vcpkg\installed
|
||||
key: vcpkg-msvc2022-x64-windows-protobuf-abseil-v1
|
||||
|
||||
- name: Clone vcpkg
|
||||
if: steps.cache-vcpkg-msvc2022.outputs.cache-hit != 'true'
|
||||
run: git clone https://github.com/microsoft/vcpkg.git
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Bootstrap vcpkg
|
||||
if: steps.cache-vcpkg-msvc2022.outputs.cache-hit != 'true'
|
||||
run: .\vcpkg\bootstrap-vcpkg.bat
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Create vcpkg.json
|
||||
if: steps.cache-vcpkg-msvc2022.outputs.cache-hit != 'true'
|
||||
working-directory: ${{ runner.workspace }}
|
||||
run: |
|
||||
echo '{
|
||||
@@ -1455,12 +1581,13 @@ jobs:
|
||||
],
|
||||
"builtin-baseline": "8c2fcacefba009d63672f9d137f192765e632c9f"
|
||||
}' > vcpkg.json
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-vcpkg-msvc2022.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Build
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
|
||||
101
CLAUDE.md
101
CLAUDE.md
@@ -98,6 +98,57 @@ The application follows a hierarchical device architecture:
|
||||
4. Update `qdomyos-zwift.pri` with new source files
|
||||
5. Add tests in `tst/Devices/` following existing patterns
|
||||
|
||||
### Adding Device Detection to bluetooth.cpp
|
||||
|
||||
**CRITICAL: Always verify device pattern conflicts before adding to bluetooth.cpp**
|
||||
|
||||
When adding a new device pattern to `src/devices/bluetooth.cpp`, you **MUST** follow these verification steps:
|
||||
|
||||
1. **Search for Similar Patterns**: Use grep/search to find all existing device patterns that might conflict
|
||||
- Search for device name prefixes (e.g., if adding "KS-NG-", search for all "KS-" patterns)
|
||||
- Check patterns in all device type cases (bikes, treadmills, ellipticals, rowers, etc.)
|
||||
|
||||
2. **Analyze Pattern Specificity**: Understand the pattern hierarchy
|
||||
- More specific patterns should be checked BEFORE less specific ones
|
||||
- Example: "KS-NGCH-" is more specific than "KS-NG-"
|
||||
- The order matters: devices are matched by the FIRST matching pattern in the if-else chain
|
||||
|
||||
3. **Check Case Order**: Verify the order of device type cases in bluetooth.cpp
|
||||
- Earlier cases take precedence over later cases
|
||||
- Ensure more specific patterns in earlier cases won't prevent your pattern from matching
|
||||
- Ensure your pattern won't incorrectly match devices intended for other cases
|
||||
|
||||
4. **Document Conflicts**: When conflicts exist, verify they are intentional
|
||||
- More specific patterns earlier in the chain should catch specific devices
|
||||
- Your pattern should only catch devices not matched by more specific patterns
|
||||
- Example: "KS-NGCH-X21C" (kingsmithR2Treadmill) should match before "KS-NG-" (horizontreadmill)
|
||||
|
||||
5. **Test Pattern Matching**: Consider these scenarios
|
||||
- Will your pattern match the intended device? (e.g., "KS-NG-X218")
|
||||
- Will it incorrectly match other devices? (e.g., "KS-NGCH-X21C")
|
||||
- Are there existing patterns that would match your device first?
|
||||
|
||||
**Example Verification Process:**
|
||||
|
||||
```bash
|
||||
# Search for similar patterns
|
||||
grep -n "KS-" src/devices/bluetooth.cpp
|
||||
|
||||
# Review each match for conflicts
|
||||
# - kingsmithR2Treadmill has "KS-NGCH-X21C" (line 1323)
|
||||
# - horizontreadmill has "KS-MC" (line 1562)
|
||||
# - Adding "KS-NG-" to horizontreadmill is safe because:
|
||||
# 1. "KS-NGCH-" patterns are more specific
|
||||
# 2. kingsmithR2Treadmill case comes first (line 1312 vs 1560)
|
||||
# 3. "KS-NG-X218" won't match "KS-NGCH-" patterns
|
||||
```
|
||||
|
||||
**Common Pitfalls:**
|
||||
- Adding a pattern without checking existing patterns
|
||||
- Not considering pattern order in the if-else chain
|
||||
- Adding overly broad patterns that match unintended devices
|
||||
- Not testing with actual device names
|
||||
|
||||
### Characteristics & Protocols
|
||||
- Bluetooth characteristics handlers in `src/characteristics/`
|
||||
- FTMS (Fitness Machine Service) protocol support
|
||||
@@ -368,7 +419,55 @@ The ProForm 995i implementation serves as the reference example:
|
||||
- Test device detection thoroughly using the existing test infrastructure
|
||||
- Consider platform differences when adding new features
|
||||
|
||||
## Updating Version Numbers
|
||||
|
||||
When releasing a new version of QDomyos-Zwift, you must update the version number in **3 files**:
|
||||
|
||||
### 1. Android Manifest
|
||||
**File**: `src/android/AndroidManifest.xml`
|
||||
|
||||
Update both `versionName` and `versionCode`:
|
||||
```xml
|
||||
<manifest ... android:versionName="X.XX.XX" android:versionCode="XXXX" ...>
|
||||
```
|
||||
|
||||
- `versionName`: The human-readable version (e.g., "2.20.26")
|
||||
- `versionCode`: Integer build number that must be incremented (e.g., 1274)
|
||||
|
||||
### 2. Main QML File
|
||||
**File**: `src/main.qml`
|
||||
|
||||
Update the version text displayed in the UI (around line 938):
|
||||
```qml
|
||||
ItemDelegate {
|
||||
text: "version X.XX.XX"
|
||||
width: parent.width
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Qt Project Include File
|
||||
**File**: `src/qdomyos-zwift.pri`
|
||||
|
||||
Update the VERSION variable (around line 1011):
|
||||
```pri
|
||||
VERSION = X.XX.XX
|
||||
```
|
||||
|
||||
### Version Numbering Convention
|
||||
|
||||
- **Major.Minor.Patch** format (e.g., 2.20.26)
|
||||
- **Build number** must always increment, never reuse
|
||||
- Update all 3 files together to keep versions synchronized
|
||||
|
||||
### iOS Version (Optional)
|
||||
|
||||
iOS version is managed through Xcode project variables:
|
||||
- `MARKETING_VERSION` in project.pbxproj (corresponds to versionName)
|
||||
- `CURRENT_PROJECT_VERSION` in project.pbxproj (corresponds to versionCode)
|
||||
|
||||
These are typically updated via Xcode IDE rather than manually editing files.
|
||||
|
||||
## Additional Memories
|
||||
|
||||
- When adding a new setting in QML (setting-tiles.qml), you must:
|
||||
* Add the property at the END of the properties list
|
||||
* Add the property at the END of the properties list
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.cagnulein.qdomyoszwift</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== QDomyos-Zwift CI Post Clone Script ==="
|
||||
echo "Installing Qt 5.15.2 EXACTLY and preparing environment"
|
||||
|
||||
# Exit if not on macOS (sanity check)
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
echo "ERROR: This script must run on macOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Apply Xcode Cloud network workarounds
|
||||
export HOMEBREW_NO_AUTO_UPDATE=1
|
||||
export GIT_HTTP_MAX_REQUESTS=1
|
||||
|
||||
# Check if Qt 5.15.2 is already installed
|
||||
if command -v qmake &> /dev/null; then
|
||||
QT_VERSION=$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)
|
||||
if [[ "$QT_VERSION" == "5.15.2" ]]; then
|
||||
echo "Qt 5.15.2 already installed - PERFECT!"
|
||||
export QT_DIR=$(dirname $(dirname $(which qmake)))
|
||||
export PATH="$QT_DIR/bin:$PATH"
|
||||
|
||||
# CRITICAL: Save Qt path to persistent file for next script
|
||||
echo "Saving existing Qt installation path for ci_pre_xcodebuild.sh..."
|
||||
echo "export QT_DIR=\"$QT_DIR\"" > /tmp/qt_env.sh
|
||||
echo "export PATH=\"$QT_DIR/bin:/tmp/Qt-5.15.2/ios/bin:/private/tmp/Qt-5.15.2/ios/bin:\$PATH\"" >> /tmp/qt_env.sh
|
||||
chmod +x /tmp/qt_env.sh
|
||||
else
|
||||
echo "WRONG Qt version found: $QT_VERSION"
|
||||
echo "MUST install Qt 5.15.2 exactly"
|
||||
# Uninstall wrong version
|
||||
brew uninstall --ignore-dependencies qt@5 qt || echo "No Qt to uninstall"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Force install Qt 5.15.2 EXACTLY
|
||||
if ! command -v qmake &> /dev/null || [[ "$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)" != "5.15.2" ]]; then
|
||||
echo "Installing Qt 5.15.2 EXACTLY - NO OTHER VERSION ACCEPTED"
|
||||
|
||||
# Method 1: Use aqt (Another Qt Installer) to get exact version
|
||||
echo "Installing aqt (Another Qt Installer) for exact Qt version control..."
|
||||
python3 -m pip install aqt || echo "aqt installation failed, trying homebrew method"
|
||||
|
||||
if command -v aqt &> /dev/null; then
|
||||
echo "Using aqt to install Qt 5.15.2 exactly..."
|
||||
aqt install-qt mac desktop 5.15.2 --outputdir /usr/local/Qt
|
||||
export QT_DIR="/usr/local/Qt/5.15.2/clang_64"
|
||||
export PATH="$QT_DIR/bin:$PATH"
|
||||
|
||||
# CRITICAL: Save Qt path to persistent file for next script
|
||||
echo "Saving aqt Qt installation path for ci_pre_xcodebuild.sh..."
|
||||
echo "export QT_DIR=\"/usr/local/Qt/5.15.2/clang_64\"" > /tmp/qt_env.sh
|
||||
echo "export PATH=\"/usr/local/Qt/5.15.2/clang_64/bin:/tmp/Qt-5.15.2/ios/bin:/private/tmp/Qt-5.15.2/ios/bin:\$PATH\"" >> /tmp/qt_env.sh
|
||||
chmod +x /tmp/qt_env.sh
|
||||
else
|
||||
echo "aqt failed, using precompiled Qt 5.15.2 from GitHub..."
|
||||
|
||||
# Download precompiled Qt 5.15.2 from your GitHub release
|
||||
echo "Downloading precompiled Qt 5.15.2 from GitHub..."
|
||||
cd /tmp
|
||||
curl -L "https://github.com/cagnulein/qt5.15.2/releases/download/qt-5.15.2/qt-5.15.2.tar.xz" -o qt-5.15.2.tar.xz
|
||||
|
||||
if [[ -f "qt-5.15.2.tar.xz" ]]; then
|
||||
echo "Extracting precompiled Qt 5.15.2..."
|
||||
tar -mxf qt-5.15.2.tar.xz
|
||||
|
||||
cd 5.15.2 || { echo "Extraction failed or directory not found"; exit 1; }
|
||||
|
||||
# Debug: Check extraction result
|
||||
echo "Contents after extraction:"
|
||||
ls -la
|
||||
|
||||
# Install to temp location (no sudo needed)
|
||||
echo "Setting up Qt 5.15.2..."
|
||||
mkdir -p /tmp/Qt-5.15.2
|
||||
|
||||
# Files are extracted directly - copy Qt directories
|
||||
echo "Files extracted directly, copying Qt directories..."
|
||||
|
||||
# Copy the Qt directories we need
|
||||
if [[ -d "ios" ]]; then
|
||||
cp -R ios /tmp/Qt-5.15.2/
|
||||
echo "Copied ios directory"
|
||||
fi
|
||||
|
||||
if [[ -d "clang_64" ]]; then
|
||||
cp -R clang_64 /tmp/Qt-5.15.2/
|
||||
echo "Copied clang_64 directory"
|
||||
fi
|
||||
|
||||
if [[ -d "qthttpserver" ]]; then
|
||||
cp -R qthttpserver /tmp/Qt-5.15.2/
|
||||
echo "Copied qthttpserver directory"
|
||||
fi
|
||||
|
||||
if [[ -f "sha1s.txt" ]]; then
|
||||
cp sha1s.txt /tmp/Qt-5.15.2/
|
||||
echo "Copied sha1s.txt"
|
||||
fi
|
||||
|
||||
# Set environment for iOS development - support both /tmp and /private/tmp
|
||||
export QT_DIR="/tmp/Qt-5.15.2/ios"
|
||||
export PATH="$QT_DIR/bin:$PATH"
|
||||
|
||||
# CRITICAL: Save Qt path to persistent file for next script
|
||||
echo "Saving Qt installation path for ci_pre_xcodebuild.sh..."
|
||||
echo "export QT_DIR=\"/tmp/Qt-5.15.2/ios\"" > /tmp/qt_env.sh
|
||||
echo "export PATH=\"/tmp/Qt-5.15.2/ios/bin:/private/tmp/Qt-5.15.2/ios/bin:\$PATH\"" >> /tmp/qt_env.sh
|
||||
chmod +x /tmp/qt_env.sh
|
||||
|
||||
echo "Qt 5.15.2 precompiled installation completed"
|
||||
|
||||
# CRITICAL: Fix hardcoded paths in .pri files
|
||||
# The Qt archive contains .pri files with absolute paths from local machine
|
||||
# Replace them with the Xcode Cloud installation path
|
||||
echo "Fixing hardcoded paths in Qt .pri files..."
|
||||
find /tmp/Qt-5.15.2 -name "*.pri" -type f -exec sed -i '' 's|/Users/cagnulein/Qt/5.15.2|/tmp/Qt-5.15.2|g' {} \;
|
||||
find /tmp/Qt-5.15.2 -name "*.pri" -type f -exec sed -i '' 's|/Users/cagnulein/Qt/5.15.2|/private/tmp/Qt-5.15.2|g' {} \;
|
||||
echo "Fixed paths in .pri files"
|
||||
|
||||
# CRITICAL: Download missing qmldbg libraries
|
||||
echo "Downloading missing qmldbg libraries..."
|
||||
cd /tmp
|
||||
|
||||
# Download libqmldbg_debugger.a
|
||||
echo "Downloading libqmldbg_debugger.a.zip..."
|
||||
curl -L -o libqmldbg_debugger.a.zip https://github.com/cagnulein/qt5.15.2/releases/download/qt-5.15.2/libqmldbg_debugger.a.zip
|
||||
unzip -o libqmldbg_debugger.a.zip
|
||||
|
||||
# Download libqmldbg_nativedebugger.a (from the old zip)
|
||||
echo "Downloading libqmldbg_nativedebugger.zip..."
|
||||
curl -L -o libqmldbg_nativedebugger.zip https://github.com/cagnulein/qt5.15.2/releases/download/qt-5.15.2/libqmldbg_debugger.zip
|
||||
unzip -o libqmldbg_nativedebugger.zip
|
||||
|
||||
echo "Contents after extraction:"
|
||||
ls -la libqmldbg*.a 2>/dev/null || echo "No .a files found in current directory"
|
||||
|
||||
# Ensure target directory exists
|
||||
mkdir -p /tmp/Qt-5.15.2/ios/plugins/qmltooling
|
||||
|
||||
# Move libqmldbg_debugger.a
|
||||
if [[ -f "libqmldbg_debugger.a" ]]; then
|
||||
mv libqmldbg_debugger.a /tmp/Qt-5.15.2/ios/plugins/qmltooling/
|
||||
echo "SUCCESS: Moved libqmldbg_debugger.a"
|
||||
else
|
||||
echo "FATAL ERROR: libqmldbg_debugger.a not found after extraction"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Move libqmldbg_nativedebugger.a (rename from _debug version if needed)
|
||||
if [[ -f "libqmldbg_nativedebugger.a" ]]; then
|
||||
mv libqmldbg_nativedebugger.a /tmp/Qt-5.15.2/ios/plugins/qmltooling/
|
||||
echo "SUCCESS: Moved libqmldbg_nativedebugger.a"
|
||||
elif [[ -f "libqmldbg_nativedebugger_debug.a" ]]; then
|
||||
# Use debug version as fallback (better than nothing)
|
||||
mv libqmldbg_nativedebugger_debug.a /tmp/Qt-5.15.2/ios/plugins/qmltooling/libqmldbg_nativedebugger.a
|
||||
echo "WARNING: Used libqmldbg_nativedebugger_debug.a as fallback"
|
||||
else
|
||||
echo "FATAL ERROR: libqmldbg_nativedebugger.a not found after extraction"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installed missing qmldbg libraries"
|
||||
rm -f libqmldbg_debugger.a.zip libqmldbg_nativedebugger.zip
|
||||
|
||||
# Verify httpserver module is now findable
|
||||
if [[ -f "/tmp/Qt-5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri" ]]; then
|
||||
echo "SUCCESS: httpserver module .pri file found"
|
||||
grep "QT.httpserver.libs" /tmp/Qt-5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri | head -1
|
||||
else
|
||||
echo "WARNING: httpserver .pri file not found at expected location"
|
||||
find /tmp/Qt-5.15.2 -name "*httpserver*.pri" 2>/dev/null || echo "No httpserver .pri files found"
|
||||
fi
|
||||
else
|
||||
echo "ERROR: Failed to download precompiled Qt from GitHub"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fi
|
||||
fi
|
||||
|
||||
# MANDATORY verification - FAIL if not 5.15.2
|
||||
echo "MANDATORY Qt 5.15.2 verification..."
|
||||
if command -v qmake &> /dev/null; then
|
||||
QT_VERSION=$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)
|
||||
if [[ "$QT_VERSION" != "5.15.2" ]]; then
|
||||
echo "FATAL ERROR: Qt version is $QT_VERSION, NOT 5.15.2"
|
||||
echo "Build CANNOT continue with wrong Qt version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SUCCESS: Qt 5.15.2 verified!"
|
||||
qmake -v
|
||||
|
||||
# Show Qt installation path
|
||||
QT_INSTALL_PATH=$(dirname $(dirname $(which qmake)))
|
||||
echo "Qt 5.15.2 installed at: $QT_INSTALL_PATH"
|
||||
|
||||
echo "Qt 5.15.2 installation completed successfully (Bluetooth already patched)"
|
||||
else
|
||||
echo "FATAL ERROR: No qmake found after installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# CRITICAL: Generate secret.h from Xcode Cloud environment variables
|
||||
echo "Generating secret.h from environment variables..."
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH/src"
|
||||
|
||||
echo "#define STRAVA_SECRET_KEY ${STRAVA_SECRET_KEY}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${PELOTON_SECRET_KEY}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${SMTP_USERNAME}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${SMTP_PASSWORD}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${SMTP_SERVER}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${INTERVALSICU_CLIENT_ID}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${INTERVALSICU_CLIENT_SECRET}" >> secret.h
|
||||
|
||||
echo "secret.h generated successfully"
|
||||
|
||||
# Generate cesium-key.js if cesiumkey is provided
|
||||
if [[ -n "${CESIUMKEY}" ]]; then
|
||||
echo "Generating cesium-key.js..."
|
||||
echo "${CESIUMKEY}" > inner_templates/googlemaps/cesium-key.js
|
||||
echo "cesium-key.js generated successfully"
|
||||
else
|
||||
echo "CESIUMKEY not provided, skipping cesium-key.js generation"
|
||||
fi
|
||||
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH"
|
||||
|
||||
# CRITICAL FIX: Disable legacy build locations to enable Swift Package support
|
||||
# This must be done BEFORE xcodebuild -resolvePackageDependencies is called
|
||||
echo "Configuring Xcode project to disable legacy build locations..."
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug"
|
||||
|
||||
# Create xcshareddata directory if it doesn't exist
|
||||
mkdir -p qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata
|
||||
|
||||
# Create WorkspaceSettings.xcsettings to disable legacy build locations
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildSystemType</key>
|
||||
<string>Latest</string>
|
||||
<key>BuildLocationStyle</key>
|
||||
<string>UseAppPreferences</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Create IDEWorkspaceChecks.plist
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Workspace settings created - modern build system enabled"
|
||||
|
||||
# Remove SYMROOT from project.pbxproj to disable legacy build locations
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Removing SYMROOT settings from project.pbxproj..."
|
||||
sed -i '' '/SYMROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' '/OBJROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# Fix all absolute paths: replace local machine path with Xcode Cloud path
|
||||
echo "Fixing absolute paths for Xcode Cloud..."
|
||||
sed -i '' 's|/Users/cagnulein/qdomyos-zwift/|/Volumes/workspace/repository/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
echo "SYMROOT removed and paths fixed - legacy build locations disabled"
|
||||
else
|
||||
echo "WARNING: project.pbxproj not found"
|
||||
fi
|
||||
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH"
|
||||
|
||||
echo "Post-clone setup completed successfully - Qt 5.15.2 EXACTLY installed"
|
||||
354
build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/ci_scripts/ci_pre_xcodebuild.sh
Executable file
354
build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/ci_scripts/ci_pre_xcodebuild.sh
Executable file
@@ -0,0 +1,354 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== QDomyos-Zwift CI Pre-Xcodebuild Script ==="
|
||||
echo "Running qmake to generate Xcode project with MOC files"
|
||||
|
||||
# CRITICAL: Load Qt environment from persistent file
|
||||
echo "Loading Qt environment from ci_post_clone.sh..."
|
||||
if [[ -f "/tmp/qt_env.sh" ]]; then
|
||||
echo "Found Qt environment file, loading..."
|
||||
source /tmp/qt_env.sh
|
||||
echo "Qt environment loaded from persistent file"
|
||||
echo "QT_DIR: $QT_DIR"
|
||||
echo "PATH: $PATH"
|
||||
else
|
||||
echo "WARNING: No Qt environment file found, trying to find Qt anyway..."
|
||||
fi
|
||||
|
||||
# Find Qt installation (should be 5.15.2 from post_clone script)
|
||||
if command -v qmake &> /dev/null; then
|
||||
QT_VERSION=$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)
|
||||
if [[ "$QT_VERSION" != "5.15.2" ]]; then
|
||||
echo "FATAL ERROR: Qt version is $QT_VERSION, expected 5.15.2"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using Qt 5.15.2 - CORRECT!"
|
||||
echo "qmake location: $(which qmake)"
|
||||
else
|
||||
echo "FATAL ERROR: qmake not found"
|
||||
echo "Current PATH: $PATH"
|
||||
echo "Listing /tmp for debugging:"
|
||||
ls -la /tmp/ | grep -i qt || echo "No Qt directories in /tmp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to project root directory
|
||||
cd ../..
|
||||
|
||||
# CRITICAL: Save absolute path to project root for later use
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
export PROJECT_ROOT
|
||||
echo "Project root saved: $PROJECT_ROOT"
|
||||
|
||||
# Verify we're in the correct directory
|
||||
if [[ ! -f "qdomyos-zwift.pro" ]]; then
|
||||
echo "ERROR: qdomyos-zwift.pro not found. Are we in the right directory?"
|
||||
pwd
|
||||
ls -la
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "Running qmake for iOS Debug build..."
|
||||
|
||||
# Run qmake to generate Xcode project and Makefiles
|
||||
# Use release config since precompiled Qt doesn't have debug libs
|
||||
# Force iphoneos SDK for device builds (not simulator)
|
||||
export QMAKE_XCODE_DEVELOPER_PATH="/Applications/Xcode.app/Contents/Developer"
|
||||
export QMAKE_IOS_DEPLOYMENT_TARGET=12.0
|
||||
qmake -spec macx-ios-clang CONFIG+=release CONFIG+=device CONFIG-=simulator CONFIG+=iphoneos "QMAKE_APPLE_DEVICE_ARCHS=arm64"
|
||||
|
||||
echo "qmake completed successfully"
|
||||
|
||||
# CRITICAL: Debug Qt installation before make
|
||||
echo "Debugging Qt installation before make..."
|
||||
echo "Checking Qt include directories:"
|
||||
ls -la /tmp/Qt-5.15.2/ios/include/ 2>/dev/null || echo "No /tmp/Qt-5.15.2/ios/include/"
|
||||
ls -la /private/tmp/Qt-5.15.2/ios/include/ 2>/dev/null || echo "No /private/tmp/Qt-5.15.2/ios/include/"
|
||||
|
||||
echo "Checking for QDebug specifically:"
|
||||
find /tmp/Qt-5.15.2/ios/include/ -name "*QDebug*" 2>/dev/null || echo "QDebug not found in /tmp/"
|
||||
find /private/tmp/Qt-5.15.2/ios/include/ -name "*QDebug*" 2>/dev/null || echo "QDebug not found in /private/tmp/"
|
||||
|
||||
echo "Checking QtCore include directory:"
|
||||
ls -la /tmp/Qt-5.15.2/ios/include/QtCore/ 2>/dev/null || echo "No QtCore in /tmp/"
|
||||
ls -la /private/tmp/Qt-5.15.2/ios/include/QtCore/ 2>/dev/null || echo "No QtCore in /private/tmp/"
|
||||
|
||||
# Setup build cache for faster compilation
|
||||
BUILD_CACHE_DIR="$HOME/Library/Caches/XcodeCloud/QDomyos-Zwift-Build"
|
||||
mkdir -p "$BUILD_CACHE_DIR"
|
||||
|
||||
# Check if we have cached object files
|
||||
if [[ -d "$BUILD_CACHE_DIR/objects" && -f "$BUILD_CACHE_DIR/build_hash.txt" ]]; then
|
||||
CURRENT_HASH=$(find "$PROJECT_ROOT/src" -name "*.cpp" -o -name "*.h" -o -name "*.mm" | sort | xargs cat | shasum -a 256 | cut -d' ' -f1)
|
||||
CACHED_HASH=$(cat "$BUILD_CACHE_DIR/build_hash.txt" 2>/dev/null || echo "none")
|
||||
|
||||
if [[ "$CURRENT_HASH" == "$CACHED_HASH" ]]; then
|
||||
echo "Source files unchanged, restoring build cache..."
|
||||
if cp -r "$BUILD_CACHE_DIR/objects/"* . 2>/dev/null; then
|
||||
echo "Build cache restored successfully"
|
||||
else
|
||||
echo "Cache restoration failed, will build from scratch"
|
||||
fi
|
||||
else
|
||||
echo "Source files changed, cache invalid"
|
||||
rm -rf "$BUILD_CACHE_DIR/objects" "$BUILD_CACHE_DIR/build_hash.txt"
|
||||
fi
|
||||
fi
|
||||
|
||||
# CRITICAL: Create fake xcodebuild BEFORE make to prevent build failures
|
||||
# During make, qmake will try to call xcodebuild which will fail due to code signing
|
||||
# We create a fake xcodebuild that just returns success
|
||||
echo "Creating fake xcodebuild to skip Xcode build during make..."
|
||||
mkdir -p /tmp/fake_xcode
|
||||
cat > /tmp/fake_xcode/xcodebuild << 'XCODE_EOF'
|
||||
#!/bin/bash
|
||||
echo "Skipping xcodebuild during make - will use correct project later"
|
||||
exit 0
|
||||
XCODE_EOF
|
||||
chmod +x /tmp/fake_xcode/xcodebuild
|
||||
|
||||
# Prepend fake xcodebuild to PATH so it's found first
|
||||
export PATH="/tmp/fake_xcode:$PATH"
|
||||
echo "Fake xcodebuild created and added to PATH"
|
||||
which xcodebuild
|
||||
|
||||
# CRITICAL: Run make to compile Qt project and generate MOC files
|
||||
echo "Running make to compile Qt project and generate MOC files..."
|
||||
# Use parallel compilation for faster builds
|
||||
make -j$(sysctl -n hw.ncpu)
|
||||
|
||||
echo "make completed successfully - MOC files generated"
|
||||
|
||||
# Remove fake xcodebuild from PATH
|
||||
export PATH="${PATH#/tmp/fake_xcode:}"
|
||||
echo "Fake xcodebuild removed from PATH"
|
||||
|
||||
# Cache the build results for next time
|
||||
echo "Caching build results..."
|
||||
mkdir -p "$BUILD_CACHE_DIR/objects"
|
||||
# Cache compiled object files and MOC files
|
||||
find . -name "*.o" -o -name "moc_*.cpp" -o -name "moc_*.h" | while read file; do
|
||||
cp "$file" "$BUILD_CACHE_DIR/objects/" 2>/dev/null || echo "Could not cache $file"
|
||||
done
|
||||
|
||||
# Store hash of source files for cache validation
|
||||
CURRENT_HASH=$(find "$PROJECT_ROOT/src" -name "*.cpp" -o -name "*.h" -o -name "*.mm" | sort | xargs cat | shasum -a 256 | cut -d' ' -f1)
|
||||
echo "$CURRENT_HASH" > "$BUILD_CACHE_DIR/build_hash.txt"
|
||||
echo "Build cache updated"
|
||||
|
||||
# NOW restore Xcode project and fix qmake corruption AFTER make
|
||||
echo "Restoring Xcode project from git AFTER make..."
|
||||
echo "qmake regenerates src/qdomyoszwift.xcodeproj without proper code signing"
|
||||
|
||||
# Return to project root for git operations (use absolute path)
|
||||
cd "$PROJECT_ROOT"
|
||||
echo "Back to project root: $(pwd)"
|
||||
|
||||
# Restore the build directory project (has WatchOS and proper code signing)
|
||||
git checkout -- build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/
|
||||
|
||||
echo "Build directory Xcode project restored from git"
|
||||
|
||||
# CRITICAL: Verify Qt labs calendar library exists
|
||||
echo "Verifying Qt labs calendar library..."
|
||||
if [[ -f "/tmp/Qt-5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a" ]]; then
|
||||
echo "SUCCESS: libqtlabscalendarplugin.a found"
|
||||
ls -lh /tmp/Qt-5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a
|
||||
else
|
||||
echo "ERROR: libqtlabscalendarplugin.a NOT FOUND"
|
||||
echo "Searching for calendar files..."
|
||||
find /tmp/Qt-5.15.2 -name "*calendar*" 2>/dev/null || echo "No calendar files found"
|
||||
fi
|
||||
|
||||
# CRITICAL: Fix ALL paths in Xcode project for Xcode Cloud compatibility
|
||||
# The project has absolute paths from local development that need to be converted
|
||||
echo "Fixing all paths in Xcode project for Xcode Cloud..."
|
||||
cd "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug"
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Converting local development paths to Xcode Cloud paths..."
|
||||
|
||||
# Fix all paths in correct order (specific to general)
|
||||
|
||||
# 1. Fix Qt library paths (most specific)
|
||||
sed -i '' 's|/Users/cagnulein/Qt/5\.15\.2/ios/|/tmp/Qt-5.15.2/ios/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' 's|../../Qt/5\.15\.2/ios/|/tmp/Qt-5.15.2/ios/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' 's|../Qt/5\.15\.2/ios/|/tmp/Qt-5.15.2/ios/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# 2. Fix source file paths to relative (must be before general fix)
|
||||
sed -i '' 's|/Users/cagnulein/qdomyos-zwift/src/|../src/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# 3. Fix all other absolute paths with general replacement
|
||||
sed -i '' 's|/Users/cagnulein/qdomyos-zwift/|/Volumes/workspace/repository/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# 4. Fix sourceTree for relative src paths (must be <group> not <absolute>)
|
||||
sed -i '' 's|path = "\.\./src/\([^"]*\)"; sourceTree = "<absolute>";|path = "../src/\1"; sourceTree = "<group>";|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
echo "Fixed all paths in project file"
|
||||
|
||||
# CRITICAL: Change scheme to Release configuration
|
||||
# The scheme is committed with Debug configuration but we need Release for Xcode Cloud
|
||||
echo "Changing scheme to Release configuration..."
|
||||
if [[ -f "qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme" ]]; then
|
||||
# Change TestAction from Debug to Release
|
||||
sed -i '' 's|<TestAction[^>]*buildConfiguration = "Debug"|<TestAction buildConfiguration = "Release"|g' qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme
|
||||
|
||||
# Change LaunchAction from Debug to Release
|
||||
sed -i '' 's|<LaunchAction[^>]*buildConfiguration = "Debug"|<LaunchAction buildConfiguration = "Release"|g' qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme
|
||||
|
||||
# Change AnalyzeAction from Debug to Release
|
||||
sed -i '' 's|<AnalyzeAction[^>]*buildConfiguration = "Debug"|<AnalyzeAction buildConfiguration = "Release"|g' qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme
|
||||
|
||||
echo "Scheme changed to Release configuration"
|
||||
else
|
||||
echo "WARNING: Scheme file not found"
|
||||
fi
|
||||
|
||||
# CRITICAL: Remove _debug suffix from Qt libraries
|
||||
# The Qt package only contains release libraries, not debug versions
|
||||
# Replace all lib*_debug.a references with lib*.a (release versions)
|
||||
echo "Replacing debug Qt libraries with release versions..."
|
||||
sed -i '' 's|lib\([a-zA-Z0-9_]*\)_debug\.a|lib\1.a|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' 's|-l\([a-zA-Z0-9_]*\)_debug|-l\1|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
echo "Replaced all _debug library references with release versions"
|
||||
|
||||
# Add ALL necessary Qt library search paths
|
||||
# qmake generates these but they might be missing from the committed project
|
||||
echo "Adding all Qt library search paths..."
|
||||
sed -i '' 's|\(LIBRARY_SEARCH_PATHS = (\)|\1\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/Qt/labs/calendar,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/Qt/labs/platform,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtCharts,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtWebView,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtPositioning,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtLocation,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtMultimedia,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/platforms,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/webview,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/texttospeech,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/geoservices,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/sqldrivers,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/mediaservice,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/playlistformats,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/audio,|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
echo "Added all necessary Qt library search paths"
|
||||
|
||||
# Verify the fix
|
||||
grep -c "libqtlabscalendarplugin.a" qdomyoszwift.xcodeproj/project.pbxproj && echo "qtlabscalendarplugin references found"
|
||||
grep -c "labs/calendar" qdomyoszwift.xcodeproj/project.pbxproj && echo "labs/calendar path references found"
|
||||
else
|
||||
echo "ERROR: project.pbxproj not found"
|
||||
exit 1
|
||||
fi
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# CRITICAL: Copy ALL generated files from src/ to build directory AFTER git restore
|
||||
# qmake/make generates many files (moc_*.cpp, qrc_*.cpp, *.o, *.json, qmltyperegistrations, etc.) in src/
|
||||
# but Xcode project expects them in build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/
|
||||
# This must happen AFTER git checkout to avoid wiping out the copied files
|
||||
echo "Copying ALL Qt-generated files from src/ to build directory..."
|
||||
cd "$PROJECT_ROOT/src"
|
||||
|
||||
# Copy all generated files (cpp, o, json, a) but exclude directories
|
||||
echo "Looking for generated files in: $(pwd)"
|
||||
find . -maxdepth 1 -type f \( -name "moc_*.cpp" -o -name "moc_*.cpp.json" -o -name "qrc_*.cpp" -o -name "*.o" -o -name "*.a" -o -name "*_qmltyperegistrations.*" -o -name "*.qmltypes" -o -name "*_metatypes.json" -o -name "*_plugin_import.cpp" \) -print -exec cp {} "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/" \;
|
||||
|
||||
echo "Generated files copied to build directory"
|
||||
|
||||
# CRITICAL FIX: Rename qdomyos-zwift_qmltyperegistrations.cpp to qdomyoszwift_qmltyperegistrations.cpp
|
||||
# qmake generates the file with a hyphen but Xcode project expects it without hyphen
|
||||
echo "Fixing qmltyperegistrations filename mismatch..."
|
||||
if [[ -f "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.cpp" ]]; then
|
||||
cp "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.cpp" \
|
||||
"$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift_qmltyperegistrations.cpp"
|
||||
echo "Renamed qdomyos-zwift_qmltyperegistrations.cpp -> qdomyoszwift_qmltyperegistrations.cpp"
|
||||
else
|
||||
echo "WARNING: qdomyos-zwift_qmltyperegistrations.cpp not found in build directory"
|
||||
fi
|
||||
|
||||
# Also handle .o file if it exists
|
||||
if [[ -f "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.o" ]]; then
|
||||
cp "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.o" \
|
||||
"$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift_qmltyperegistrations.o"
|
||||
echo "Renamed qdomyos-zwift_qmltyperegistrations.o -> qdomyoszwift_qmltyperegistrations.o"
|
||||
fi
|
||||
|
||||
echo "Verifying qdomyoszwift_qmltyperegistrations.cpp exists:"
|
||||
ls -la "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift_qmltyperegistrations.cpp" 2>&1
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# CRITICAL FIX: Delete corrupted project in src/ and symlink to the good one
|
||||
# qmake regenerates src/qdomyoszwift.xcodeproj without code signing during make
|
||||
# xcodebuild will build from src/, so we symlink to the correct project in build/
|
||||
echo "Removing corrupted Xcode project from src/ and creating symlink..."
|
||||
if [[ -d "src/qdomyoszwift.xcodeproj" ]]; then
|
||||
rm -rf src/qdomyoszwift.xcodeproj
|
||||
echo "Corrupted project removed from src/"
|
||||
fi
|
||||
|
||||
# Create symlink from src/ to the correct project in build/
|
||||
ln -s ../build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj src/qdomyoszwift.xcodeproj
|
||||
echo "Symlink created: src/qdomyoszwift.xcodeproj -> ../build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj"
|
||||
|
||||
# Verify symlink
|
||||
if [[ -L "src/qdomyoszwift.xcodeproj" ]]; then
|
||||
echo "Symlink verified successfully"
|
||||
ls -la src/qdomyoszwift.xcodeproj
|
||||
else
|
||||
echo "ERROR: Failed to create symlink"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Xcode project fix completed - symlink created to correct project with code signing"
|
||||
|
||||
# CRITICAL FIX: Disable legacy build locations to enable Swift Package support
|
||||
# Create workspace settings to force modern build system
|
||||
echo "Configuring workspace to disable legacy build locations..."
|
||||
cd build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug
|
||||
|
||||
# Create xcshareddata directory if it doesn't exist
|
||||
mkdir -p qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata
|
||||
|
||||
# Create WorkspaceSettings.xcsettings to disable legacy build locations
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildSystemType</key>
|
||||
<string>Latest</string>
|
||||
<key>BuildLocationStyle</key>
|
||||
<string>UseAppPreferences</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Create IDEWorkspaceChecks.plist
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Workspace settings created - modern build system enabled"
|
||||
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Removing SYMROOT settings from project.pbxproj..."
|
||||
|
||||
# Remove all SYMROOT lines completely (they cause the legacy build locations error)
|
||||
sed -i '' '/SYMROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# Also remove OBJROOT if present
|
||||
sed -i '' '/OBJROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# Ensure new build system is enabled
|
||||
sed -i '' 's/UseNewBuildSystem = NO/UseNewBuildSystem = YES/g' qdomyoszwift.xcodeproj/project.pbxproj || echo "New build system already enabled"
|
||||
|
||||
echo "Legacy build locations disabled - Swift packages now supported"
|
||||
else
|
||||
echo "ERROR: Xcode project not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the Xcode project exists and is properly configured
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Xcode project found and configured for Xcode Cloud"
|
||||
echo "Project size: $(du -sh qdomyoszwift.xcodeproj)"
|
||||
else
|
||||
echo "ERROR: Xcode project not found after qmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pre-xcodebuild setup completed successfully"
|
||||
@@ -557,6 +557,14 @@
|
||||
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
|
||||
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
|
||||
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
|
||||
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */; };
|
||||
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */; };
|
||||
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */; };
|
||||
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */; };
|
||||
87DBD7802F40601B00342F2B /* sportstechrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD77F2F40601B00342F2B /* sportstechrower.cpp */; };
|
||||
87DBD7812F40601B00342F2B /* moc_sportstechrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD77D2F40601B00342F2B /* moc_sportstechrower.cpp */; };
|
||||
87DBD7852F4060A200342F2B /* filesearcher.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD7832F4060A200342F2B /* filesearcher.cpp */; };
|
||||
87DBD7862F4060A200342F2B /* moc_filesearcher.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD7842F4060A200342F2B /* moc_filesearcher.cpp */; };
|
||||
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
|
||||
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
|
||||
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
|
||||
@@ -1660,6 +1668,18 @@
|
||||
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = thinkridercontroller.h; path = ../src/devices/thinkridercontroller/thinkridercontroller.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = thinkridercontroller.cpp; path = ../src/devices/thinkridercontroller/thinkridercontroller.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_thinkridercontroller.cpp; sourceTree = "<group>"; };
|
||||
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sunnyfitstepper.cpp; sourceTree = "<group>"; };
|
||||
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sunnyfitstepper.h; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sunnyfitstepper.cpp; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD77D2F40601B00342F2B /* moc_sportstechrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechrower.cpp; sourceTree = "<group>"; };
|
||||
87DBD77E2F40601B00342F2B /* sportstechrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportstechrower.h; path = ../src/devices/sportstechrower/sportstechrower.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD77F2F40601B00342F2B /* sportstechrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sportstechrower.cpp; path = ../src/devices/sportstechrower/sportstechrower.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD7822F4060A200342F2B /* filesearcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = filesearcher.h; path = ../src/filesearcher.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD7832F4060A200342F2B /* filesearcher.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = filesearcher.cpp; path = ../src/filesearcher.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD7842F4060A200342F2B /* moc_filesearcher.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_filesearcher.cpp; sourceTree = "<group>"; };
|
||||
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
@@ -2335,6 +2355,15 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87DBD7822F4060A200342F2B /* filesearcher.h */,
|
||||
87DBD7832F4060A200342F2B /* filesearcher.cpp */,
|
||||
87DBD7842F4060A200342F2B /* moc_filesearcher.cpp */,
|
||||
87DBD77D2F40601B00342F2B /* moc_sportstechrower.cpp */,
|
||||
87DBD77E2F40601B00342F2B /* sportstechrower.h */,
|
||||
87DBD77F2F40601B00342F2B /* sportstechrower.cpp */,
|
||||
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */,
|
||||
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */,
|
||||
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */,
|
||||
87A892572F0C173600811D95 /* sportsplusrower.cpp */,
|
||||
87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */,
|
||||
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */,
|
||||
@@ -2883,6 +2912,9 @@
|
||||
87F1BD652DBFBCE700416506 /* android_antbike.h */,
|
||||
87F1BD662DBFBCE700416506 /* android_antbike.cpp */,
|
||||
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */,
|
||||
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */,
|
||||
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */,
|
||||
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */,
|
||||
);
|
||||
name = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -3865,6 +3897,8 @@
|
||||
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
|
||||
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
|
||||
873CD22327EF8E18000131BC /* inappstoreqmltype.cpp in Compile Sources */,
|
||||
87DBD7852F4060A200342F2B /* filesearcher.cpp in Compile Sources */,
|
||||
87DBD7862F4060A200342F2B /* moc_filesearcher.cpp in Compile Sources */,
|
||||
87C481FA26DFA7C3006211AD /* eliterizer.cpp in Compile Sources */,
|
||||
873824EE27E647A9004F1B46 /* service.cpp in Compile Sources */,
|
||||
8772A0E625E43ADB0080718C /* trxappgateusbbike.cpp in Compile Sources */,
|
||||
@@ -3892,6 +3926,7 @@
|
||||
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
|
||||
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
|
||||
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
|
||||
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */,
|
||||
87097D2F275EA9A30020EE6F /* sportsplusbike.cpp in Compile Sources */,
|
||||
333C629F93DB3941862924F7 /* fit_field_base.cpp in Compile Sources */,
|
||||
87473A9827ECAA0500C203F5 /* moc_proformrower.cpp in Compile Sources */,
|
||||
@@ -3974,6 +4009,8 @@
|
||||
8768C8BD2BBC11C80099DBE1 /* console.c in Compile Sources */,
|
||||
8738249227E646E3004F1B46 /* characteristicnotifier2a63.cpp in Compile Sources */,
|
||||
8738249327E646E3004F1B46 /* characteristicwriteprocessor2ad9.cpp in Compile Sources */,
|
||||
87DBD7802F40601B00342F2B /* sportstechrower.cpp in Compile Sources */,
|
||||
87DBD7812F40601B00342F2B /* moc_sportstechrower.cpp in Compile Sources */,
|
||||
873824AD27E64706004F1B46 /* moc_characteristicnotifier.cpp in Compile Sources */,
|
||||
8768C9022BBC12B80099DBE1 /* socket_loopback_client.c in Compile Sources */,
|
||||
87C5F0B926285E5F0067A1B5 /* mimehtml.cpp in Compile Sources */,
|
||||
@@ -4134,6 +4171,8 @@
|
||||
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */,
|
||||
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */,
|
||||
87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */,
|
||||
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */,
|
||||
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */,
|
||||
877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */,
|
||||
877758B62C98629B00BB1697 /* sportstechelliptical.cpp in Compile Sources */,
|
||||
8762D5102601F7EA00F6F049 /* M3iNSQT.cpp in Compile Sources */,
|
||||
@@ -4198,6 +4237,7 @@
|
||||
874D272029AFA11F0007C079 /* apexbike.cpp in Compile Sources */,
|
||||
8798C8872733E103003148B3 /* strydrunpowersensor.cpp in Compile Sources */,
|
||||
87C5F0B626285E5F0067A1B5 /* quotedprintable.cpp in Compile Sources */,
|
||||
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */,
|
||||
87310B23266FBB78008BA0D6 /* moc_smartrowrower.cpp in Compile Sources */,
|
||||
EE29228550794460E7654533 /* moc_trxappgateusbtreadmill.cpp in Compile Sources */,
|
||||
3DB7B5F0CE1E2390CEFFC1E8 /* moc_virtualbike.cpp in Compile Sources */,
|
||||
@@ -4573,7 +4613,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4774,7 +4814,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5011,7 +5051,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5107,7 +5147,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5199,7 +5239,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5315,7 +5355,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5425,7 +5465,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5455,7 +5495,7 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
@@ -5516,7 +5556,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
@@ -5542,7 +5582,7 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "swift-protobuf",
|
||||
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "65e8f29b2d63c4e38e736b25c27b83e012159be8",
|
||||
"version": "1.25.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
200
ci-scripts/homebrew-formulas/qt5.rb
Normal file
200
ci-scripts/homebrew-formulas/qt5.rb
Normal file
@@ -0,0 +1,200 @@
|
||||
class Qt5 < Formula
|
||||
desc "Cross-platform application and UI framework"
|
||||
homepage "https://www.qt.io/"
|
||||
url "https://download.qt.io/official_releases/qt/5.15/5.15.2/single/qt-everywhere-src-5.15.2.tar.xz"
|
||||
mirror "https://mirrors.dotsrc.org/qtproject/archive/qt/5.15/5.15.2/single/qt-everywhere-src-5.15.2.tar.xz"
|
||||
mirror "https://mirrors.ocf.berkeley.edu/qt/archive/qt/5.15/5.15.2/single/qt-everywhere-src-5.15.2.tar.xz"
|
||||
sha256 "3a530d1b243b5dec00bc54937455471aaa3e56849d2593edb8ded07228202240"
|
||||
license all_of: ["GFDL-1.3-only", "GPL-2.0-only", "GPL-3.0-only", "LGPL-2.1-only", "LGPL-3.0-only"]
|
||||
|
||||
head "https://code.qt.io/qt/qt5.git", branch: "dev", shallow: false
|
||||
|
||||
livecheck do
|
||||
url "https://download.qt.io/official_releases/qt/5.15/"
|
||||
regex(%r{href=["']?v?(\d+(?:\.\d+)+)/?["' >]}i)
|
||||
end
|
||||
|
||||
bottle do
|
||||
sha256 cellar: :any, arm64_monterey: "8c734e90fb331e80242652aa19e5e427b7119a73b9abf99f2e1f8576b2ad5c51"
|
||||
sha256 cellar: :any, arm64_big_sur: "b23511e84ce7f3a2a3bf3d13eeb54b50b23c52b79b29ce31c6e4eb8ad1006eae"
|
||||
sha256 cellar: :any, monterey: "1481de79fb599b77b7c71788a07e4b5894e03b8cc5509b2a30e4c3e1f5ca4bcb"
|
||||
sha256 cellar: :any, big_sur: "1e2f35ffa5b10d5d81831f34b1a8ea3bbc9e7aab96e5a6dea5a433e3e9e7f6b0"
|
||||
sha256 cellar: :any, catalina: "9d6ad925c80a6bd4c7f7b7a3c0b5b42c21999da7b5f5b7ad3b9d96b98fbe89b5"
|
||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "9c7f25a7c5c5b5e4b44e7bb7b0c49e7de9c7d89e9d3b3f7e7e0b6c9b0f3b6e8d"
|
||||
end
|
||||
|
||||
depends_on "node" => :build
|
||||
depends_on "pkg-config" => :build
|
||||
depends_on "python@3.9" => :build
|
||||
|
||||
depends_on "freetype"
|
||||
depends_on "glib"
|
||||
depends_on "jpeg-turbo"
|
||||
depends_on "libpng"
|
||||
depends_on "pcre2"
|
||||
|
||||
uses_from_macos "gperf" => :build
|
||||
uses_from_macos "bison"
|
||||
uses_from_macos "flex"
|
||||
uses_from_macos "sqlite"
|
||||
|
||||
on_linux do
|
||||
depends_on "alsa-lib"
|
||||
depends_on "at-spi2-core"
|
||||
depends_on "expat"
|
||||
depends_on "fontconfig"
|
||||
depends_on "gstreamer"
|
||||
depends_on "gst-plugins-base"
|
||||
depends_on "harfbuzz"
|
||||
depends_on "icu4c"
|
||||
depends_on "krb5"
|
||||
depends_on "libdrm"
|
||||
depends_on "libevent"
|
||||
depends_on "libice"
|
||||
depends_on "libsm"
|
||||
depends_on "libvpx"
|
||||
depends_on "libxcomposite"
|
||||
depends_on "libxkbcommon"
|
||||
depends_on "libxkbfile"
|
||||
depends_on "libxrandr"
|
||||
depends_on "libxtst"
|
||||
depends_on "little-cms2"
|
||||
depends_on "mesa"
|
||||
depends_on "minizip"
|
||||
depends_on "nss"
|
||||
depends_on "opus"
|
||||
depends_on "pulseaudio"
|
||||
depends_on "sdl2"
|
||||
depends_on "snappy"
|
||||
depends_on "systemd"
|
||||
depends_on "wayland"
|
||||
depends_on "webp"
|
||||
depends_on "xcb-util"
|
||||
depends_on "xcb-util-image"
|
||||
depends_on "xcb-util-keysyms"
|
||||
depends_on "xcb-util-renderutil"
|
||||
depends_on "xcb-util-wm"
|
||||
depends_on "zstd"
|
||||
end
|
||||
|
||||
fails_with gcc: "5"
|
||||
|
||||
resource "qtwebengine" do
|
||||
url "https://code.qt.io/qt/qtwebengine.git",
|
||||
tag: "v5.15.2-lts",
|
||||
revision: "d6041c6e9bf0b9e9395ce33b35e1c9f90b8eb2d5"
|
||||
|
||||
# Add missing includes for newer Xcode
|
||||
# https://code.qt.io/cgit/qt/qtwebengine.git/commit/?id=96d4c79fe14b2b4b85b9b1b36b9b6b4c3e0ca9a0
|
||||
patch do
|
||||
url "https://raw.githubusercontent.com/Homebrew/formula-patches/7ae178a617d1e0eceb742557e63721af949bd28c/qt5/qtwebengine-xcode12.5.patch"
|
||||
sha256 "ac7bb0c1b8b6f29b3fb8218a4f91a9f4b3b6e3da6a9b4c5e1a8f3a5d4e0b2c3d"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
args = %W[
|
||||
-verbose
|
||||
-prefix #{prefix}
|
||||
-release
|
||||
-opensource -confirm-license
|
||||
-system-freetype
|
||||
-system-pcre
|
||||
-system-zlib
|
||||
-qt-libpng
|
||||
-qt-libjpeg
|
||||
-qt-sqlite
|
||||
-nomake examples
|
||||
-nomake tests
|
||||
-pkg-config
|
||||
-dbus-runtime
|
||||
-proprietary-codecs
|
||||
]
|
||||
|
||||
if OS.mac?
|
||||
args << "-no-rpath"
|
||||
args << "-system-png"
|
||||
else
|
||||
args << "-system-harfbuzz"
|
||||
args << "-system-sqlite"
|
||||
args << "-opengl es2"
|
||||
args << "-no-opengl"
|
||||
args << "-R#{lib}"
|
||||
# https://bugreports.qt.io/browse/QTBUG-71564
|
||||
args << "-no-avx2"
|
||||
args << "-no-avx512"
|
||||
args << "-no-feature-avx2"
|
||||
args << "-no-feature-avx512f"
|
||||
end
|
||||
|
||||
# Disable QtWebEngine on Apple Silicon
|
||||
if Hardware::CPU.arm?
|
||||
args << "-skip" << "qtwebengine"
|
||||
args << "-skip" << "qtwebkit"
|
||||
end
|
||||
|
||||
ENV.deparallelize
|
||||
system "./configure", *args
|
||||
system "make"
|
||||
ENV.deparallelize
|
||||
system "make", "install"
|
||||
|
||||
# Some config scripts will only find Qt in a "Frameworks" folder
|
||||
frameworks.install_symlink Dir["#{lib}/*.framework"]
|
||||
|
||||
# The pkg-config files installed suggest that headers can be found in the
|
||||
# `include` directory. Make this so by creating symlinks from `include` to
|
||||
# the Frameworks' Headers folders.
|
||||
Pathname.glob("#{lib}/*.framework/Headers") do |path|
|
||||
include.install_symlink path => path.parent.basename(".framework")
|
||||
end
|
||||
|
||||
# Move `*.app` bundles into `libexec` to expose them to `brew linkapps` and
|
||||
# because we don't like having them in `bin`.
|
||||
# (Note: This move breaks invocation of Assistant via the Help menu
|
||||
# of both Designer and Linguist as that relies on Assistant being in `bin`.)
|
||||
libexec.mkpath
|
||||
Pathname.glob("#{bin}/*.app") { |app| mv app, libexec }
|
||||
end
|
||||
|
||||
def caveats
|
||||
s = ""
|
||||
|
||||
if Hardware::CPU.arm?
|
||||
s += <<~EOS
|
||||
This version of Qt on Apple Silicon does not include QtWebEngine.
|
||||
EOS
|
||||
end
|
||||
|
||||
s
|
||||
end
|
||||
|
||||
test do
|
||||
(testpath/"hello.pro").write <<~EOS
|
||||
QT += core
|
||||
QT -= gui
|
||||
TARGET = hello
|
||||
CONFIG += console
|
||||
CONFIG -= app_bundle
|
||||
SOURCES += main.cpp
|
||||
EOS
|
||||
|
||||
(testpath/"main.cpp").write <<~EOS
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
qDebug() << "Hello World!";
|
||||
return 0;
|
||||
}
|
||||
EOS
|
||||
|
||||
system bin/"qmake", testpath/"hello.pro"
|
||||
system "make"
|
||||
assert_predicate testpath/"hello", :exist?
|
||||
|
||||
assert_match "Hello World!", shell_output("./hello")
|
||||
end
|
||||
end
|
||||
@@ -19,6 +19,15 @@ ios: {
|
||||
SUBDIRS = \
|
||||
src/qdomyos-zwift-lib.pro \
|
||||
src/qdomyos-zwift.pro
|
||||
|
||||
# Team signing configuration
|
||||
QMAKE_IOS_DEPLOYMENT_TARGET = 12.0
|
||||
QMAKE_DEVELOPMENT_TEAM = 6335M7T29D
|
||||
QMAKE_CODE_SIGN_IDENTITY = "iPhone Developer"
|
||||
QMAKE_CODE_SIGN_STYLE = Automatic
|
||||
|
||||
# Output directory configuration
|
||||
DESTDIR = $$PWD/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,32 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
property var selectedFileUrl: ""
|
||||
property bool isSearching: false
|
||||
|
||||
// Model for search results
|
||||
ListModel {
|
||||
id: searchResultsModel
|
||||
}
|
||||
|
||||
// Function to perform C++-based recursive search
|
||||
function searchRecursively(folderUrl, filter) {
|
||||
searchResultsModel.clear()
|
||||
|
||||
if (!filter || filter.trim() === "") {
|
||||
isSearching = false
|
||||
return
|
||||
}
|
||||
|
||||
isSearching = true
|
||||
|
||||
// Call C++ FileSearcher for fast recursive search
|
||||
var results = fileSearcher.searchRecursively(folderUrl, filter, ["*.xml", "*.zwo"])
|
||||
|
||||
// Populate search results model
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
searchResultsModel.append(results[i])
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
@@ -79,21 +105,36 @@ ColumnLayout {
|
||||
TextField {
|
||||
id: filterField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Search (recursive)..."
|
||||
|
||||
function updateFilter() {
|
||||
var text = filterField.text
|
||||
var filter = "*"
|
||||
for(var i = 0; i<text.length; i++)
|
||||
filter+= "[%1%2]".arg(text[i].toUpperCase()).arg(text[i].toLowerCase())
|
||||
filter+="*"
|
||||
folderModel.nameFilters = [filter + ".zwo", filter + ".xml"]
|
||||
var text = filterField.text.trim()
|
||||
|
||||
if (text === "") {
|
||||
// No filter - use normal folder browsing
|
||||
isSearching = false
|
||||
} else {
|
||||
// Trigger recursive C++ search
|
||||
var baseFolder = "file://" + rootItem.getWritableAppDir() + 'training'
|
||||
searchRecursively(baseFolder, text)
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: updateFilter()
|
||||
onTextChanged: {
|
||||
searchTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: filterField.updateFilter()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "←"
|
||||
visible: !isSearching
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
}
|
||||
@@ -114,62 +155,80 @@ ColumnLayout {
|
||||
showDirsFirst: true
|
||||
}
|
||||
|
||||
model: folderModel
|
||||
model: isSearching ? searchResultsModel : folderModel
|
||||
|
||||
delegate: Component {
|
||||
Rectangle {
|
||||
width: ListView.view.width
|
||||
height: 50
|
||||
color: ListView.isCurrentItem ? Material.color(Material.Green, Material.Shade800) : Material.backgroundColor
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: 50
|
||||
color: ListView.isCurrentItem ? Material.color(Material.Green, Material.Shade800) : Material.backgroundColor
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
// Determine item properties based on which model is active
|
||||
property bool isItemFolder: isSearching ? model.isFolder : folderModel.isFolder(index)
|
||||
property string itemFileName: isSearching ? model.fileName : folderModel.get(index, "fileName")
|
||||
property string itemFileUrl: isSearching ? model.filePath : (folderModel.get(index, 'fileUrl') || folderModel.get(index, 'fileURL'))
|
||||
property string itemRelativePath: isSearching ? model.relativePath : ""
|
||||
|
||||
Text {
|
||||
id: fileIcon
|
||||
text: folderModel.isFolder(index) ? "📁" : "📄"
|
||||
font.pixelSize: 24
|
||||
}
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
id: fileIcon
|
||||
text: isItemFolder ? "📁" : "📄"
|
||||
font.pixelSize: 24
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
id: fileName
|
||||
Layout.fillWidth: true
|
||||
text: !folderModel.isFolder(index) ?
|
||||
folderModel.get(index, "fileName").substring(0, folderModel.get(index, "fileName").length-4) :
|
||||
folderModel.get(index, "fileName")
|
||||
color: folderModel.isFolder(index) ? Material.color(Material.Orange) : "white"
|
||||
text: !isItemFolder ?
|
||||
itemFileName.substring(0, itemFileName.length-4) :
|
||||
itemFileName
|
||||
color: isItemFolder ? Material.color(Material.Orange) : "white"
|
||||
font.pixelSize: 16
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "›"
|
||||
font.pixelSize: 24
|
||||
Layout.fillWidth: true
|
||||
text: itemRelativePath
|
||||
color: Material.color(Material.Grey)
|
||||
visible: !ListView.isCurrentItem
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideMiddle
|
||||
visible: isSearching && itemRelativePath !== ""
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
list.currentIndex = index
|
||||
let fileUrl = folderModel.get(index, 'fileUrl') || folderModel.get(index, 'fileURL');
|
||||
Text {
|
||||
text: "›"
|
||||
font.pixelSize: 24
|
||||
color: Material.color(Material.Grey)
|
||||
visible: !ListView.isCurrentItem
|
||||
}
|
||||
}
|
||||
|
||||
if (folderModel.isFolder(index)) {
|
||||
// Navigate to folder
|
||||
folderModel.folder = fileUrl
|
||||
} else if (fileUrl) {
|
||||
// Load preview and show detail view
|
||||
console.log('Loading preview for: ' + fileUrl);
|
||||
trainprogram_preview(fileUrl)
|
||||
pendingWorkoutUrl = fileUrl
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
list.currentIndex = index
|
||||
|
||||
// Wait for preview to load then push detail view
|
||||
detailViewTimer.restart()
|
||||
if (isItemFolder) {
|
||||
// Navigate to folder (only in browse mode)
|
||||
if (!isSearching) {
|
||||
folderModel.folder = itemFileUrl
|
||||
}
|
||||
} else if (itemFileUrl) {
|
||||
// Load preview and show detail view
|
||||
trainprogram_preview(itemFileUrl)
|
||||
pendingWorkoutUrl = itemFileUrl
|
||||
|
||||
// Wait for preview to load then push detail view
|
||||
detailViewTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,93 +314,12 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
// WebView con grafico
|
||||
// Preview data is now loaded via WebSocket, no runJavaScript needed
|
||||
WebView {
|
||||
id: previewWebView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/workoutpreview/preview.html"
|
||||
|
||||
Component.onCompleted: {
|
||||
// Update workout after a short delay to ensure data is loaded
|
||||
updateTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: previewWebView.updateWorkout()
|
||||
}
|
||||
|
||||
function updateWorkout() {
|
||||
if (!rootItem.preview_workout_points) return;
|
||||
|
||||
// Build arrays for the workout data
|
||||
var watts = [];
|
||||
var speed = [];
|
||||
var inclination = [];
|
||||
var resistance = [];
|
||||
var cadence = [];
|
||||
|
||||
var hasWatts = false;
|
||||
var hasSpeed = false;
|
||||
var hasInclination = false;
|
||||
var hasResistance = false;
|
||||
var hasCadence = false;
|
||||
|
||||
for (var i = 0; i < rootItem.preview_workout_points; i++) {
|
||||
if (rootItem.preview_workout_watt && rootItem.preview_workout_watt[i] !== undefined && rootItem.preview_workout_watt[i] > 0) {
|
||||
watts.push({ x: i, y: rootItem.preview_workout_watt[i] });
|
||||
hasWatts = true;
|
||||
}
|
||||
if (rootItem.preview_workout_speed && rootItem.preview_workout_speed[i] !== undefined && rootItem.preview_workout_speed[i] > 0) {
|
||||
speed.push({ x: i, y: rootItem.preview_workout_speed[i] });
|
||||
hasSpeed = true;
|
||||
}
|
||||
if (rootItem.preview_workout_inclination && rootItem.preview_workout_inclination[i] !== undefined && rootItem.preview_workout_inclination[i] > -200) {
|
||||
inclination.push({ x: i, y: rootItem.preview_workout_inclination[i] });
|
||||
hasInclination = true;
|
||||
}
|
||||
if (rootItem.preview_workout_resistance && rootItem.preview_workout_resistance[i] !== undefined && rootItem.preview_workout_resistance[i] >= 0) {
|
||||
resistance.push({ x: i, y: rootItem.preview_workout_resistance[i] });
|
||||
hasResistance = true;
|
||||
}
|
||||
if (rootItem.preview_workout_cadence && rootItem.preview_workout_cadence[i] !== undefined && rootItem.preview_workout_cadence[i] > 0) {
|
||||
cadence.push({ x: i, y: rootItem.preview_workout_cadence[i] });
|
||||
hasCadence = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine device type based on available data
|
||||
var deviceType = 'bike'; // default
|
||||
|
||||
// Priority 1: If has resistance, it's a bike (regardless of inclination)
|
||||
if (hasResistance) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
// Priority 2: If has speed or inclination (without resistance), it's a treadmill
|
||||
else if (hasSpeed || hasInclination) {
|
||||
deviceType = 'treadmill';
|
||||
}
|
||||
// Priority 3: If has power or cadence (bike metrics), it's a bike
|
||||
else if (hasWatts || hasCadence) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
|
||||
// Call JavaScript function in the WebView
|
||||
var data = {
|
||||
points: rootItem.preview_workout_points,
|
||||
watts: watts,
|
||||
speed: speed,
|
||||
inclination: inclination,
|
||||
resistance: resistance,
|
||||
cadence: cadence,
|
||||
deviceType: deviceType,
|
||||
miles_unit: settings.value("miles_unit", false)
|
||||
};
|
||||
|
||||
runJavaScript("if(window.setWorkoutData) window.setWorkoutData(" + JSON.stringify(data) + ");");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ Page {
|
||||
property string heart_rate_belt_name: "Disabled"
|
||||
property bool garmin_companion: false
|
||||
property string filter_device: "Disabled"
|
||||
property bool weight_kg_unit: false
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
@@ -1181,7 +1182,7 @@ Page {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Weight (" + (settings.miles_unit ? "lbs" : "kg") + ")")
|
||||
text: qsTr("Weight (" + ((settings.miles_unit && !settings.weight_kg_unit) ? "lbs" : "kg") + ")")
|
||||
font.pixelSize: 20
|
||||
color: "white"
|
||||
}
|
||||
@@ -1189,13 +1190,13 @@ Page {
|
||||
SpinBox {
|
||||
id: weightSpinBox
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: settings.miles_unit ? 660 : 300 // 66.0 lbs or 30.0 kg
|
||||
to: settings.miles_unit ? 4400 : 2000 // 440.0 lbs or 200.0 kg
|
||||
value: settings.miles_unit ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
|
||||
from: (settings.miles_unit && !settings.weight_kg_unit) ? 660 : 300 // 66.0 lbs or 30.0 kg
|
||||
to: (settings.miles_unit && !settings.weight_kg_unit) ? 4400 : 2000 // 440.0 lbs or 200.0 kg
|
||||
value: (settings.miles_unit && !settings.weight_kg_unit) ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
|
||||
stepSize: 1
|
||||
editable: true
|
||||
|
||||
property real realValue: settings.miles_unit ? value / 22.0462 : value / 10
|
||||
property real realValue: (settings.miles_unit && !settings.weight_kg_unit) ? value / 22.0462 : value / 10
|
||||
|
||||
textFromValue: function(value, locale) {
|
||||
return Number(value / 10).toLocaleString(locale, 'f', 1)
|
||||
|
||||
@@ -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.23" android:versionCode="1264" 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.26" android:versionCode="1274" 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 -->
|
||||
|
||||
@@ -57,6 +57,7 @@ dependencies {
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.60'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
|
||||
implementation("com.garmin.connectiq:ciq-companion-app-sdk:2.2.0@aar")
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
Binary file not shown.
@@ -37,6 +37,9 @@ import java.util.UUID;
|
||||
|
||||
public class BleAdvertiser {
|
||||
private static final UUID SERVICE_UUID = UUID.fromString("00001826-0000-1000-8000-00805f9b34fb");
|
||||
// PM5 Concept2 UUIDs
|
||||
private static final UUID PM5_DISCOVERY_SERVICE_UUID = UUID.fromString("CE060000-43E5-11E4-916C-0800200C9A66");
|
||||
private static final UUID PM5_ROWING_SERVICE_UUID = UUID.fromString("CE060030-43E5-11E4-916C-0800200C9A66");
|
||||
private static final byte[] SERVICE_DATA_ROWER = {0x01, 0x10, 0x00};
|
||||
private static final byte[] SERVICE_DATA_TREADMILL = {0x01, 0x01, 0x00};
|
||||
|
||||
@@ -63,6 +66,36 @@ public class BleAdvertiser {
|
||||
}
|
||||
}
|
||||
|
||||
public static void startAdvertisingRowerPM5(Context context) {
|
||||
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (bluetoothManager != null) {
|
||||
android.bluetooth.le.BluetoothLeAdvertiser advertiser = bluetoothManager.getAdapter().getBluetoothLeAdvertiser();
|
||||
|
||||
AdvertiseSettings settings = new AdvertiseSettings.Builder()
|
||||
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
||||
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
|
||||
.setConnectable(true)
|
||||
.build();
|
||||
|
||||
// PM5 advertising data - device name only (to save space)
|
||||
// Full name "PM5 430000000" is set via Bluetooth adapter
|
||||
AdvertiseData advertiseData = new AdvertiseData.Builder()
|
||||
.setIncludeDeviceName(true)
|
||||
.build();
|
||||
|
||||
// Scan response contains the PM5 discovery service UUID (CE060000)
|
||||
// This is how real PM5 devices advertise - UUID in scan response
|
||||
AdvertiseData scanResponse = new AdvertiseData.Builder()
|
||||
.addServiceUuid(new ParcelUuid(PM5_DISCOVERY_SERVICE_UUID))
|
||||
.build();
|
||||
|
||||
if (advertiser != null) {
|
||||
QLog.d("BleAdvertiser", "Starting PM5 advertising with scan response UUID: " + PM5_DISCOVERY_SERVICE_UUID.toString());
|
||||
advertiser.startAdvertising(settings, advertiseData, scanResponse, advertiseCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void startAdvertisingTreadmill(Context context) {
|
||||
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (bluetoothManager != null) {
|
||||
|
||||
@@ -82,10 +82,13 @@ public class Garmin {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
connectIQ = ConnectIQ.getInstance(c, ConnectIQ.IQConnectType.WIRELESS);
|
||||
// Create wrapped context BEFORE getInstance to ensure all SDK operations use it
|
||||
context = createWrappedContext(c);
|
||||
|
||||
connectIQ = ConnectIQ.getInstance(context, ConnectIQ.IQConnectType.WIRELESS);
|
||||
|
||||
// init a wrapped SDK with fix for "Cannot cast to Long" issue viz https://forums.garmin.com/forum/developers/connect-iq/connect-iq-bug-reports/158068-?p=1278464#post1278464
|
||||
context = initializeConnectIQWrapped(c, connectIQ, false, new ConnectIQ.ConnectIQListener() {
|
||||
initializeConnectIQWithContext(connectIQ, false, new ConnectIQ.ConnectIQListener() {
|
||||
|
||||
@Override
|
||||
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
|
||||
@@ -158,12 +161,8 @@ public class Garmin {
|
||||
connectIQ.sendMessage(getDevice(), getApp(), message, listener);
|
||||
}
|
||||
|
||||
private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
if (connectIQ instanceof ConnectIQAdbStrategy) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
return context;
|
||||
}
|
||||
Context wrappedContext = new ContextWrapper(context) {
|
||||
private static Context createWrappedContext(Context context) {
|
||||
return new ContextWrapper(context) {
|
||||
private HashMap<BroadcastReceiver, BroadcastReceiver> receiverToWrapper = new HashMap<>();
|
||||
|
||||
@Override
|
||||
@@ -190,6 +189,18 @@ public class Garmin {
|
||||
if (wrappedReceiver != null) super.unregisterReceiver(wrappedReceiver);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void initializeConnectIQWithContext(ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
}
|
||||
|
||||
private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
if (connectIQ instanceof ConnectIQAdbStrategy) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
return context;
|
||||
}
|
||||
Context wrappedContext = createWrappedContext(context);
|
||||
connectIQ.initialize(wrappedContext, autoUI, listener);
|
||||
return wrappedContext;
|
||||
}
|
||||
|
||||
@@ -20,32 +20,73 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
QLog.d(TAG, "onReceive intent " + intent.getAction());
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
}
|
||||
|
||||
try {
|
||||
QLog.d(TAG, "=== GARMIN INTENT DEBUG START ===");
|
||||
QLog.d(TAG, "Action: " + intent.getAction());
|
||||
|
||||
// Log all extras in the intent
|
||||
if (intent.getExtras() != null) {
|
||||
QLog.d(TAG, "Extras bundle: " + intent.getExtras());
|
||||
try {
|
||||
for (String key : intent.getExtras().keySet()) {
|
||||
Object value = intent.getExtras().get(key);
|
||||
QLog.d(TAG, " Extra[" + key + "] = " + value + " (type: " + (value != null ? value.getClass().getName() : "null") + ")");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error iterating extras: " + e.toString());
|
||||
}
|
||||
} else {
|
||||
QLog.d(TAG, "No extras in intent");
|
||||
}
|
||||
|
||||
// Process known actions
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing SEND_MESSAGE_STATUS");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing OPEN_APPLICATION");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing DEVICE_STATUS");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.INCOMING_MESSAGE".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing INCOMING_MESSAGE");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else {
|
||||
QLog.d(TAG, "Unknown action, no processing");
|
||||
}
|
||||
|
||||
QLog.d(TAG, "Calling wrapped receiver.onReceive()");
|
||||
receiver.onReceive(context, intent);
|
||||
} catch (IllegalArgumentException | BufferUnderflowException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
QLog.d(TAG, "=== GARMIN INTENT DEBUG END (success) ===");
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "=== EXCEPTION in wrapper (BEFORE or DURING receiver call) ===");
|
||||
QLog.e(TAG, "Exception type: " + e.getClass().getName());
|
||||
QLog.e(TAG, "Exception message: " + e.getMessage());
|
||||
QLog.e(TAG, "Stack trace:", e);
|
||||
QLog.e(TAG, "=== GARMIN INTENT DEBUG END (error) ===");
|
||||
}
|
||||
}
|
||||
|
||||
private static void replaceIQDeviceById(Intent intent, String extraName) {
|
||||
try {
|
||||
QLog.d(TAG, " Attempting to get Parcelable for extra: " + extraName);
|
||||
IQDevice device = intent.getParcelableExtra(extraName);
|
||||
if (device != null) {
|
||||
// Logger.logDebug("Replacing " + device.describeContents() + " " + device.getFriendlyName() + " by " + device.getDeviceIdentifier() );
|
||||
intent.putExtra(extraName, device.getDeviceIdentifier());
|
||||
QLog.d(TAG, " Found IQDevice: " + device.getFriendlyName() + " (ID: " + device.getDeviceIdentifier() + ")");
|
||||
long deviceId = device.getDeviceIdentifier();
|
||||
intent.putExtra(extraName, deviceId);
|
||||
QLog.d(TAG, " Replaced IQDevice with Long ID: " + deviceId);
|
||||
} else {
|
||||
QLog.d(TAG, " Extra '" + extraName + "' is null or not an IQDevice");
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
// It's already a long, i.e. on the simulator.
|
||||
QLog.d(TAG, " ClassCastException for '" + extraName + "': " + e.toString());
|
||||
QLog.d(TAG, " (Extra is already a Long, probably on simulator)");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, " Unexpected exception in replaceIQDeviceById: " + e.toString());
|
||||
QLog.e(TAG, " Stack trace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,12 +201,15 @@ void bluetooth::finished() {
|
||||
|
||||
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
|
||||
|
||||
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
|
||||
|
||||
if ((!heartRateBeltFound && !heartRateBeltAvaiable()) || (!ftmsAccessoryFound && !ftmsAccessoryAvaiable()) ||
|
||||
(!cscFound && !cscSensorAvaiable()) || (!powerSensorFound && !powerSensorAvaiable()) ||
|
||||
(!eliteRizerFound && !eliteRizerAvaiable()) || (!eliteSterzoSmartFound && !eliteSterzoSmartAvaiable()) ||
|
||||
(!fitmetriaFanfitFound && !fitmetriaFanfitAvaiable()) ||
|
||||
(!zwiftDeviceFound && !zwiftDeviceAvaiable()) ||
|
||||
(!sramDeviceFound && !sramDeviceAvaiable())) {
|
||||
(!sramDeviceFound && !sramDeviceAvaiable()) ||
|
||||
(!thinkriderDeviceFound && !thinkriderDeviceAvaiable())) {
|
||||
|
||||
// force heartRateBelt off
|
||||
forceHeartBeltOffForTimeout = true;
|
||||
@@ -336,6 +339,16 @@ bool bluetooth::sramDeviceAvaiable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bluetooth::thinkriderDeviceAvaiable() {
|
||||
|
||||
Q_FOREACH (QBluetoothDeviceInfo b, devices) {
|
||||
if (b.name().toUpper().startsWith("THINK VS") || b.name().toUpper().startsWith("THINKRIDER")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool bluetooth::powerSensorAvaiable() {
|
||||
|
||||
@@ -437,6 +450,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
|
||||
bool zwiftDeviceFound =
|
||||
!settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool() && !settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool();
|
||||
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
|
||||
bool fitmetriaFanfitFound =
|
||||
!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
|
||||
bool toorx_ftms = settings.value(QZSettings::toorx_ftms, QZSettings::default_toorx_ftms).toBool();
|
||||
@@ -549,6 +563,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
|
||||
sramDeviceFound = sramDeviceAvaiable();
|
||||
}
|
||||
if(!thinkriderDeviceFound) {
|
||||
|
||||
thinkriderDeviceFound = thinkriderDeviceAvaiable();
|
||||
}
|
||||
if (!ftmsAccessoryFound) {
|
||||
|
||||
ftmsAccessoryFound = ftmsAccessoryAvaiable();
|
||||
@@ -681,7 +699,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
#endif
|
||||
|
||||
bool searchDevices = (heartRateBeltFound && ftmsAccessoryFound && cscFound && powerSensorFound && eliteRizerFound &&
|
||||
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound) ||
|
||||
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound && sramDeviceFound && thinkriderDeviceFound) ||
|
||||
forceHeartBeltOffForTimeout;
|
||||
|
||||
if (searchDevices) {
|
||||
@@ -1108,6 +1126,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("SCH_590E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SCH411/510E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MRK-E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FEIER-EM-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MX-AS ")) ||
|
||||
(b.name().startsWith(QStringLiteral("Domyos-EL")) && settings.value(QZSettings::domyos_elliptical_fmts, QZSettings::default_domyos_elliptical_fmts).toBool()) ||
|
||||
@@ -1250,7 +1269,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("E98")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("XG400")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("E98S"))) &&
|
||||
!soleElliptical && filter) {
|
||||
!soleElliptical && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
soleElliptical = new soleelliptical(noWriteResistance, noHeartService, testResistance,
|
||||
@@ -1384,11 +1403,12 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
this->signalBluetoothDeviceConnected(shuaA5Treadmill);
|
||||
} else if (((b.name().toUpper().startsWith(QStringLiteral("TRUE")) &&
|
||||
!(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) && b.name().length() == 19)) ||
|
||||
!(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) && b.name().length() == 19) &&
|
||||
!(b.name().toUpper().startsWith(QStringLiteral("TRUE 1000")))) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ASSAULT TREADMILL ")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WDWAY")) && b.name().length() == 8) || // WdWay179
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && !gem_module_inclination && !deviceHasService(b, QBluetoothUuid((quint16)0x1814)) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) &&
|
||||
!trueTreadmill && filter) {
|
||||
!trueTreadmill && ftms_treadmill.contains(QZSettings::default_ftms_treadmill) && !horizonTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trueTreadmill = new truetreadmill(noWriteResistance, noHeartService);
|
||||
@@ -1486,6 +1506,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(lifefitnessTreadmill);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("HORIZON")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("HZ_T101-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("HZ_7.0AT-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("AFG SPORT")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT2541")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && (gem_module_inclination || deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) ||
|
||||
@@ -1520,7 +1541,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM4500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM6500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("RUNN ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BFX_T")) ||
|
||||
(b.name().toUpper().startsWith("3G PRO ")) ||
|
||||
@@ -1533,7 +1554,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("FIT-TM-")) || // FIT-TM- treadmill with real inclination
|
||||
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
|
||||
b.name().toUpper().startsWith(QStringLiteral("TRUE 1000")) ||
|
||||
(b.name().toUpper().startsWith("SCHWINN 810")) ||
|
||||
(b.name().toUpper().startsWith("URTM")) || // Urevo Spacewalk 3S Model URTM024
|
||||
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
|
||||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
|
||||
(b.name().toUpper().startsWith("SF-T")) || // Sunny Fitness Treadmill
|
||||
@@ -1545,6 +1568,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) ||
|
||||
(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("KS-NG-"))) || // Kingsmith X218 / Walking Pad
|
||||
(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))) ||
|
||||
@@ -1824,6 +1848,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
|
||||
(b.name().toUpper().startsWith("RIVO COG")) ||
|
||||
(b.name().toUpper().startsWith("RAVE")) ||
|
||||
(b.name().toUpper().startsWith("TOPUTURE-")) ||
|
||||
(b.name().toUpper().startsWith("BESP-")) || // FITFIU BESP 250 indoor bike
|
||||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith("SPORT01-") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // Labgrey Magnetic Exercise Bike https://www.amazon.co.uk/dp/B0CXMF1NPY?_encoding=UTF8&psc=1&ref=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&ref_=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&social_share=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&skipTwisterOG=1
|
||||
@@ -1836,8 +1861,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
|
||||
(b.name().toUpper().startsWith("INCONDI")) || // inCondi S150i
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10) ||
|
||||
(b.name().toUpper().startsWith("JFICCYCLE"))) &&
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10)) &&
|
||||
ftms_rower.contains(QZSettings::default_ftms_rower) &&
|
||||
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -2016,6 +2040,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(echelonStairclimber, &echelonstairclimber::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
echelonStairclimber->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(echelonStairclimber);
|
||||
} else if (b.name().toUpper().startsWith(QLatin1String("SF-S")) &&
|
||||
!sunnyfitStepper && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
sunnyfitStepper = new sunnyfitstepper(this->pollDeviceTime, noConsole, noHeartService);
|
||||
emit deviceConnected(b);
|
||||
connect(sunnyfitStepper, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
connect(sunnyfitStepper, &sunnyfitstepper::debug, this, &bluetooth::debug);
|
||||
connect(sunnyfitStepper, &sunnyfitstepper::speedChanged, this, &bluetooth::speedChanged);
|
||||
connect(sunnyfitStepper, &sunnyfitstepper::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
sunnyfitStepper->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(sunnyfitStepper);
|
||||
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) ||
|
||||
@@ -2230,6 +2267,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// SLOT(inclinationChanged(double)));
|
||||
sportsTechElliptical->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(sportsTechElliptical);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("EW-ST-")) && !sportsTechRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
sportsTechRower = new sportstechrower(noWriteResistance, noHeartService, bikeResistanceOffset,
|
||||
bikeResistanceGain);
|
||||
emit deviceConnected(b);
|
||||
connect(sportsTechRower, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
connect(sportsTechRower, &sportstechrower::debug, this, &bluetooth::debug);
|
||||
sportsTechRower->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(sportsTechRower);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("CARDIOFIT")) ||
|
||||
(b.name().toUpper().contains(QStringLiteral("CARE")) &&
|
||||
b.name().length() == 11)) // CARE9040177 - Carefitness CV-351
|
||||
@@ -2854,6 +2902,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
|
||||
connect(heartRateBelt, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
|
||||
connect(heartRateBelt, SIGNAL(heartRate(uint8_t)), this->device(), SLOT(heartRate(uint8_t)));
|
||||
connect(heartRateBelt, SIGNAL(rrIntervalReceived(double)), this->device(), SLOT(rrIntervalReceived(double)));
|
||||
QBluetoothDeviceInfo bt;
|
||||
bt.setDeviceUuid(QBluetoothUuid(
|
||||
settings.value(QZSettings::hrm_lastdevice_address, QZSettings::default_hrm_lastdevice_address)
|
||||
@@ -2878,6 +2927,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
|
||||
connect(heartRateBelt, &heartratebelt::debug, this, &bluetooth::debug);
|
||||
connect(heartRateBelt, &heartratebelt::heartRate, this->device(), &bluetoothdevice::heartRate);
|
||||
connect(heartRateBelt, &heartratebelt::rrIntervalReceived, this->device(), &bluetoothdevice::rrIntervalReceived);
|
||||
heartRateBelt->deviceDiscovered(b);
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested(b.name() + " (HR sensor) connected!");
|
||||
@@ -3115,6 +3165,24 @@ void bluetooth::connectedAndDiscovered() {
|
||||
}
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("THINK VS")) || (b.name().toUpper().startsWith("THINKRIDER"))) && !thinkriderController && this->device() &&
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
thinkriderController = new thinkridercontroller(this->device());
|
||||
|
||||
connect(thinkriderController, &thinkridercontroller::debug, this, &bluetooth::debug);
|
||||
connect(thinkriderController, &thinkridercontroller::plus, (bike*)this->device(), &bike::gearUp);
|
||||
connect(thinkriderController, &thinkridercontroller::minus, (bike*)this->device(), &bike::gearDown);
|
||||
thinkriderController->deviceDiscovered(b);
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Thinkrider Controller Connected!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
|
||||
@@ -3615,6 +3683,11 @@ void bluetooth::restart() {
|
||||
delete echelonStairclimber;
|
||||
echelonStairclimber = nullptr;
|
||||
}
|
||||
if (sunnyfitStepper) {
|
||||
|
||||
delete sunnyfitStepper;
|
||||
sunnyfitStepper = nullptr;
|
||||
}
|
||||
if (octaneTreadmill) {
|
||||
|
||||
delete octaneTreadmill;
|
||||
@@ -3749,6 +3822,11 @@ void bluetooth::restart() {
|
||||
delete sportsTechElliptical;
|
||||
sportsTechElliptical = nullptr;
|
||||
}
|
||||
if (sportsTechRower) {
|
||||
|
||||
delete sportsTechRower;
|
||||
sportsTechRower = nullptr;
|
||||
}
|
||||
if (sportsPlusBike) {
|
||||
|
||||
delete sportsPlusBike;
|
||||
@@ -4044,6 +4122,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return echelonStride;
|
||||
} else if (echelonStairclimber) {
|
||||
return echelonStairclimber;
|
||||
} else if (sunnyfitStepper) {
|
||||
return sunnyfitStepper;
|
||||
} else if (octaneTreadmill) {
|
||||
return octaneTreadmill;
|
||||
} else if (ziproTreadmill) {
|
||||
@@ -4098,6 +4178,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return sportsTechBike;
|
||||
} else if (sportsTechElliptical) {
|
||||
return sportsTechElliptical;
|
||||
} else if (sportsTechRower) {
|
||||
return sportsTechRower;
|
||||
} else if (sportsPlusBike) {
|
||||
return sportsPlusBike;
|
||||
} else if (sportsPlusRower) {
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
#include "signalhandler.h"
|
||||
#include "devices/skandikawiribike/skandikawiribike.h"
|
||||
#include "devices/smartrowrower/smartrowrower.h"
|
||||
#include "devices/sunnyfitstepper/sunnyfitstepper.h"
|
||||
#include "devices/smartspin2k/smartspin2k.h"
|
||||
#include "devices/snodebike/snodebike.h"
|
||||
#include "devices/strydrunpowersensor/strydrunpowersensor.h"
|
||||
@@ -126,6 +127,7 @@
|
||||
#include "devices/sportsplusrower/sportsplusrower.h"
|
||||
#include "devices/sportstechbike/sportstechbike.h"
|
||||
#include "devices/sportstechelliptical/sportstechelliptical.h"
|
||||
#include "devices/sportstechrower/sportstechrower.h"
|
||||
#include "devices/sramAXSController/sramAXSController.h"
|
||||
#include "devices/stagesbike/stagesbike.h"
|
||||
|
||||
@@ -154,6 +156,7 @@
|
||||
|
||||
#include "zwift_play/zwiftPlayDevice.h"
|
||||
#include "zwift_play/zwiftclickremote.h"
|
||||
#include "devices/thinkridercontroller/thinkridercontroller.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
@@ -248,6 +251,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
technogymbike* technogymBike = nullptr;
|
||||
sportstechbike *sportsTechBike = nullptr;
|
||||
sportstechelliptical *sportsTechElliptical = nullptr;
|
||||
sportstechrower *sportsTechRower = nullptr;
|
||||
sportsplusbike *sportsPlusBike = nullptr;
|
||||
sportsplusrower *sportsPlusRower = nullptr;
|
||||
inspirebike *inspireBike = nullptr;
|
||||
@@ -269,6 +273,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
echelonrower *echelonRower = nullptr;
|
||||
ftmsrower *ftmsRower = nullptr;
|
||||
smartrowrower *smartrowRower = nullptr;
|
||||
sunnyfitstepper *sunnyfitStepper = nullptr;
|
||||
echelonstride *echelonStride = nullptr;
|
||||
echelonstairclimber *echelonStairclimber = nullptr;
|
||||
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
|
||||
@@ -306,6 +311,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
QList<eliteariafan *> eliteAriaFan;
|
||||
QList<zwiftclickremote* > zwiftPlayDevice;
|
||||
zwiftclickremote* zwiftClickRemote = nullptr;
|
||||
thinkridercontroller* thinkriderController = nullptr;
|
||||
sramaxscontroller* sramAXSController = nullptr;
|
||||
elitesquarecontroller* eliteSquareController = nullptr;
|
||||
QString filterDevice = QLatin1String("");
|
||||
@@ -343,6 +349,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
bool fitmetriaFanfitAvaiable();
|
||||
bool zwiftDeviceAvaiable();
|
||||
bool sramDeviceAvaiable();
|
||||
bool thinkriderDeviceAvaiable();
|
||||
bool fitmetria_fanfit_isconnected(QString name);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
#include <QTime>
|
||||
#include <cmath>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QAndroidJniObject>
|
||||
@@ -192,6 +193,35 @@ void bluetoothdevice::heartRate(uint8_t heart) {
|
||||
void bluetoothdevice::coreBodyTemperature(double coreBodyTemperature) { CoreBodyTemperature.setValue(coreBodyTemperature); }
|
||||
void bluetoothdevice::skinTemperature(double skinTemperature) { SkinTemperature.setValue(skinTemperature); }
|
||||
void bluetoothdevice::heatStrainIndex(double heatStrainIndex) { HeatStrainIndex.setValue(heatStrainIndex); }
|
||||
void bluetoothdevice::rrIntervalReceived(double rrInterval) {
|
||||
// RR-interval is in milliseconds
|
||||
// Add to buffer for RMSSD calculation (keep max 30 samples for real-time HRV display)
|
||||
// Using 30 samples (~20-30 seconds of data) gives more responsive and accurate HRV
|
||||
// than using longer windows which can include heart rate transitions
|
||||
rrIntervals.append(rrInterval);
|
||||
while (rrIntervals.size() > 30) {
|
||||
rrIntervals.removeFirst();
|
||||
}
|
||||
|
||||
// Also add to FIT file buffer (will be cleared when SessionLine is created)
|
||||
rrIntervalsForFit.append(rrInterval);
|
||||
|
||||
// Calculate RMSSD when we have at least 5 RR-intervals
|
||||
if (rrIntervals.size() >= 5) {
|
||||
double sumSquaredDiff = 0.0;
|
||||
int count = 0;
|
||||
for (int i = 1; i < rrIntervals.size(); i++) {
|
||||
double diff = rrIntervals.at(i) - rrIntervals.at(i - 1);
|
||||
sumSquaredDiff += diff * diff;
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
double rmssd = sqrt(sumSquaredDiff / count);
|
||||
HRV.setValue(rmssd);
|
||||
qDebug() << "HRV (RMSSD):" << rmssd << "ms from" << rrIntervals.size() << "RR-intervals";
|
||||
}
|
||||
}
|
||||
}
|
||||
void bluetoothdevice::disconnectBluetooth() {
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
|
||||
@@ -489,10 +489,26 @@ class bluetoothdevice : public QObject {
|
||||
metric SkinTemperature; // Skin temperature in °C or °F
|
||||
metric HeatStrainIndex; // Heat Strain Index (0-25.4, scaled by 10)
|
||||
|
||||
/**
|
||||
* @brief HRV Heart Rate Variability metric (RMSSD). Unit: milliseconds
|
||||
*/
|
||||
metric currentHRV() { return HRV; }
|
||||
|
||||
/**
|
||||
* @brief Get and clear accumulated RR-intervals for FIT file saving
|
||||
* @return List of RR-intervals in milliseconds
|
||||
*/
|
||||
QList<double> getRRIntervalsAndClear() {
|
||||
QList<double> intervals = rrIntervalsForFit;
|
||||
rrIntervalsForFit.clear();
|
||||
return intervals;
|
||||
}
|
||||
|
||||
public Q_SLOTS:
|
||||
virtual void start();
|
||||
virtual void stop(bool pause);
|
||||
virtual void heartRate(uint8_t heart);
|
||||
virtual void rrIntervalReceived(double rrInterval);
|
||||
virtual void cadenceSensor(uint8_t cadence);
|
||||
virtual void powerSensor(uint16_t power);
|
||||
virtual void speedSensor(double speed);
|
||||
@@ -593,6 +609,21 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
metric Heart;
|
||||
|
||||
/**
|
||||
* @brief HRV Heart Rate Variability (RMSSD). Unit: milliseconds
|
||||
*/
|
||||
metric HRV;
|
||||
|
||||
/**
|
||||
* @brief RR-intervals buffer for HRV calculation
|
||||
*/
|
||||
QList<double> rrIntervals;
|
||||
|
||||
/**
|
||||
* @brief RR-intervals buffer for FIT file saving (cleared after each SessionLine)
|
||||
*/
|
||||
QList<double> rrIntervalsForFit;
|
||||
|
||||
int8_t requestStart = -1;
|
||||
int8_t requestStop = -1;
|
||||
int8_t requestPause = -1;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "cscbike.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
@@ -459,6 +460,8 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QSettings settings;
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
@@ -473,11 +476,17 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
if (virtual_device_rower) {
|
||||
emit debug(QStringLiteral("creating virtual rower interface..."));
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
} else {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
|
||||
@@ -160,22 +160,24 @@ uint8_t deerruntreadmill::calculatePitPatChecksum(uint8_t arr[], size_t size) {
|
||||
}
|
||||
|
||||
|
||||
void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
void deerruntreadmill::forceSpeedAndInclination(double requestSpeed, double requestInclination) {
|
||||
QSettings settings;
|
||||
|
||||
if (pitpat) {
|
||||
// PitPat speed template
|
||||
// Pattern: 6a 17 00 00 00 00 [speed_high] [speed_low] 01 00 8a 00 04 00 00 00 00 00 12 2e 0c [checksum] 43
|
||||
// Speed encoding: speed value * 1000 (e.g., 2.0 km/h = 2000 = 0x07d0)
|
||||
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x07, 0x6c, 0x01, 0x00, 0x8a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x2e, 0x0c, 0xc3, 0x43};
|
||||
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x01, 0x08, 0x64, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x7a, 0x67, 0x96, 0x43};
|
||||
|
||||
uint16_t speed = (uint16_t)(requestSpeed * 1000.0);
|
||||
uint16_t incline = (uint16_t)(requestInclination);
|
||||
writeSpeed[6] = (speed >> 8) & 0xFF; // High byte
|
||||
writeSpeed[7] = speed & 0xFF; // Low byte
|
||||
writeSpeed[9] = incline & 0xFF; // Low byte
|
||||
writeSpeed[21] = calculatePitPatChecksum(writeSpeed, sizeof(writeSpeed)); // Checksum at byte 21
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
|
||||
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed), false, true);
|
||||
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed) + QStringLiteral(" incline=") + QString::number(requestInclination), false, true);
|
||||
} else if (superun_ba04) {
|
||||
// Superun BA04 speed template
|
||||
uint8_t writeSpeed[] = {0x4d, 0x00, 0x14, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0xdb, 0x43};
|
||||
@@ -201,8 +203,12 @@ void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::forceIncline(double requestIncline) {
|
||||
void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
forceSpeedAndInclination(requestSpeed, currentInclination().value());
|
||||
}
|
||||
|
||||
void deerruntreadmill::forceIncline(double requestIncline) {
|
||||
forceSpeedAndInclination(currentSpeed().value(), requestIncline);
|
||||
}
|
||||
|
||||
void deerruntreadmill::changeInclinationRequested(double grade, double percentage) {
|
||||
@@ -385,6 +391,9 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
speed = ((double)((value[3] << 8) | ((uint8_t)value[4])) / 1000.0);
|
||||
}
|
||||
double incline = 0.0;
|
||||
if(pitpat) {
|
||||
incline = (double)(value[11] & 0xFF);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
|
||||
@@ -46,6 +46,7 @@ class deerruntreadmill : public treadmill {
|
||||
private:
|
||||
void forceSpeed(double requestSpeed);
|
||||
void forceIncline(double requestIncline);
|
||||
void forceSpeedAndInclination(double requestSpeed, double requestInclination);
|
||||
void btinit(bool startTape);
|
||||
void writeCharacteristic(const QLowEnergyCharacteristic characteristic, uint8_t *data, uint8_t data_len,
|
||||
const QString &info, bool disable_log = false, bool wait_for_response = false);
|
||||
|
||||
@@ -983,3 +983,5 @@ bool domyostreadmill::connected() {
|
||||
}
|
||||
|
||||
void domyostreadmill::searchingStop() { searchStopped = true; }
|
||||
|
||||
double domyostreadmill::minStepSpeed() { return 0.1; }
|
||||
|
||||
@@ -42,6 +42,7 @@ class domyostreadmill : public treadmill {
|
||||
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
|
||||
bool connected() override;
|
||||
bool changeFanSpeed(uint8_t speed) override;
|
||||
double minStepSpeed() override;
|
||||
|
||||
private:
|
||||
// Structure for async write queue
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ftmsbike.h"
|
||||
#include "speedracex_defaults.h"
|
||||
#include "homeform.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
@@ -260,7 +261,7 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
if(SL010 || SPORT01)
|
||||
Resistance = requestResistance;
|
||||
|
||||
if(JFBK5_0 || DIRETO_XR || YPBM || FIT_BK || ZIPRO_RAVE) {
|
||||
if(JFBK5_0 || DIRETO_XR || YPBM || FIT_BK || ZIPRO_RAVE || SPEEDRACEX) {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00, 0x00};
|
||||
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
|
||||
write[2] = ((uint16_t)requestResistance * 10) >> 8;
|
||||
@@ -297,6 +298,42 @@ void ftmsbike::forceInclination(double requestInclination) {
|
||||
QStringLiteral("forceInclination ") + QString::number(requestInclination));
|
||||
}
|
||||
|
||||
void ftmsbike::sendZwiftPlayInclination(double inclination) {
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
QByteArray message = lockscreen::zwift_hub_inclinationCommand(inclination);
|
||||
#else
|
||||
QByteArray message;
|
||||
#endif
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(
|
||||
"org/cagnulen/qdomyoszwift/ZwiftHubBike",
|
||||
"inclinationCommand",
|
||||
"(D)[B",
|
||||
inclination);
|
||||
|
||||
if(!result.isValid()) {
|
||||
qDebug() << "inclinationCommand returned invalid value";
|
||||
return;
|
||||
}
|
||||
|
||||
jbyteArray array = result.object<jbyteArray>();
|
||||
QAndroidJniEnvironment env;
|
||||
jbyte* bytes = env->GetByteArrayElements(array, nullptr);
|
||||
jsize length = env->GetArrayLength(array);
|
||||
|
||||
QByteArray message((char*)bytes, length);
|
||||
|
||||
env->ReleaseByteArrayElements(array, bytes, JNI_ABORT);
|
||||
#else
|
||||
QByteArray message;
|
||||
qDebug() << "implement zwift hub protobuf!";
|
||||
return;
|
||||
#endif
|
||||
writeCharacteristicZwiftPlay((uint8_t*)message.data(), message.length(), "gearInclination", false, false);
|
||||
gearInclinationSent = true;
|
||||
}
|
||||
|
||||
void ftmsbike::update() {
|
||||
|
||||
QSettings settings;
|
||||
@@ -387,18 +424,24 @@ void ftmsbike::update() {
|
||||
}
|
||||
|
||||
if(zwiftPlayService && gears_zwift_ratio && lastGearValue != gears()) {
|
||||
// Workaround: gear commands don't work until an inclination command has been sent first
|
||||
if (!gearInclinationSent) {
|
||||
qDebug() << "Sending initial inclination command (0.4%) before first gear command";
|
||||
sendZwiftPlayInclination(0.4);
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
wheelCircumference::GearTable table;
|
||||
wheelCircumference::GearTable::GearInfo g = table.getGear((int)gears());
|
||||
double original_ratio = ((double)settings.value(QZSettings::gear_crankset_size, QZSettings::default_gear_crankset_size).toDouble()) /
|
||||
((double)settings.value(QZSettings::gear_cog_size, QZSettings::default_gear_cog_size).toDouble());
|
||||
|
||||
|
||||
double current_ratio = ((double)g.crankset / (double)g.rearCog);
|
||||
|
||||
|
||||
uint32_t gear_value = static_cast<uint32_t>(10000.0 * (current_ratio/original_ratio) * (42.0/14.0));
|
||||
|
||||
|
||||
qDebug() << "zwift hub gear current ratio" << current_ratio << g.crankset << g.rearCog << "gear_value" << gear_value << "original_ratio" << original_ratio;
|
||||
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
QByteArray proto = lockscreen::zwift_hub_setGearsCommand(gear_value);
|
||||
@@ -452,6 +495,30 @@ void ftmsbike::update() {
|
||||
forcePower(requestPower);
|
||||
requestPower = -1;
|
||||
}
|
||||
// Continuous ERG for resistance-level bikes:
|
||||
// Re-evaluate resistance when cadence changes to maintain target power.
|
||||
// Without this, resistance is only set once when Zwift sends a new power target,
|
||||
// and cadence changes don't trigger resistance adjustment.
|
||||
if (resistance_lvl_mode && !ergModeSupported &&
|
||||
lastRequestedPower().value() > 0 && autoResistance()) {
|
||||
resistance_t newR = resistanceFromPowerRequest(
|
||||
(uint16_t)lastRequestedPower().value());
|
||||
if (newR != m_lastErgResistance && newR > 0) {
|
||||
// ERG death spiral protection: below 50 RPM, only allow resistance decreases
|
||||
if (Cadence.value() > 0 && Cadence.value() < 50 && newR > m_lastErgResistance) {
|
||||
qDebug() << "ERG death spiral protection: cadence" << Cadence.value()
|
||||
<< "< 50, blocking resistance increase"
|
||||
<< m_lastErgResistance << "->" << newR;
|
||||
} else {
|
||||
qDebug() << "continuous ERG: cadence" << Cadence.value()
|
||||
<< "target" << lastRequestedPower().value()
|
||||
<< "resistance" << m_lastErgResistance << "->" << newR;
|
||||
forceResistance(newR);
|
||||
m_lastErgResistance = newR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestStart != -1) {
|
||||
emit debug(QStringLiteral("starting..."));
|
||||
|
||||
@@ -753,11 +820,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
} 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 (JFICCYCLE) {
|
||||
// JFICCYCLE sends power but always at 0, so calculate from cadence or heart rate
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt (JFICCYCLE calculated): ") + QString::number(m_watt.value()));
|
||||
} else if (LYDSTO && watt_ignore_builtin) {
|
||||
} else if ((LYDSTO || DMASUN) && watt_ignore_builtin) {
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else {
|
||||
@@ -1465,9 +1528,17 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
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:" << ergModeNotSupported
|
||||
<< "resistance_lvl_mode:" << resistance_lvl_mode << "power_sensor:" << power_sensor << "isPowerCommand:" << isPowerCommand;
|
||||
// For bikes that don't support ERG natively but accept resistance levels (e.g. SpeedRaceX),
|
||||
// intercept power commands and route through changePower() which converts power→resistance
|
||||
if (isPowerCommand && !ergModeSupported && resistance_lvl_mode && autoResistance() && newValue.length() > 2) {
|
||||
uint16_t power = (((uint8_t)newValue.at(1)) + (newValue.at(2) << 8));
|
||||
qDebug() << "routing power command through changePower() for resistance_lvl_mode bike, power:" << power;
|
||||
changePower(power);
|
||||
} else {
|
||||
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:" << ergModeNotSupported
|
||||
<< "resistance_lvl_mode:" << resistance_lvl_mode << "power_sensor:" << power_sensor << "isPowerCommand:" << isPowerCommand;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1533,39 +1604,7 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
|
||||
} else if(b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && zwiftPlayService != nullptr && gears_zwift_ratio) {
|
||||
int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8));
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
QByteArray message = lockscreen::zwift_hub_inclinationCommand(((double)slope) / 100.0);
|
||||
#else
|
||||
QByteArray message;
|
||||
#endif
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(
|
||||
"org/cagnulen/qdomyoszwift/ZwiftHubBike",
|
||||
"inclinationCommand",
|
||||
"(D)[B",
|
||||
((double)slope) / 100.0);
|
||||
|
||||
if(!result.isValid()) {
|
||||
qDebug() << "inclinationCommand returned invalid value";
|
||||
return;
|
||||
}
|
||||
|
||||
jbyteArray array = result.object<jbyteArray>();
|
||||
QAndroidJniEnvironment env;
|
||||
jbyte* bytes = env->GetByteArrayElements(array, nullptr);
|
||||
jsize length = env->GetArrayLength(array);
|
||||
|
||||
QByteArray message((char*)bytes, length);
|
||||
|
||||
env->ReleaseByteArrayElements(array, bytes, JNI_ABORT);
|
||||
#else
|
||||
QByteArray message;
|
||||
qDebug() << "implement zwift hub protobuf!";
|
||||
return;
|
||||
#endif
|
||||
writeCharacteristicZwiftPlay((uint8_t*)message.data(), message.length(), "gearInclination", false, false);
|
||||
sendZwiftPlayInclination(((double)slope) / 100.0);
|
||||
return;
|
||||
} else if(b.at(0) == FTMS_SET_TARGET_POWER && !ergModeSupported) {
|
||||
qDebug() << "discarding";
|
||||
@@ -1751,6 +1790,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("LYDSTO"))) {
|
||||
qDebug() << QStringLiteral("LYDSTO found");
|
||||
LYDSTO = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("DMASUN-") && bluetoothDevice.name().toUpper().endsWith("-BIKE"))) {
|
||||
qDebug() << QStringLiteral("DMASUN bike found");
|
||||
DMASUN = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("SL010-"))) {
|
||||
qDebug() << QStringLiteral("SL010 found");
|
||||
SL010 = true;
|
||||
@@ -1829,9 +1871,13 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("S18 found");
|
||||
S18 = true;
|
||||
max_resistance = 24;
|
||||
} else if(device.name().toUpper().startsWith("JFICCYCLE")) {
|
||||
qDebug() << QStringLiteral("JFICCYCLE found");
|
||||
JFICCYCLE = true;
|
||||
} else if(device.name().toUpper().startsWith("SPEEDRACEX")) {
|
||||
qDebug() << QStringLiteral("SpeedRaceX found");
|
||||
SPEEDRACEX = true;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
max_resistance = 32;
|
||||
_ergTable.loadDefaultData(kSpeedRaceXDefaultErgData);
|
||||
}
|
||||
|
||||
|
||||
@@ -1903,6 +1949,7 @@ void ftmsbike::controllerStateChanged(QLowEnergyController::ControllerState stat
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
gearInclinationSent = false;
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ class ftmsbike : public bike {
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void forcePower(int16_t requestPower);
|
||||
void forceInclination(double requestInclination);
|
||||
void sendZwiftPlayInclination(double inclination);
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
|
||||
QTimer *refresh;
|
||||
@@ -130,6 +131,7 @@ class ftmsbike : public bike {
|
||||
bool noHeartService = false;
|
||||
|
||||
bool powerForced = false;
|
||||
resistance_t m_lastErgResistance = 0;
|
||||
|
||||
bool resistance_lvl_mode = false;
|
||||
bool resistance_received = false;
|
||||
@@ -154,6 +156,7 @@ class ftmsbike : public bike {
|
||||
bool BIKE_ = false;
|
||||
bool SMB1 = false;
|
||||
bool LYDSTO = false;
|
||||
bool DMASUN = false;
|
||||
bool SL010 = false;
|
||||
bool REEBOK = false;
|
||||
bool TITAN_7000 = false;
|
||||
@@ -172,8 +175,8 @@ class ftmsbike : public bike {
|
||||
bool SPORT01 = false;
|
||||
bool FS_YK = false;
|
||||
bool S18 = false;
|
||||
bool JFICCYCLE = false;
|
||||
bool ZIPRO_RAVE = false;
|
||||
bool SPEEDRACEX = false;
|
||||
|
||||
uint8_t secondsToResetTimer = 5;
|
||||
|
||||
@@ -182,6 +185,7 @@ class ftmsbike : public bike {
|
||||
uint8_t battery_level = 0;
|
||||
|
||||
bool wattReceived = false;
|
||||
bool gearInclinationSent = false;
|
||||
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
|
||||
46
src/devices/ftmsbike/speedracex_defaults.h
Normal file
46
src/devices/ftmsbike/speedracex_defaults.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef SPEEDRACEX_DEFAULTS_H
|
||||
#define SPEEDRACEX_DEFAULTS_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
// SpeedRaceX default ergTable calibration data (9 cadences x 32 resistance levels = 288 points)
|
||||
// Format: "cadence|wattage|resistance;..." — measured at 80 RPM, extrapolated 60-100 RPM via P ∝ cadence^1.1
|
||||
static const QString kSpeedRaceXDefaultErgData = QStringLiteral(
|
||||
"60|84|1;60|90|2;60|96|3;60|102|4;60|109|5;60|114|6;60|120|7;60|126|8;60|132|9;60|138|10;"
|
||||
"60|144|11;60|150|12;60|156|13;60|162|14;60|169|15;60|175|16;60|181|17;60|187|18;60|192|19;60|200|20;"
|
||||
"60|206|21;60|212|22;60|218|23;60|224|24;60|230|25;60|236|26;60|243|27;60|248|28;60|254|29;60|265|30;"
|
||||
"60|275|31;60|278|32;"
|
||||
"65|92|1;65|99|2;65|105|3;65|111|4;65|119|5;65|125|6;65|131|7;65|138|8;65|144|9;65|151|10;"
|
||||
"65|158|11;65|164|12;65|170|13;65|177|14;65|185|15;65|191|16;65|197|17;65|204|18;65|210|19;65|219|20;"
|
||||
"65|225|21;65|232|22;65|238|23;65|244|24;65|251|25;65|258|26;65|265|27;65|271|28;65|278|29;65|289|30;"
|
||||
"65|301|31;65|304|32;"
|
||||
"70|99|1;70|107|2;70|114|3;70|121|4;70|129|5;70|136|6;70|142|7;70|149|8;70|156|9;70|164|10;"
|
||||
"70|171|11;70|178|12;70|185|13;70|192|14;70|200|15;70|207|16;70|214|17;70|221|18;70|228|19;70|237|20;"
|
||||
"70|244|21;70|251|22;70|258|23;70|265|24;70|273|25;70|280|26;70|288|27;70|294|28;70|301|29;70|313|30;"
|
||||
"70|326|31;70|330|32;"
|
||||
"75|107|1;75|116|2;75|123|3;75|130|4;75|139|5;75|146|6;75|154|7;75|161|8;75|169|9;75|177|10;"
|
||||
"75|184|11;75|192|12;75|199|13;75|207|14;75|216|15;75|224|16;75|231|17;75|238|18;75|246|19;75|256|20;"
|
||||
"75|264|21;75|271|22;75|279|23;75|286|24;75|294|25;75|302|26;75|310|27;75|318|28;75|325|29;75|338|30;"
|
||||
"75|352|31;75|356|32;"
|
||||
"80|115|1;80|124|2;80|132|3;80|140|4;80|149|5;80|157|6;80|165|7;80|173|8;80|181|9;80|190|10;"
|
||||
"80|198|11;80|206|12;80|214|13;80|222|14;80|232|15;80|240|16;80|248|17;80|256|18;80|264|19;80|275|20;"
|
||||
"80|283|21;80|291|22;80|299|23;80|307|24;80|316|25;80|324|26;80|333|27;80|341|28;80|349|29;80|363|30;"
|
||||
"80|378|31;80|382|32;"
|
||||
"85|123|1;85|133|2;85|141|3;85|150|4;85|159|5;85|168|6;85|176|7;85|185|8;85|193|9;85|203|10;"
|
||||
"85|212|11;85|220|12;85|229|13;85|237|14;85|248|15;85|257|16;85|265|17;85|274|18;85|282|19;85|294|20;"
|
||||
"85|303|21;85|311|22;85|320|23;85|328|24;85|338|25;85|346|26;85|356|27;85|365|28;85|373|29;85|388|30;"
|
||||
"85|404|31;85|408|32;"
|
||||
"90|131|1;90|141|2;90|150|3;90|159|4;90|170|5;90|179|6;90|188|7;90|197|8;90|206|9;90|216|10;"
|
||||
"90|225|11;90|234|12;90|244|13;90|253|14;90|264|15;90|273|16;90|282|17;90|291|18;90|301|19;90|313|20;"
|
||||
"90|322|21;90|331|22;90|340|23;90|349|24;90|360|25;90|369|26;90|379|27;90|388|28;90|397|29;90|413|30;"
|
||||
"90|430|31;90|435|32;"
|
||||
"95|139|1;95|150|2;95|159|3;95|169|4;95|180|5;95|190|6;95|199|7;95|209|8;95|219|9;95|230|10;"
|
||||
"95|239|11;95|249|12;95|259|13;95|268|14;95|280|15;95|290|16;95|300|17;95|309|18;95|319|19;95|332|20;"
|
||||
"95|342|21;95|352|22;95|361|23;95|371|24;95|382|25;95|391|26;95|402|27;95|412|28;95|422|29;95|439|30;"
|
||||
"95|457|31;95|461|32;"
|
||||
"100|147|1;100|158|2;100|169|3;100|179|4;100|190|5;100|201|6;100|211|7;100|221|8;100|231|9;100|243|10;"
|
||||
"100|253|11;100|263|12;100|274|13;100|284|14;100|297|15;100|307|16;100|317|17;100|327|18;100|337|19;100|352|20;"
|
||||
"100|362|21;100|372|22;100|382|23;100|392|24;100|404|25;100|414|26;100|426|27;100|436|28;100|446|29;100|464|30;"
|
||||
"100|483|31;100|488|32");
|
||||
|
||||
#endif // SPEEDRACEX_DEFAULTS_H
|
||||
@@ -82,10 +82,47 @@ void heartratebelt::characteristicChanged(const QLowEnergyCharacteristic &charac
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Heart Rate Measurement
|
||||
// Handle Heart Rate Measurement according to Bluetooth Heart Rate Profile
|
||||
if (newValue.length() > 1) {
|
||||
Heart = (uint8_t)newValue[1];
|
||||
uint8_t flags = (uint8_t)newValue[0];
|
||||
int index = 1;
|
||||
|
||||
// Bit 0: Heart Rate Value Format
|
||||
// 0 = UINT8, 1 = UINT16
|
||||
bool hrFormat16bit = (flags & 0x01) != 0;
|
||||
|
||||
if (hrFormat16bit && newValue.length() > 2) {
|
||||
// 16-bit heart rate value
|
||||
Heart = (uint16_t)(((uint8_t)newValue[2] << 8) | (uint8_t)newValue[1]);
|
||||
index = 3;
|
||||
} else {
|
||||
// 8-bit heart rate value
|
||||
Heart = (uint8_t)newValue[1];
|
||||
index = 2;
|
||||
}
|
||||
emit heartRate((uint8_t)Heart.value());
|
||||
|
||||
// Bit 3: Energy Expended Status
|
||||
// If set, 2 bytes of Energy Expended follow the HR value
|
||||
bool energyExpendedPresent = (flags & 0x08) != 0;
|
||||
if (energyExpendedPresent) {
|
||||
index += 2; // Skip 2 bytes of energy expended
|
||||
}
|
||||
|
||||
// Bit 4: RR-Interval
|
||||
// If set, one or more RR-Interval values are present (2 bytes each)
|
||||
// RR-Interval is in units of 1/1024 seconds
|
||||
bool rrIntervalPresent = (flags & 0x10) != 0;
|
||||
if (rrIntervalPresent) {
|
||||
while (index + 1 < newValue.length()) {
|
||||
uint16_t rrRaw = (uint16_t)(((uint8_t)newValue[index + 1] << 8) | (uint8_t)newValue[index]);
|
||||
// Convert from 1/1024 seconds to milliseconds
|
||||
double rrMs = (rrRaw / 1024.0) * 1000.0;
|
||||
emit debug(QStringLiteral("RR-Interval: ") + QString::number(rrMs, 'f', 1) + QStringLiteral(" ms"));
|
||||
emit rrIntervalReceived(rrMs);
|
||||
index += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value()));
|
||||
|
||||
@@ -49,6 +49,7 @@ class heartratebelt : public treadmill {
|
||||
void debug(QString string);
|
||||
void packetReceived();
|
||||
void heartRate(uint8_t heart) override;
|
||||
void rrIntervalReceived(double rrInterval);
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
@@ -326,8 +326,8 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
}
|
||||
|
||||
if (Flags.expEnergy && newValue.length() > index + 1) {
|
||||
KCal = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
/*KCal = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));*/
|
||||
index += 2;
|
||||
|
||||
// energy per hour
|
||||
@@ -335,7 +335,7 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
|
||||
// energy per minute
|
||||
index += 1;
|
||||
} else {
|
||||
} /*else*/ {
|
||||
if (watts())
|
||||
KCal += ((((0.048 * ((double)watts()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
|
||||
@@ -428,6 +428,111 @@ void proformbike::forceResistance(resistance_t requestResistance) {
|
||||
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
|
||||
break;
|
||||
}
|
||||
} else if (nordictrack_gx_4_5_pro) {
|
||||
// Nordic Track GX 4.5 Pro - 25 resistance levels
|
||||
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x8f, 0x01, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x1f, 0x03, 0x00, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xaf, 0x04, 0x00, 0xca, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x3f, 0x06, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xcf, 0x07, 0x00, 0xed, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x5f, 0x09, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xef, 0x0a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x7f, 0x0c, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x0f, 0x0e, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x9f, 0x0f, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x2f, 0x11, 0x00, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xbf, 0x12, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x4f, 0x14, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xdf, 0x15, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x6f, 0x17, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xff, 0x18, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x8f, 0x1a, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x1f, 0x1c, 0x00, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xaf, 0x1d, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x3f, 0x1f, 0x00, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xcf, 0x20, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x5f, 0x22, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xef, 0x23, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x7f, 0x25, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res25[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x0f, 0x27, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
switch (requestResistance) {
|
||||
case 1:
|
||||
writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true);
|
||||
break;
|
||||
case 2:
|
||||
writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true);
|
||||
break;
|
||||
case 3:
|
||||
writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true);
|
||||
break;
|
||||
case 4:
|
||||
writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true);
|
||||
break;
|
||||
case 5:
|
||||
writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true);
|
||||
break;
|
||||
case 6:
|
||||
writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true);
|
||||
break;
|
||||
case 7:
|
||||
writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true);
|
||||
break;
|
||||
case 8:
|
||||
writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true);
|
||||
break;
|
||||
case 9:
|
||||
writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true);
|
||||
break;
|
||||
case 10:
|
||||
writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true);
|
||||
break;
|
||||
case 11:
|
||||
writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true);
|
||||
break;
|
||||
case 12:
|
||||
writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true);
|
||||
break;
|
||||
case 13:
|
||||
writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true);
|
||||
break;
|
||||
case 14:
|
||||
writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true);
|
||||
break;
|
||||
case 15:
|
||||
writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true);
|
||||
break;
|
||||
case 16:
|
||||
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
|
||||
break;
|
||||
case 17:
|
||||
writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true);
|
||||
break;
|
||||
case 18:
|
||||
writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true);
|
||||
break;
|
||||
case 19:
|
||||
writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true);
|
||||
break;
|
||||
case 20:
|
||||
writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true);
|
||||
break;
|
||||
case 21:
|
||||
writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true);
|
||||
break;
|
||||
case 22:
|
||||
writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true);
|
||||
break;
|
||||
case 23:
|
||||
writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true);
|
||||
break;
|
||||
case 24:
|
||||
writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true);
|
||||
break;
|
||||
case 25:
|
||||
writeCharacteristic((uint8_t *)res25, sizeof(res25), QStringLiteral("resistance25"), false, true);
|
||||
break;
|
||||
}
|
||||
} else if (nordictrack_gx_2_7 || proform_bike_225_csx || proform_225_csx_PFEX32925_INT_0) {
|
||||
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0xc2, 0x01, 0x00, 0xda, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
@@ -849,9 +954,9 @@ bool proformbike::innerWriteResistance() {
|
||||
if (requestResistance != currentResistance().value()) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
forceResistance(requestResistance);
|
||||
return true;
|
||||
}
|
||||
requestResistance = -1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -996,9 +1101,20 @@ void proformbike::update() {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
|
||||
// nordictrack gx 4.5 pro
|
||||
uint8_t noOpData1_nordictrack_gx_4_5_pro[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t noOpData2_nordictrack_gx_4_5_pro[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, 0x0d, 0x3c, 0x9c, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80};
|
||||
uint8_t noOpData3_nordictrack_gx_4_5_pro[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData4_nordictrack_gx_4_5_pro[] = {0xfe, 0x02, 0x0d, 0x02};
|
||||
uint8_t noOpData5_nordictrack_gx_4_5_pro[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t noOpData6_nordictrack_gx_4_5_pro[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x00, 0x0f, 0xbc, 0x90, 0x70, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00};
|
||||
uint8_t noOpData7_nordictrack_gx_4_5_pro[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x08, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
switch (counterPoll) {
|
||||
case 0:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData1_nordictrack_gx_4_5_pro, sizeof(noOpData1_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData1_proform_csx210, sizeof(noOpData1_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_225_csx || proform_bike_325_csx || proform_xbike || proform_225_csx_PFEX32925_INT_0) {
|
||||
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
|
||||
@@ -1009,7 +1125,9 @@ void proformbike::update() {
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData2_nordictrack_gx_4_5_pro, sizeof(noOpData2_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData2_proform_csx210, sizeof(noOpData2_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData2_proform_xbike, sizeof(noOpData2_proform_xbike), QStringLiteral("noOp"));
|
||||
@@ -1045,7 +1163,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 2:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData3_nordictrack_gx_4_5_pro, sizeof(noOpData3_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData3_proform_csx210, sizeof(noOpData3_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData3_proform_xbike, sizeof(noOpData3_proform_xbike), QStringLiteral("noOp"));
|
||||
@@ -1081,7 +1201,10 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 3:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData4_nordictrack_gx_4_5_pro, sizeof(noOpData4_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
innerWriteResistance();
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData4_proform_csx210, sizeof(noOpData4_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
innerWriteResistance();
|
||||
@@ -1106,13 +1229,15 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 4:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData5_nordictrack_gx_4_5_pro, sizeof(noOpData5_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData5_proform_csx210, sizeof(noOpData5_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData5_proform_xbike, sizeof(noOpData5_proform_xbike), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
} else if (proform_studio || proform_tdf_10) {
|
||||
writeCharacteristic(noOpData5_proform_studio, sizeof(noOpData5_proform_studio), QStringLiteral("noOp"));
|
||||
else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) {
|
||||
} else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) {
|
||||
writeCharacteristic(noOpData5_nordictrack_gx_2_7, sizeof(noOpData5_nordictrack_gx_2_7),
|
||||
QStringLiteral("noOp"));
|
||||
} else if (proform_hybrid_trainer_PFEL03815) {
|
||||
@@ -1136,7 +1261,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 5:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData6_nordictrack_gx_4_5_pro, sizeof(noOpData6_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData6_proform_csx210, sizeof(noOpData6_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
writeCharacteristic(noOpData6_proform_studio, sizeof(noOpData6_proform_studio), QStringLiteral("noOp"));
|
||||
@@ -1171,13 +1298,18 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 6:
|
||||
if (proform_studio || proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
}
|
||||
writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp"));
|
||||
if (!proform_studio && !proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData7_nordictrack_gx_4_5_pro, sizeof(noOpData7_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else {
|
||||
if (proform_studio || proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
}
|
||||
writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp"));
|
||||
if (!proform_studio && !proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
}
|
||||
}
|
||||
|
||||
if (requestInclination != -100 && (proform_studio || proform_tdf_10)) {
|
||||
// only 0.5 steps ara available
|
||||
double inc = qRound(requestInclination * 2.0) / 2.0;
|
||||
@@ -1194,7 +1326,7 @@ void proformbike::update() {
|
||||
counterPoll++;
|
||||
if (counterPoll > 6) {
|
||||
counterPoll = 0;
|
||||
} else if(counterPoll == 6 && (proform_bike_225_csx || proform_225_csx_PFEX32925_INT_0 || proform_bike_PFEVEX71316_0)) {
|
||||
} else if(counterPoll == 6 && (nordictrack_gx_4_5_pro || proform_bike_225_csx || proform_225_csx_PFEX32925_INT_0 || proform_bike_PFEVEX71316_0)) {
|
||||
counterPoll = 0;
|
||||
} else if (counterPoll == 6 &&
|
||||
(proform_tour_de_france_clc || proform_cycle_trainer_400 || proform_bike_PFEVEX71316_1) &&
|
||||
@@ -1817,6 +1949,119 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
Resistance = 1;
|
||||
m_pelotonResistance = 10;
|
||||
}
|
||||
} else if(nordictrack_gx_4_5_pro) {
|
||||
switch ((uint8_t)newValue.at(11)) {
|
||||
case 0x00:
|
||||
case 0x01:
|
||||
Resistance = 1;
|
||||
m_pelotonResistance = 10;
|
||||
break;
|
||||
case 0x02:
|
||||
case 0x03:
|
||||
Resistance = 2;
|
||||
m_pelotonResistance = 20;
|
||||
break;
|
||||
case 0x04:
|
||||
case 0x05:
|
||||
Resistance = 3;
|
||||
m_pelotonResistance = 25;
|
||||
break;
|
||||
case 0x06:
|
||||
Resistance = 4;
|
||||
m_pelotonResistance = 30;
|
||||
break;
|
||||
case 0x07:
|
||||
case 0x08:
|
||||
Resistance = 5;
|
||||
m_pelotonResistance = 33;
|
||||
break;
|
||||
case 0x09:
|
||||
Resistance = 6;
|
||||
m_pelotonResistance = 35;
|
||||
break;
|
||||
case 0x0A:
|
||||
case 0x0b:
|
||||
Resistance = 7;
|
||||
m_pelotonResistance = 38;
|
||||
break;
|
||||
case 0x0c:
|
||||
case 0x0d:
|
||||
Resistance = 8;
|
||||
m_pelotonResistance = 40;
|
||||
break;
|
||||
case 0x0e:
|
||||
Resistance = 9;
|
||||
m_pelotonResistance = 45;
|
||||
break;
|
||||
case 0x0f:
|
||||
case 0x10:
|
||||
Resistance = 10;
|
||||
m_pelotonResistance = 50;
|
||||
break;
|
||||
case 0x11:
|
||||
Resistance = 11;
|
||||
m_pelotonResistance = 55;
|
||||
break;
|
||||
case 0x12:
|
||||
case 0x13:
|
||||
Resistance = 12;
|
||||
m_pelotonResistance = 60;
|
||||
break;
|
||||
case 0x14:
|
||||
Resistance = 13;
|
||||
m_pelotonResistance = 63;
|
||||
break;
|
||||
case 0x15:
|
||||
case 0x16:
|
||||
Resistance = 14;
|
||||
m_pelotonResistance = 65;
|
||||
break;
|
||||
case 0x17:
|
||||
Resistance = 15;
|
||||
m_pelotonResistance = 68;
|
||||
case 0x18:
|
||||
case 0x19:
|
||||
Resistance = 16;
|
||||
m_pelotonResistance = 70;
|
||||
break;
|
||||
case 0x1a:
|
||||
case 0x1b:
|
||||
Resistance = 17;
|
||||
m_pelotonResistance = 75;
|
||||
break;
|
||||
case 0x1c:
|
||||
Resistance = 18;
|
||||
m_pelotonResistance = 80;
|
||||
break;
|
||||
case 0x1d:
|
||||
Resistance = 19;
|
||||
m_pelotonResistance = 85;
|
||||
break;
|
||||
case 0x1f:
|
||||
Resistance = 20;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x20:
|
||||
Resistance = 21;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x22:
|
||||
Resistance = 22;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x23:
|
||||
Resistance = 23;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x25:
|
||||
Resistance = 24;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x27:
|
||||
Resistance = 25;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
}
|
||||
} else if (!nordictrack_gx_2_7) {
|
||||
switch ((uint8_t)newValue.at(11)) {
|
||||
case 0x00:
|
||||
@@ -2080,9 +2325,10 @@ void proformbike::btinit() {
|
||||
proform_xbike = settings.value(QZSettings::proform_xbike, QZSettings::default_proform_xbike).toBool();
|
||||
proform_225_csx_PFEX32925_INT_0 = settings.value(QZSettings::proform_225_csx_PFEX32925_INT_0, QZSettings::default_proform_225_csx_PFEX32925_INT_0).toBool();
|
||||
proform_csx210 = settings.value(QZSettings::proform_csx210, QZSettings::default_proform_csx210).toBool();
|
||||
nordictrack_gx_4_5_pro = settings.value(QZSettings::nordictrack_gx_4_5_pro, QZSettings::default_nordictrack_gx_4_5_pro).toBool();
|
||||
|
||||
|
||||
if(nordictrack_GX4_5_bike)
|
||||
if(nordictrack_GX4_5_bike || nordictrack_gx_4_5_pro)
|
||||
max_resistance = 25;
|
||||
if(proform_csx210)
|
||||
max_resistance = 16;
|
||||
@@ -3028,6 +3274,30 @@ void proformbike::btinit() {
|
||||
|
||||
writeCharacteristic(noOpData22, sizeof(noOpData22), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
} else if (nordictrack_gx_4_5_pro) {
|
||||
max_resistance = 25;
|
||||
|
||||
uint8_t initData14[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07, 0x01, 0x99, 0x78, 0x65, 0x40, 0x29, 0x10, 0x0d, 0xf8, 0xe9};
|
||||
uint8_t initData15[] = {0x01, 0x12, 0xd8, 0xc5, 0xb0, 0xb9, 0xa0, 0xbd, 0xb8, 0xb9, 0xb8, 0xa5, 0xa0, 0xa9, 0xd0, 0xcd, 0xf8, 0xe9, 0x18, 0x05};
|
||||
uint8_t initData16[] = {0xff, 0x08, 0x30, 0x59, 0x40, 0x98, 0x02, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData17[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t initData18[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData19[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// Execution
|
||||
writeCharacteristic(initData14, sizeof(initData14), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData15, sizeof(initData15), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData16, sizeof(initData16), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData18, sizeof(initData18), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData19, sizeof(initData19), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
|
||||
} else if (proform_bike_225_csx) {
|
||||
max_resistance = 20;
|
||||
uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07, 0x01, 0xd2, 0x74, 0x14, 0xb2, 0x5e, 0x08, 0xa0, 0x5e, 0x0a};
|
||||
|
||||
@@ -85,6 +85,7 @@ class proformbike : public bike {
|
||||
bool proform_hybrid_trainer_PFEL03815 = false;
|
||||
bool proform_bike_sb = false;
|
||||
bool proform_cycle_trainer_300_ci =false;
|
||||
bool nordictrack_gx_4_5_pro = false;
|
||||
bool proform_bike_225_csx = false;
|
||||
bool proform_bike_325_csx = false;
|
||||
bool proform_tour_de_france_clc = false;
|
||||
|
||||
@@ -488,7 +488,7 @@ void proformwifibike::characteristicChanged(const QString &newValue) {
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = m_rawWatt.value();
|
||||
m_watt.setValue(m_rawWatt.value(), false);
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
|
||||
} else if (!values[QStringLiteral("Watt attuali")].isUndefined()) {
|
||||
double watt = values[QStringLiteral("Watt attuali")].toString().toDouble();
|
||||
|
||||
457
src/devices/sportstechrower/sportstechrower.cpp
Normal file
457
src/devices/sportstechrower/sportstechrower.cpp
Normal file
@@ -0,0 +1,457 @@
|
||||
#include "sportstechrower.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
sportstechrower::sportstechrower(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
this->bikeResistanceGain = bikeResistanceGain;
|
||||
this->bikeResistanceOffset = bikeResistanceOffset;
|
||||
initDone = false;
|
||||
connect(refresh, &QTimer::timeout, this, &sportstechrower::update);
|
||||
refresh->start(200ms);
|
||||
}
|
||||
|
||||
void sportstechrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
if (wait_for_response) {
|
||||
connect(this, &sportstechrower::packetReceived, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + " // " + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
void sportstechrower::forceResistance(resistance_t requestResistance) {
|
||||
Q_UNUSED(requestResistance);
|
||||
// Resistance control disabled for this rower
|
||||
}
|
||||
|
||||
void sportstechrower::update() {
|
||||
// qDebug() << bike.isValid() << m_control->state() << gattCommunicationChannelService <<
|
||||
// gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
|
||||
|
||||
if (!m_control) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
btinit(false);
|
||||
} else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
|
||||
gattCommunicationChannelService && gattWriteCharacteristic.isValid() &&
|
||||
gattNotify1Characteristic.isValid() && initDone) {
|
||||
update_metrics(false, 0);
|
||||
|
||||
// updating the bike console every second
|
||||
if (sec1update++ == (1000 / refresh->interval())) {
|
||||
sec1update = 0;
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
uint8_t noOpData[] = {0xf2, 0xc3, 0x07, 0x04, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbe};
|
||||
// Always send resistance = 0 (no resistance control for rower)
|
||||
writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true);
|
||||
}
|
||||
}
|
||||
|
||||
void sportstechrower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void sportstechrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
// qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
|
||||
Q_UNUSED(characteristic);
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
emit packetReceived();
|
||||
|
||||
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
|
||||
|
||||
lastPacket = newValue;
|
||||
if (newValue.length() != 20) {
|
||||
return;
|
||||
}
|
||||
|
||||
double speed = GetSpeedFromPacket(newValue);
|
||||
double strokeRate = GetStrokeRateFromPacket(newValue);
|
||||
double resistance = GetResistanceFromPacket(newValue);
|
||||
double kcal = GetKcalFromPacket(newValue);
|
||||
double watt = GetWattFromPacket(newValue);
|
||||
bool disable_hr_frommachinery =
|
||||
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
Heart = (uint8_t)KeepAwakeHelper::heart();
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
|
||||
|
||||
uint8_t heart = ((uint8_t)newValue.at(11));
|
||||
if (heart == 0 || disable_hr_frommachinery) {
|
||||
update_hr_from_external();
|
||||
} else {
|
||||
Heart = heart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstCharChanged) {
|
||||
Distance += ((speed / 3600.0) / (1000.0 / (lastTimeCharChanged.msecsTo(QTime::currentTime()))));
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("Current speed: ") + QString::number(speed));
|
||||
emit debug(QStringLiteral("Current stroke rate: ") + QString::number(strokeRate));
|
||||
emit debug(QStringLiteral("Current resistance: ") + QString::number(resistance));
|
||||
emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value()));
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(kcal));
|
||||
emit debug(QStringLiteral("Current watt: ") + QString::number(watt));
|
||||
emit debug(QStringLiteral("Current Elapsed from the bike (not used): ") +
|
||||
QString::number(GetElapsedFromPacket(newValue)));
|
||||
emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(Distance.value()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
}
|
||||
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = speed;
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), 0);
|
||||
}
|
||||
Resistance = resistance;
|
||||
emit resistanceRead(Resistance.value());
|
||||
KCal = kcal;
|
||||
|
||||
// For rowers, cadence = stroke rate (strokes per minute)
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = strokeRate;
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = watt;
|
||||
|
||||
lastTimeCharChanged = QTime::currentTime();
|
||||
firstCharChanged = false;
|
||||
}
|
||||
|
||||
uint16_t sportstechrower::GetElapsedFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedDataSec = (packet.at(4));
|
||||
uint16_t convertedDataMin = (packet.at(3));
|
||||
uint16_t convertedData = convertedDataMin * 60.f + convertedDataSec;
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
double sportstechrower::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(12) << 8) | ((uint8_t)packet.at(13));
|
||||
double data = (double)(convertedData) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
|
||||
double sportstechrower::GetKcalFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(7) << 8) | ((uint8_t)packet.at(8));
|
||||
return (double)(convertedData);
|
||||
}
|
||||
|
||||
double sportstechrower::GetWattFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(9) << 8) | ((uint8_t)packet.at(10));
|
||||
double data = ((double)(convertedData));
|
||||
return data;
|
||||
}
|
||||
|
||||
double sportstechrower::GetStrokeRateFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = packet.at(17);
|
||||
double data = (convertedData);
|
||||
if (data < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
double sportstechrower::GetResistanceFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = packet.at(15);
|
||||
double data = (convertedData);
|
||||
if (data < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void sportstechrower::btinit(bool startTape) {
|
||||
Q_UNUSED(startTape);
|
||||
QSettings settings;
|
||||
|
||||
const uint8_t initData1[] = {0xf2, 0xc0, 0x00, 0xb2};
|
||||
const uint8_t initData2[] = {0xf2, 0xc1, 0x05, 0x01, 0xff, 0xff, 0xff, 0xff, 0xb5};
|
||||
const uint8_t initData3[] = {0xf2, 0xc4, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0};
|
||||
const uint8_t initData4[] = {0xf2, 0xc3, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbb};
|
||||
|
||||
writeCharacteristic((uint8_t *)initData1, sizeof(initData1), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData2, sizeof(initData2), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData4, sizeof(initData4), QStringLiteral("init"), false, true);
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void sportstechrower::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
auto characteristics_list = gattCommunicationChannelService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
emit debug(QStringLiteral("characteristic ") + c.uuid().toString());
|
||||
}
|
||||
|
||||
// QString uuidWrite = "0000fff2-0000-1000-8000-00805f9b34fb";
|
||||
// QString uuidNotify1 = "0000fff1-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0000fff2-0000-1000-8000-00805f9b34fb"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0000fff1-0000-1000-8000-00805f9b34fb"));
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&sportstechrower::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&sportstechrower::characteristicWritten);
|
||||
connect(gattCommunicationChannelService,
|
||||
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &sportstechrower::errorService);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&sportstechrower::descriptorWritten);
|
||||
|
||||
// ******************************************* virtual device init *************************************
|
||||
if (!firstVirtualBike && !this->hasVirtualDevice()) {
|
||||
QSettings settings;
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_rower) {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &sportstechrower::changeInclination);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
emit debug(QStringLiteral("creating virtual rower interface..."));
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstVirtualBike = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
void sportstechrower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' '));
|
||||
|
||||
initRequest = true;
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void sportstechrower::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void sportstechrower::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
// QString uuid = "0000fff0-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
if (gattCommunicationChannelService == nullptr) {
|
||||
qDebug() << QStringLiteral("invalid service") << _gattCommunicationChannelServiceId.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &sportstechrower::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void sportstechrower::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("sportstechrower::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sportstechrower::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("sportstechrower::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sportstechrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &sportstechrower::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &sportstechrower::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &sportstechrower::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &sportstechrower::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("LowEnergy controller disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t sportstechrower::watts() {
|
||||
if (currentCadence().value() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
bool sportstechrower::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void sportstechrower::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t sportstechrower::wattsFromResistance(double resistance) {
|
||||
// Coefficients from the polynomial regression
|
||||
double intercept = 14.4968;
|
||||
double b1 = -4.1878;
|
||||
double b2 = -0.5051;
|
||||
double b3 = 0.00387;
|
||||
double b4 = 0.2392;
|
||||
double b5 = 0.01108;
|
||||
double cadence = Cadence.value();
|
||||
|
||||
// Calculate power using the polynomial equation
|
||||
double power = intercept +
|
||||
(b1 * resistance) +
|
||||
(b2 * cadence) +
|
||||
(b3 * resistance * resistance) +
|
||||
(b4 * resistance * cadence) +
|
||||
(b5 * cadence * cadence);
|
||||
|
||||
return power;
|
||||
}
|
||||
|
||||
resistance_t sportstechrower::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
}
|
||||
100
src/devices/sportstechrower/sportstechrower.h
Normal file
100
src/devices/sportstechrower/sportstechrower.h
Normal file
@@ -0,0 +1,100 @@
|
||||
#ifndef SPORTSTECHROWER_H
|
||||
#define SPORTSTECHROWER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
|
||||
#include "devices/rower.h"
|
||||
|
||||
class sportstechrower : public rower {
|
||||
Q_OBJECT
|
||||
public:
|
||||
sportstechrower(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain);
|
||||
bool connected() override;
|
||||
resistance_t maxResistance() override { return 24; }
|
||||
resistance_t resistanceFromPowerRequest(uint16_t power) override;
|
||||
|
||||
private:
|
||||
double GetSpeedFromPacket(const QByteArray &packet);
|
||||
double GetResistanceFromPacket(const QByteArray &packet);
|
||||
double GetKcalFromPacket(const QByteArray &packet);
|
||||
double GetDistanceFromPacket(QByteArray packet);
|
||||
uint16_t GetElapsedFromPacket(const QByteArray &packet);
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void updateDisplay(uint16_t elapsed);
|
||||
void btinit(bool startTape);
|
||||
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response);
|
||||
void startDiscover();
|
||||
uint16_t watts() override;
|
||||
double GetWattFromPacket(const QByteArray &packet);
|
||||
double GetStrokeRateFromPacket(const QByteArray &packet);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
double bikeResistanceGain = 1.0;
|
||||
|
||||
uint8_t firstVirtualBike = 0;
|
||||
bool firstCharChanged = true;
|
||||
QTime lastTimeCharChanged;
|
||||
uint8_t sec1update = 0;
|
||||
QByteArray lastPacket;
|
||||
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotify1Characteristic;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
bool readyToStart = false;
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void packetReceived();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // SPORTSTECHROWER_H
|
||||
389
src/devices/sunnyfitstepper/sunnyfitstepper.cpp
Normal file
389
src/devices/sunnyfitstepper/sunnyfitstepper.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
#include "sunnyfitstepper.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualtreadmill.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
sunnyfitstepper::sunnyfitstepper(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
this->pollDeviceTime = pollDeviceTime;
|
||||
|
||||
refresh = new QTimer(this);
|
||||
initDone = false;
|
||||
frameBuffer.clear();
|
||||
expectingSecondPart = false;
|
||||
|
||||
connect(refresh, &QTimer::timeout, this, &sunnyfitstepper::update);
|
||||
refresh->start(pollDeviceTime);
|
||||
}
|
||||
|
||||
bool sunnyfitstepper::connected() {
|
||||
if (!m_control)
|
||||
return false;
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
if (wait_for_response) {
|
||||
connect(this, &sunnyfitstepper::packetReceived, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit debug(QStringLiteral("writeCharacteristic error because the connection is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::sendPoll() {
|
||||
// Alternate between two poll commands
|
||||
|
||||
counterPoll++;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::changeInclinationRequested(double grade, double percentage) {
|
||||
if (percentage < 0)
|
||||
percentage = 0;
|
||||
changeInclination(grade, percentage);
|
||||
}
|
||||
|
||||
void sunnyfitstepper::processDataFrame(const QByteArray &completeFrame) {
|
||||
if (completeFrame.length() != 32) {
|
||||
qDebug() << "ERROR: Frame length is not 32 bytes:" << completeFrame.length();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint8_t)completeFrame.at(0) != 0x5a) {
|
||||
qDebug() << "ERROR: Frame doesn't start with 0x5a";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint8_t)completeFrame.at(1) != 0x05) {
|
||||
qDebug() << "WARNING: Expected 0x05 at byte 1, got:" << QString::number((uint8_t)completeFrame.at(1), 16);
|
||||
}
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QSettings settings;
|
||||
|
||||
// Extract cadence (bytes 6-7, little-endian)
|
||||
uint16_t rawCadence = ((uint8_t)completeFrame.at(7) << 8) | (uint8_t)completeFrame.at(6);
|
||||
Cadence = (double)rawCadence;
|
||||
|
||||
// Extract step count (bytes 10-12, little-endian)
|
||||
uint32_t steps = ((uint32_t)(uint8_t)completeFrame.at(12) << 16) |
|
||||
((uint32_t)(uint8_t)completeFrame.at(11) << 8) |
|
||||
(uint32_t)(uint8_t)completeFrame.at(10);
|
||||
StepCount = steps;
|
||||
|
||||
// Calculate elevation manually (0.2 meters per step)
|
||||
elevationAcc = (double)steps * 0.20;
|
||||
|
||||
// Calculate speed from cadence (stairclimber convention)
|
||||
Speed = Cadence.value() / 3.2;
|
||||
|
||||
qDebug() << QStringLiteral("Current Cadence (SPM): ") + QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current StepCount: ") + QString::number(StepCount.value());
|
||||
qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value());
|
||||
qDebug() << QStringLiteral("Current Elevation: ") + QString::number(elevationAcc.value());
|
||||
|
||||
// Calculate metrics
|
||||
if (!firstCharacteristicChanged) {
|
||||
if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) {
|
||||
KCal += ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) +
|
||||
1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
Distance += ((Speed.value() / 3600.0) / (1000.0 / (lastTimeCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Current Distance: ") + QString::number(Distance.value());
|
||||
qDebug() << QStringLiteral("Current KCal: ") + QString::number(KCal.value());
|
||||
qDebug() << QStringLiteral("Current Watt: ") +
|
||||
QString::number(watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError)
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
|
||||
lastTimeCharacteristicChanged = now;
|
||||
firstCharacteristicChanged = false;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::update() {
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
btinit();
|
||||
} else if (m_control->state() == QLowEnergyController::DiscoveredState && gattCommunicationChannelService &&
|
||||
gattWriteCharacteristic.isValid() && gattNotify1Characteristic.isValid() &&
|
||||
gattNotify4Characteristic.isValid() && initDone) {
|
||||
QSettings settings;
|
||||
|
||||
// *********** virtual treadmill init *************************************
|
||||
if (!this->hasVirtualDevice()) {
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_force_bike =
|
||||
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike)
|
||||
.toBool();
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_force_bike) {
|
||||
debug("creating virtual treadmill interface...");
|
||||
auto virtualTreadMill = new virtualtreadmill(this, noHeartService);
|
||||
connect(virtualTreadMill, &virtualtreadmill::debug, this, &sunnyfitstepper::debug);
|
||||
connect(virtualTreadMill, &virtualtreadmill::changeInclination, this,
|
||||
&sunnyfitstepper::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
debug("creating virtual bike interface...");
|
||||
auto virtualBike = new virtualbike(this);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this,
|
||||
&sunnyfitstepper::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ************************************************************
|
||||
|
||||
update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
// Send poll every 2 seconds
|
||||
if (sec1Update++ >= (2000 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
//sendPoll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
|
||||
|
||||
// Handle command responses (Notify 1)
|
||||
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("fd710003-e950-458e-8a4d-a1cbc5aa4cce"))) {
|
||||
qDebug() << "Command response:" << newValue.toHex(' ');
|
||||
emit packetReceived();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle main data stream (Notify 4) - SPLIT FRAME LOGIC
|
||||
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("fd710006-e950-458e-8a4d-a1cbc5aa4cce"))) {
|
||||
// First part: 20 bytes starting with 0x5a
|
||||
if (newValue.length() == 20 && (uint8_t)newValue.at(0) == 0x5a) {
|
||||
frameBuffer.clear();
|
||||
frameBuffer.append(newValue);
|
||||
expectingSecondPart = true;
|
||||
qDebug() << "First part of frame received (20 bytes)";
|
||||
return;
|
||||
}
|
||||
|
||||
// Second part: 12 bytes
|
||||
if (newValue.length() == 12 && expectingSecondPart) {
|
||||
frameBuffer.append(newValue);
|
||||
expectingSecondPart = false;
|
||||
|
||||
if (frameBuffer.length() == 32) {
|
||||
emit debug(QStringLiteral(" << COMPLETE FRAME >> ") + frameBuffer.toHex(' '));
|
||||
processDataFrame(frameBuffer);
|
||||
frameBuffer.clear();
|
||||
} else {
|
||||
qDebug() << "ERROR: Complete frame size mismatch:" << frameBuffer.length();
|
||||
frameBuffer.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Unexpected frame structure
|
||||
qDebug() << "Unexpected frame - length:" << newValue.length() << "expecting second part:" << expectingSecondPart;
|
||||
frameBuffer.clear();
|
||||
expectingSecondPart = false;
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::btinit() {
|
||||
uint8_t init1[] = {0x5a, 0x02, 0x00, 0x08, 0x07, 0xa0, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0xe6, 0xa5};
|
||||
uint8_t init2[] = {0x5a, 0x02, 0x00, 0x03, 0x02, 0xa3, 0x00, 0xaa, 0xa5};
|
||||
uint8_t init3[] = {0x5a, 0x02, 0x00, 0x03, 0x02, 0xb4, 0x00, 0xbb, 0xa5};
|
||||
uint8_t init4[] = {0x5a, 0x04, 0x00, 0x03, 0x02, 0xf1, 0x00, 0xfa, 0xa5};
|
||||
|
||||
writeCharacteristic(init1, sizeof(init1), QStringLiteral("init1"), false, true);
|
||||
writeCharacteristic(init2, sizeof(init2), QStringLiteral("init2"), false, true);
|
||||
writeCharacteristic(init3, sizeof(init3), QStringLiteral("init3"), false, false);
|
||||
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, false);
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("fd710002-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("fd710003-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
QBluetoothUuid _gattNotify4CharacteristicId(QStringLiteral("fd710006-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
gattNotify4Characteristic = gattCommunicationChannelService->characteristic(_gattNotify4CharacteristicId);
|
||||
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
Q_ASSERT(gattNotify4Characteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&sunnyfitstepper::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&sunnyfitstepper::characteristicWritten);
|
||||
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)), this,
|
||||
SLOT(errorService(QLowEnergyService::ServiceError)));
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&sunnyfitstepper::descriptorWritten);
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify4Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
|
||||
initRequest = true;
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void sunnyfitstepper::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void sunnyfitstepper::serviceScanDone(void) {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
qDebug() << s << "service found!";
|
||||
}
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("fd710001-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
if (gattCommunicationChannelService == nullptr) {
|
||||
qDebug() << "invalid service";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &sunnyfitstepper::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void sunnyfitstepper::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("sunnyfitstepper::errorService ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("sunnyfitstepper::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("sunnyfitstepper::controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &sunnyfitstepper::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &sunnyfitstepper::serviceScanDone);
|
||||
connect(m_control, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(error(QLowEnergyController::Error)));
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &sunnyfitstepper::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("QLowEnergyController disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::startDiscover() {
|
||||
m_control->discoverServices();
|
||||
}
|
||||
99
src/devices/sunnyfitstepper/sunnyfitstepper.h
Normal file
99
src/devices/sunnyfitstepper/sunnyfitstepper.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef SUNNYFITSTEPPER_H
|
||||
#define SUNNYFITSTEPPER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "stairclimber.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class sunnyfitstepper : public stairclimber {
|
||||
Q_OBJECT
|
||||
public:
|
||||
sunnyfitstepper(uint32_t pollDeviceTime = 200, bool noConsole = false, bool noHeartService = false,
|
||||
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
void btinit();
|
||||
void sendPoll();
|
||||
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
|
||||
bool wait_for_response = false);
|
||||
void processDataFrame(const QByteArray &completeFrame);
|
||||
void startDiscover();
|
||||
|
||||
// Bluetooth
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotify1Characteristic;
|
||||
QLowEnergyCharacteristic gattNotify4Characteristic;
|
||||
|
||||
// Split-frame handling (CRITICAL)
|
||||
QByteArray frameBuffer;
|
||||
bool expectingSecondPart = false;
|
||||
|
||||
// State
|
||||
QTimer *refresh;
|
||||
uint8_t sec1Update = 0;
|
||||
uint8_t counterPoll = 0;
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
bool noConsole = false;
|
||||
bool noHeartService = false;
|
||||
uint32_t pollDeviceTime = 200;
|
||||
QDateTime lastTimeCharacteristicChanged;
|
||||
bool firstCharacteristicChanged = true;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void speedChanged(double speed);
|
||||
void packetReceived();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
void changeInclinationRequested(double grade, double percentage);
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // SUNNYFITSTEPPER_H
|
||||
225
src/devices/thinkridercontroller/thinkridercontroller.cpp
Normal file
225
src/devices/thinkridercontroller/thinkridercontroller.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "thinkridercontroller.h"
|
||||
#include "homeform.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Thinkrider VS200 UUIDs
|
||||
const QBluetoothUuid thinkridercontroller::SERVICE_UUID =
|
||||
QBluetoothUuid(QStringLiteral("0000fea0-0000-1000-8000-00805f9b34fb"));
|
||||
const QBluetoothUuid thinkridercontroller::CHARACTERISTIC_UUID =
|
||||
QBluetoothUuid(QStringLiteral("0000fea1-0000-1000-8000-00805f9b34fb"));
|
||||
|
||||
// Button patterns (from swiftcontrol implementation)
|
||||
const QByteArray thinkridercontroller::SHIFT_UP_PATTERN = QByteArray::fromHex("f3050301fc");
|
||||
const QByteArray thinkridercontroller::SHIFT_DOWN_PATTERN = QByteArray::fromHex("f3050300fb");
|
||||
|
||||
thinkridercontroller::thinkridercontroller(bluetoothdevice *parentDevice) {
|
||||
this->parentDevice = parentDevice;
|
||||
}
|
||||
|
||||
void thinkridercontroller::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::disconnectBluetooth() {
|
||||
qDebug() << QStringLiteral("thinkridercontroller::disconnect") << m_control;
|
||||
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
|
||||
const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
|
||||
qDebug() << QStringLiteral("thinkridercontroller << ") << newValue.toHex(' ');
|
||||
|
||||
// Check for shift up pattern
|
||||
if (newValue == SHIFT_UP_PATTERN) {
|
||||
qDebug() << QStringLiteral("Thinkrider: Shift UP detected");
|
||||
emit plus();
|
||||
}
|
||||
// Check for shift down pattern
|
||||
else if (newValue == SHIFT_DOWN_PATTERN) {
|
||||
qDebug() << QStringLiteral("Thinkrider: Shift DOWN detected");
|
||||
emit minus();
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state();
|
||||
if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) {
|
||||
qDebug() << QStringLiteral("not all services discovered");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (state != QLowEnergyService::ServiceState::ServiceDiscovered) {
|
||||
qDebug() << QStringLiteral("ignoring this state");
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("all services discovered!");
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
if (s->state() == QLowEnergyService::ServiceDiscovered) {
|
||||
// establish hook into notifications
|
||||
connect(s, &QLowEnergyService::characteristicChanged, this, &thinkridercontroller::characteristicChanged);
|
||||
connect(s, &QLowEnergyService::characteristicRead, this, &thinkridercontroller::characteristicChanged);
|
||||
connect(
|
||||
s, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &thinkridercontroller::errorService);
|
||||
connect(s, &QLowEnergyService::descriptorWritten, this, &thinkridercontroller::descriptorWritten);
|
||||
|
||||
qDebug() << s->serviceUuid() << QStringLiteral("connected!");
|
||||
|
||||
auto characteristics_list = s->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle();
|
||||
auto descriptors_list = c.descriptors();
|
||||
for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) {
|
||||
qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle();
|
||||
}
|
||||
|
||||
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
|
||||
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle()
|
||||
<< QStringLiteral(" is not valid");
|
||||
}
|
||||
|
||||
qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("notification subscribed!");
|
||||
} else if ((c.properties() & QLowEnergyCharacteristic::Indicate) ==
|
||||
QLowEnergyCharacteristic::Indicate) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x02);
|
||||
descriptor.append((char)0x00);
|
||||
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
|
||||
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle()
|
||||
<< QStringLiteral(" is not valid");
|
||||
}
|
||||
|
||||
qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("indication subscribed!");
|
||||
}
|
||||
|
||||
if (c.uuid() == CHARACTERISTIC_UUID) {
|
||||
qDebug() << QStringLiteral("Thinkrider characteristic found");
|
||||
gattNotifyCharacteristic = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void thinkridercontroller::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void thinkridercontroller::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
if (gattCommunicationChannelService.constLast()) {
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&thinkridercontroller::stateChanged);
|
||||
gattCommunicationChannelService.constLast()->discoverDetails();
|
||||
} else {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("thinkridercontroller::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("thinkridercontroller::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &thinkridercontroller::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &thinkridercontroller::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &thinkridercontroller::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &thinkridercontroller::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("LowEnergy controller disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool thinkridercontroller::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void thinkridercontroller::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
|
||||
if (m_control)
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
73
src/devices/thinkridercontroller/thinkridercontroller.h
Normal file
73
src/devices/thinkridercontroller/thinkridercontroller.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef THINKRIDERCONTROLLER_H
|
||||
#define THINKRIDERCONTROLLER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
|
||||
#include "devices/bluetoothdevice.h"
|
||||
|
||||
class thinkridercontroller : public bluetoothdevice {
|
||||
Q_OBJECT
|
||||
public:
|
||||
thinkridercontroller(bluetoothdevice *parentDevice);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
// Thinkrider VS200 UUIDs
|
||||
static const QBluetoothUuid SERVICE_UUID;
|
||||
static const QBluetoothUuid CHARACTERISTIC_UUID;
|
||||
|
||||
// Button patterns
|
||||
static const QByteArray SHIFT_UP_PATTERN;
|
||||
static const QByteArray SHIFT_DOWN_PATTERN;
|
||||
|
||||
QList<QLowEnergyService *> gattCommunicationChannelService;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
|
||||
bluetoothdevice *parentDevice = nullptr;
|
||||
|
||||
bool initDone = false;
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void plus();
|
||||
void minus();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
void disconnectBluetooth();
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
private slots:
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // THINKRIDERCONTROLLER_H
|
||||
@@ -539,8 +539,19 @@ double treadmill::treadmillInclinationOverride(double Inclination) {
|
||||
}
|
||||
|
||||
void treadmill::evaluateStepCount() {
|
||||
// Auto-detect cadence format: if < 120, assume it's per-leg and needs doubling for step count
|
||||
double effectiveCadence = (Cadence.value() < 120 && Cadence.value() > 0) ? Cadence.value() * 2 : Cadence.value();
|
||||
// Auto-detect cadence format: if per-leg, needs doubling for step count
|
||||
// Running (>6 km/h): double if cadence < 120
|
||||
// Walking (<6 km/h): double if cadence < 60
|
||||
double effectiveCadence = Cadence.value();
|
||||
|
||||
if (Speed.value() > 6.0 && Cadence.value() < 120 && Cadence.value() > 0) {
|
||||
// Running: likely per-leg cadence, double it
|
||||
effectiveCadence = Cadence.value() * 2;
|
||||
} else if (Speed.value() > 0 && Speed.value() <= 6.0 && Cadence.value() < 60 && Cadence.value() > 0) {
|
||||
// Walking: likely per-leg cadence, double it
|
||||
effectiveCadence = Cadence.value() * 2;
|
||||
}
|
||||
|
||||
StepCount += (Cadence.lastChanged().msecsTo(QDateTime::currentDateTime())) * (effectiveCadence / 60000);
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,8 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
|
||||
bool iconsole_elliptical =
|
||||
settings.value(QZSettings::iconsole_elliptical, QZSettings::default_iconsole_elliptical).toBool();
|
||||
double cadence_gain = settings.value(QZSettings::cadence_gain, QZSettings::default_cadence_gain).toDouble();
|
||||
double cadence_offset = settings.value(QZSettings::cadence_offset, QZSettings::default_cadence_offset).toDouble();
|
||||
|
||||
qDebug() << characteristic.uuid() << QStringLiteral("<<") << newvalue.toHex(' ');
|
||||
|
||||
@@ -316,8 +318,8 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
double divisor = 1.0;
|
||||
if(E35 || SCH_590E || SCH_411_510E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS)
|
||||
divisor = 2.0;
|
||||
Cadence = (((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index))))) / divisor;
|
||||
Cadence = ((((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index))))) / divisor) * cadence_gain + cadence_offset;
|
||||
}
|
||||
emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value()));
|
||||
|
||||
@@ -352,7 +354,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
double stridesPerMinute = stridesDiff / timeInMinutes;
|
||||
instantCadence = stridesPerMinute / 2.0;
|
||||
if(instantCadence.value() < 120 && instantCadence.average5s() < 200) // sanity check: reject spikes > 120 RPM
|
||||
Cadence = instantCadence.average5s();
|
||||
Cadence = instantCadence.average5s() * cadence_gain + cadence_offset;
|
||||
emit debug(QStringLiteral("Current Cadence (from strideCount): ") + QString::number(Cadence.value()) +
|
||||
QStringLiteral(" (diff: ") + QString::number(stridesDiff) + QStringLiteral(")"));
|
||||
|
||||
@@ -555,8 +557,8 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = ((double)(((uint16_t)((uint8_t)newvalue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newvalue.at(index)))) / 2.0;
|
||||
Cadence = (((double)(((uint16_t)((uint8_t)newvalue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newvalue.at(index)))) / 2.0) * cadence_gain + cadence_offset;
|
||||
emit debug(QStringLiteral("Current Cadence (2AD2): ") + QString::number(Cadence.value()));
|
||||
}
|
||||
index += 2;
|
||||
@@ -692,7 +694,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
} else if (iconsole_elliptical) {
|
||||
if (newvalue.length() == 15) {
|
||||
Speed = (double)((((uint8_t)newvalue.at(10)) << 8) | ((uint8_t)newvalue.at(9))) / 100.0;
|
||||
Cadence = newvalue.at(6);
|
||||
Cadence = newvalue.at(6) * cadence_gain + cadence_offset;
|
||||
m_watt = elliptical::watts();
|
||||
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
|
||||
@@ -234,6 +234,31 @@ class ergTable : public QObject {
|
||||
return maxRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Load default calibration data for a specific bike model.
|
||||
* Only populates if the table is currently empty (won't overwrite user's learned data).
|
||||
* Data format: "cadence|wattage|resistance;cadence|wattage|resistance;..."
|
||||
*/
|
||||
void loadDefaultData(const QString& defaultDataString) {
|
||||
if (!consolidatedData.isEmpty()) {
|
||||
qDebug() << "ergTable: skipping defaults, user data already exists ("
|
||||
<< consolidatedData.size() << "points)";
|
||||
return;
|
||||
}
|
||||
QStringList dataList = defaultDataString.split(";", Qt::SkipEmptyParts);
|
||||
for (const QString& triple : dataList) {
|
||||
QStringList fields = triple.split("|");
|
||||
if (fields.size() == 3) {
|
||||
uint16_t cadence = fields[0].toUInt();
|
||||
uint16_t wattage = fields[1].toUInt();
|
||||
uint16_t resistance = fields[2].toUInt();
|
||||
consolidatedData.append(ergDataPoint(cadence, wattage, resistance));
|
||||
}
|
||||
}
|
||||
qDebug() << "ergTable: loaded" << consolidatedData.size() << "default data points";
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
private:
|
||||
QMap<CadenceResistancePair, WattageStats> wattageData;
|
||||
QList<ergDataPoint> consolidatedData;
|
||||
|
||||
93
src/filesearcher.cpp
Normal file
93
src/filesearcher.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "filesearcher.h"
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
#include <QVariantMap>
|
||||
|
||||
FileSearcher::FileSearcher(QObject *parent)
|
||||
: QObject(parent) {
|
||||
}
|
||||
|
||||
QVariantList FileSearcher::searchRecursively(const QString &basePath,
|
||||
const QString &filterPattern,
|
||||
const QStringList &nameFilters) {
|
||||
QVariantList results;
|
||||
|
||||
// Convert base path from URL if needed
|
||||
QString cleanBasePath = basePath;
|
||||
if (cleanBasePath.startsWith("file://")) {
|
||||
cleanBasePath = QUrl(cleanBasePath).toLocalFile();
|
||||
}
|
||||
|
||||
// Verify base path exists
|
||||
QDir baseDir(cleanBasePath);
|
||||
if (!baseDir.exists()) {
|
||||
qWarning() << "FileSearcher: Base path does not exist:" << cleanBasePath;
|
||||
emit searchCompleted(0);
|
||||
return results;
|
||||
}
|
||||
|
||||
// Convert filter pattern to lowercase for case-insensitive matching
|
||||
QString lowerFilterPattern = filterPattern.toLower();
|
||||
|
||||
qDebug() << "FileSearcher: Starting recursive search in" << cleanBasePath
|
||||
<< "with pattern:" << filterPattern;
|
||||
|
||||
// Start recursive search
|
||||
searchDirectory(cleanBasePath, cleanBasePath, lowerFilterPattern, nameFilters, results);
|
||||
|
||||
qDebug() << "FileSearcher: Search completed, found" << results.size() << "files";
|
||||
emit searchCompleted(results.size());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void FileSearcher::searchDirectory(const QString &dirPath,
|
||||
const QString &basePath,
|
||||
const QString &filterPattern,
|
||||
const QStringList &nameFilters,
|
||||
QVariantList &results) {
|
||||
QDir dir(dirPath);
|
||||
|
||||
// Set name filters for file extensions
|
||||
dir.setNameFilters(nameFilters);
|
||||
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
|
||||
|
||||
// Process files in current directory
|
||||
QFileInfoList files = dir.entryInfoList();
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
// Check if filename matches filter pattern (case-insensitive)
|
||||
if (filterPattern.isEmpty() || fileName.toLower().contains(filterPattern)) {
|
||||
// Calculate relative path
|
||||
QString absolutePath = fileInfo.absoluteFilePath();
|
||||
QString relativePath = absolutePath;
|
||||
if (relativePath.startsWith(basePath)) {
|
||||
relativePath = relativePath.mid(basePath.length());
|
||||
if (relativePath.startsWith("/")) {
|
||||
relativePath = relativePath.mid(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Create result entry
|
||||
QVariantMap resultEntry;
|
||||
resultEntry["fileName"] = fileName;
|
||||
resultEntry["filePath"] = QUrl::fromLocalFile(absolutePath).toString();
|
||||
resultEntry["relativePath"] = relativePath;
|
||||
resultEntry["isFolder"] = false;
|
||||
|
||||
results.append(resultEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process subdirectories
|
||||
dir.setNameFilters(QStringList());
|
||||
dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
QFileInfoList subDirs = dir.entryInfoList();
|
||||
for (const QFileInfo &subDirInfo : subDirs) {
|
||||
searchDirectory(subDirInfo.absoluteFilePath(), basePath, filterPattern, nameFilters, results);
|
||||
}
|
||||
}
|
||||
59
src/filesearcher.h
Normal file
59
src/filesearcher.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef FILESEARCHER_H
|
||||
#define FILESEARCHER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
/**
|
||||
* @brief FileSearcher provides fast recursive file searching functionality for QML
|
||||
*
|
||||
* This class performs recursive directory scanning in C++ for much better performance
|
||||
* compared to QML-based solutions using FolderListModel.
|
||||
*/
|
||||
class FileSearcher : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FileSearcher(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Search recursively for files matching a filter pattern
|
||||
* @param basePath The root directory to start searching from
|
||||
* @param filterPattern The search pattern (case-insensitive substring match on filename)
|
||||
* @param nameFilters File extensions to include (e.g., ["*.xml", "*.zwo"])
|
||||
* @return QVariantList containing search results, each with:
|
||||
* - fileName: The file name
|
||||
* - filePath: The full file path (as URL string)
|
||||
* - relativePath: Path relative to basePath
|
||||
* - isFolder: Always false for file results
|
||||
*/
|
||||
Q_INVOKABLE QVariantList searchRecursively(const QString &basePath,
|
||||
const QString &filterPattern,
|
||||
const QStringList &nameFilters = QStringList() << "*.xml" << "*.zwo");
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted when search completes
|
||||
* @param resultCount Number of files found
|
||||
*/
|
||||
void searchCompleted(int resultCount);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Internal recursive search implementation
|
||||
* @param dir Current directory being scanned
|
||||
* @param basePath Original search root for calculating relative paths
|
||||
* @param filterPattern Search pattern (lowercase)
|
||||
* @param nameFilters File extension filters
|
||||
* @param results Output list to accumulate results
|
||||
*/
|
||||
void searchDirectory(const QString &dir,
|
||||
const QString &basePath,
|
||||
const QString &filterPattern,
|
||||
const QStringList &nameFilters,
|
||||
QVariantList &results);
|
||||
};
|
||||
|
||||
#endif // FILESEARCHER_H
|
||||
@@ -391,6 +391,9 @@ bool GarminConnect::fetchCsrfToken()
|
||||
bool GarminConnect::performLogin(const QString &email, const QString &password, bool suppressMfaSignal)
|
||||
{
|
||||
qDebug() << "GarminConnect: Performing login...";
|
||||
qDebug() << "GarminConnect: Using domain:" << m_domain;
|
||||
qDebug() << "GarminConnect: SSO URL:" << ssoUrl();
|
||||
qDebug() << "GarminConnect: Connect API URL:" << connectApiUrl();
|
||||
|
||||
QString ssoEmbedUrl = ssoUrl() + SSO_EMBED_PATH;
|
||||
|
||||
@@ -452,15 +455,54 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
qDebug() << "GarminConnect: Login response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(300);
|
||||
|
||||
// Check for success title (like Python garth library)
|
||||
// Check page title (like Python garth library)
|
||||
// garth checks ONLY the title for MFA detection, not the body
|
||||
// This is important because some servers (like garmin.cn) may have "MFA" text
|
||||
// in their Success page HTML body, which would cause false positives
|
||||
QString pageTitle;
|
||||
QRegularExpression titleRegex("<title>(.+?)</title>");
|
||||
QRegularExpressionMatch titleMatch = titleRegex.match(response);
|
||||
if (titleMatch.hasMatch()) {
|
||||
QString title = titleMatch.captured(1);
|
||||
qDebug() << "GarminConnect: Page title:" << title;
|
||||
if (title == "Success") {
|
||||
qDebug() << "GarminConnect: Login successful (Success page detected)";
|
||||
pageTitle = titleMatch.captured(1);
|
||||
qDebug() << "GarminConnect: Page title:" << pageTitle;
|
||||
}
|
||||
|
||||
// Check if MFA is required by looking at the TITLE (garth approach)
|
||||
// This is more reliable than checking the body which may contain "MFA" in scripts/URLs
|
||||
if (pageTitle.contains("MFA", Qt::CaseInsensitive)) {
|
||||
m_lastError = "MFA Required";
|
||||
qDebug() << "GarminConnect: MFA detected in page title";
|
||||
|
||||
// Extract new CSRF token from MFA page - try multiple patterns
|
||||
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
|
||||
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
|
||||
|
||||
QRegularExpressionMatch match = csrfRegex1.match(response);
|
||||
if (!match.hasMatch()) {
|
||||
match = csrfRegex2.match(response);
|
||||
}
|
||||
if (match.hasMatch()) {
|
||||
m_csrfToken = match.captured(1);
|
||||
qDebug() << "GarminConnect: CSRF token from MFA page:" << m_csrfToken.left(20) << "...";
|
||||
}
|
||||
|
||||
// Update cookies
|
||||
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
|
||||
|
||||
if (!suppressMfaSignal) {
|
||||
qDebug() << "GarminConnect: Emitting mfaRequired signal";
|
||||
emit mfaRequired();
|
||||
} else {
|
||||
qDebug() << "GarminConnect: MFA required but signal suppressed (retrying with MFA code)";
|
||||
}
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if login was successful (title is "Success")
|
||||
if (pageTitle == "Success") {
|
||||
qDebug() << "GarminConnect: Login successful (Success page detected)";
|
||||
// Continue to extract ticket below
|
||||
}
|
||||
|
||||
// Check for error messages in response
|
||||
@@ -549,39 +591,17 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if MFA is required (legacy check for non-redirect MFA)
|
||||
if (response.contains("MFA", Qt::CaseInsensitive) ||
|
||||
response.contains("Enter MFA Code", Qt::CaseInsensitive)) {
|
||||
m_lastError = "MFA Required";
|
||||
qDebug() << "GarminConnect: MFA content detected in response";
|
||||
|
||||
// Extract new CSRF token from MFA page - try multiple patterns
|
||||
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
|
||||
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
|
||||
|
||||
QRegularExpressionMatch match = csrfRegex1.match(response);
|
||||
if (!match.hasMatch()) {
|
||||
match = csrfRegex2.match(response);
|
||||
}
|
||||
if (match.hasMatch()) {
|
||||
m_csrfToken = match.captured(1);
|
||||
}
|
||||
|
||||
// Update cookies
|
||||
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
|
||||
|
||||
if (!suppressMfaSignal) {
|
||||
emit mfaRequired();
|
||||
}
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract ticket from response URL (already declared above)
|
||||
if (responseUrl.isEmpty()) {
|
||||
responseUrl = reply->url();
|
||||
}
|
||||
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response URL:" << responseUrl.toString();
|
||||
qDebug() << "GarminConnect: Response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Full response body:" << response;
|
||||
}
|
||||
|
||||
QUrlQuery responseQuery(responseUrl);
|
||||
QString ticket = responseQuery.queryItemValue("ticket");
|
||||
|
||||
@@ -599,6 +619,8 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
if (match.hasMatch()) {
|
||||
ticket = match.captured(1);
|
||||
qDebug() << "GarminConnect: Found ticket with fallback pattern:" << ticket.left(20) << "...";
|
||||
} else if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: No ticket patterns matched in response body";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -608,6 +630,9 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
if (ticket.isEmpty()) {
|
||||
m_lastError = "Failed to extract ticket from login response";
|
||||
qDebug() << "GarminConnect:" << m_lastError;
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -708,8 +733,12 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
qDebug() << "GarminConnect: MFA response status code:" << statusCode;
|
||||
qDebug() << "GarminConnect: MFA response redirect URL:" << responseUrl.toString();
|
||||
|
||||
// If no redirect, log response body to understand what happened
|
||||
if (responseUrl.isEmpty()) {
|
||||
// Log detailed response information
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: MFA response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Full MFA response body:" << response;
|
||||
} else if (responseUrl.isEmpty()) {
|
||||
// If no redirect, log response body to understand what happened (non-verbose)
|
||||
qDebug() << "GarminConnect: MFA response body (first 500 chars):" << response.left(500);
|
||||
}
|
||||
|
||||
@@ -748,6 +777,9 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
|
||||
// If not found in redirect URL, try response body
|
||||
if (ticket.isEmpty() && !response.isEmpty()) {
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Attempting to extract ticket from MFA response body";
|
||||
}
|
||||
// Try multiple patterns for ticket extraction
|
||||
QRegularExpression ticketRegex1("embed\\?ticket=([^\"]+)\"");
|
||||
QRegularExpression ticketRegex2("ticket=([^&\"']+)");
|
||||
@@ -761,6 +793,16 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
if (match.hasMatch()) {
|
||||
ticket = match.captured(1);
|
||||
qDebug() << "GarminConnect: Found ticket in response body (pattern 2):" << ticket.left(20) << "...";
|
||||
} else if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: No MFA ticket patterns matched. Checking for other patterns...";
|
||||
// Check for JSON format
|
||||
if (response.contains("ticket")) {
|
||||
qDebug() << "GarminConnect: Response contains 'ticket' keyword, may be JSON or different format";
|
||||
}
|
||||
// Check for common response patterns
|
||||
if (response.contains("\"")) {
|
||||
qDebug() << "GarminConnect: Response contains quoted strings (may be JSON)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,6 +812,9 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
if (ticket.isEmpty()) {
|
||||
m_lastError = "Failed to extract ticket after MFA";
|
||||
qDebug() << "GarminConnect:" << m_lastError;
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
|
||||
}
|
||||
emit authenticationFailed(m_lastError);
|
||||
return;
|
||||
}
|
||||
@@ -1401,6 +1446,7 @@ void GarminConnect::loadTokensFromSettings()
|
||||
m_oauth1Token.oauth_token = settings.value(QZSettings::garmin_oauth1_token, QZSettings::default_garmin_oauth1_token).toString();
|
||||
m_oauth1Token.oauth_token_secret = settings.value(QZSettings::garmin_oauth1_token_secret, QZSettings::default_garmin_oauth1_token_secret).toString();
|
||||
m_domain = settings.value(QZSettings::garmin_domain, QZSettings::default_garmin_domain).toString();
|
||||
qDebug() << "GarminConnect: Loaded Garmin domain from settings:" << m_domain;
|
||||
|
||||
if (!m_oauth2Token.access_token.isEmpty()) {
|
||||
qDebug() << "GarminConnect: Loaded tokens from settings (OAuth1 + OAuth2)";
|
||||
|
||||
@@ -176,6 +176,7 @@ private:
|
||||
static constexpr const char* SSO_URL_PATH = "/sso/signin";
|
||||
static constexpr const char* SSO_EMBED_PATH = "/sso/embed";
|
||||
static constexpr const char* OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json";
|
||||
static constexpr bool DEBUG_GARMIN_VERBOSE = false; // Set to true for detailed response logging (may contain sensitive data)
|
||||
|
||||
// Private methods
|
||||
QString ssoUrl() const { return QString("https://sso.%1").arg(m_domain); }
|
||||
|
||||
102
src/homeform.cpp
102
src/homeform.cpp
@@ -312,6 +312,8 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
QStringLiteral("0"), true, QStringLiteral("autoVirtualShiftingSprint"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Sprint", QStringLiteral("red"));
|
||||
powerAvg = new DataObject(QStringLiteral("Power Avg"), QStringLiteral("icons/icons/watt.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("powerAvg"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Off", QStringLiteral("grey"));
|
||||
hrv = new DataObject(QStringLiteral("HRV (ms)"), QStringLiteral("icons/icons/heart_red.png"),
|
||||
QStringLiteral("0"), false, QStringLiteral("hrv"), 48, labelFontSize);
|
||||
pidHR = new DataObject(QStringLiteral("PID Heart"), QStringLiteral("icons/icons/heart_red.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("pid_hr"), 48, labelFontSize);
|
||||
extIncline = new DataObject(QStringLiteral("Ext.Inclin.(%)"), QStringLiteral("icons/icons/inclination.png"),
|
||||
@@ -558,6 +560,10 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
&homeform::pelotonOffset_Minus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Plus, this, &homeform::gearUp);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Minus, this, &homeform::gearDown);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::speed_Plus, this, &homeform::speedPlus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::speed_Minus, this, &homeform::speedMinus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::inclination_Plus, this, &homeform::inclinationPlus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::inclination_Minus, this, &homeform::inclinationMinus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonOffset, this, &homeform::pelotonOffset);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonAskStart, this, &homeform::pelotonAskStart);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::peloton_start_workout, this,
|
||||
@@ -1610,6 +1616,22 @@ void homeform::gearDown() {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::speedPlus() {
|
||||
Plus(QStringLiteral("speed"));
|
||||
}
|
||||
|
||||
void homeform::speedMinus() {
|
||||
Minus(QStringLiteral("speed"));
|
||||
}
|
||||
|
||||
void homeform::inclinationPlus() {
|
||||
Plus(QStringLiteral("inclination"));
|
||||
}
|
||||
|
||||
void homeform::inclinationMinus() {
|
||||
Minus(QStringLiteral("inclination"));
|
||||
}
|
||||
|
||||
void homeform::ftmsAccessoryConnected(smartspin2k *d) {
|
||||
connect(this, &homeform::autoResistanceChanged, d, &smartspin2k::autoResistanceChanged);
|
||||
connect(d, &smartspin2k::gearUp, this, &homeform::gearUp);
|
||||
@@ -1738,6 +1760,12 @@ void homeform::sortTiles() {
|
||||
dataList.append(heart);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_hrv_enabled, QZSettings::default_tile_hrv_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_hrv_order, QZSettings::default_tile_hrv_order).toInt() == i) {
|
||||
hrv->setGridId(i);
|
||||
dataList.append(hrv);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_fan_enabled, true).toBool() &&
|
||||
settings.value(QZSettings::tile_fan_order, 0).toInt() == i) {
|
||||
fan->setGridId(i);
|
||||
@@ -2127,6 +2155,12 @@ void homeform::sortTiles() {
|
||||
dataList.append(heart);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_hrv_enabled, QZSettings::default_tile_hrv_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_hrv_order, QZSettings::default_tile_hrv_order).toInt() == i) {
|
||||
hrv->setGridId(i);
|
||||
dataList.append(hrv);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_fan_enabled, true).toBool() &&
|
||||
settings.value(QZSettings::tile_fan_order, 0).toInt() == i) {
|
||||
fan->setGridId(i);
|
||||
@@ -2516,6 +2550,12 @@ void homeform::sortTiles() {
|
||||
dataList.append(heart);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_hrv_enabled, QZSettings::default_tile_hrv_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_hrv_order, QZSettings::default_tile_hrv_order).toInt() == i) {
|
||||
hrv->setGridId(i);
|
||||
dataList.append(hrv);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_fan_enabled, true).toBool() &&
|
||||
settings.value(QZSettings::tile_fan_order, 0).toInt() == i) {
|
||||
fan->setGridId(i);
|
||||
@@ -2990,6 +3030,12 @@ void homeform::sortTiles() {
|
||||
dataList.append(heart);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_hrv_enabled, QZSettings::default_tile_hrv_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_hrv_order, QZSettings::default_tile_hrv_order).toInt() == i) {
|
||||
hrv->setGridId(i);
|
||||
dataList.append(hrv);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_fan_enabled, true).toBool() &&
|
||||
settings.value(QZSettings::tile_fan_order, 0).toInt() == i) {
|
||||
fan->setGridId(i);
|
||||
@@ -3356,6 +3402,12 @@ void homeform::sortTiles() {
|
||||
dataList.append(heart);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_hrv_enabled, QZSettings::default_tile_hrv_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_hrv_order, QZSettings::default_tile_hrv_order).toInt() == i) {
|
||||
hrv->setGridId(i);
|
||||
dataList.append(hrv);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_fan_enabled, true).toBool() &&
|
||||
settings.value(QZSettings::tile_fan_order, 0).toInt() == i) {
|
||||
fan->setGridId(i);
|
||||
@@ -3719,6 +3771,12 @@ void homeform::sortTiles() {
|
||||
dataList.append(heart);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_hrv_enabled, QZSettings::default_tile_hrv_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_hrv_order, QZSettings::default_tile_hrv_order).toInt() == i) {
|
||||
hrv->setGridId(i);
|
||||
dataList.append(hrv);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_fan_enabled, true).toBool() &&
|
||||
settings.value(QZSettings::tile_fan_order, 0).toInt() == i) {
|
||||
fan->setGridId(i);
|
||||
@@ -5396,6 +5454,7 @@ void homeform::update() {
|
||||
double stepCount = 0;
|
||||
|
||||
bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
bool weight_kg_unit = settings.value(QZSettings::weight_kg_unit, QZSettings::default_weight_kg_unit).toBool();
|
||||
double ftpSetting = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble();
|
||||
double unit_conversion = 1.0;
|
||||
double meter_feet_conversion = 1.0;
|
||||
@@ -5509,7 +5568,19 @@ void homeform::update() {
|
||||
QString::number((bluetoothManager->device())->currentSpeed().average() * unit_conversion, 'f', 1) +
|
||||
QStringLiteral(" MAX: ") +
|
||||
QString::number((bluetoothManager->device())->currentSpeed().max() * unit_conversion, 'f', 1));
|
||||
heart->setValue(QString::number(bluetoothManager->device()->currentHeart().value(), 'f', 0));
|
||||
// Heart rate display - show as percentage if enabled
|
||||
if (settings.value(QZSettings::tile_heart_show_as_percent, QZSettings::default_tile_heart_show_as_percent).toBool()) {
|
||||
double currentHR = bluetoothManager->device()->currentHeart().value();
|
||||
double maxHR = heartRateMax();
|
||||
double hrPercent = (currentHR / maxHR) * 100.0;
|
||||
heart->setValue(QString::number(hrPercent, 'f', 0) + "%");
|
||||
} else {
|
||||
heart->setValue(QString::number(bluetoothManager->device()->currentHeart().value(), 'f', 0));
|
||||
}
|
||||
hrv->setValue(QString::number(bluetoothManager->device()->currentHRV().value(), 'f', 2));
|
||||
hrv->setSecondLine(QStringLiteral("AVG: ") +
|
||||
QString::number(bluetoothManager->device()->currentHRV().average(), 'f', 2));
|
||||
|
||||
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
calories->setValue(QString::number(bluetoothManager->device()->calories().value(), 'f', 0));
|
||||
@@ -5679,7 +5750,7 @@ void homeform::update() {
|
||||
datetime->setValue(formattedTime);
|
||||
watts = bluetoothManager->device()->wattsMetricforUI();
|
||||
watt->setValue(QString::number(watts, 'f', 0));
|
||||
weightLoss->setValue(QString::number(miles ? bluetoothManager->device()->weightLoss() * 35.274
|
||||
weightLoss->setValue(QString::number((miles && !weight_kg_unit) ? bluetoothManager->device()->weightLoss() * 35.274
|
||||
: bluetoothManager->device()->weightLoss(),
|
||||
'f', 2));
|
||||
|
||||
@@ -6752,10 +6823,22 @@ void homeform::update() {
|
||||
}
|
||||
bluetoothManager->device()->setHeartZone(currentHRZone);
|
||||
Z = QStringLiteral("Z") + QString::number(currentHRZone, 'f', 1);
|
||||
heart->setSecondLine(Z + QStringLiteral(" AVG: ") +
|
||||
QString::number((bluetoothManager->device())->currentHeart().average(), 'f', 0) +
|
||||
QStringLiteral(" MAX: ") +
|
||||
QString::number((bluetoothManager->device())->currentHeart().max(), 'f', 0));
|
||||
|
||||
// Heart rate second line - show as percentage if enabled
|
||||
if (settings.value(QZSettings::tile_heart_show_as_percent, QZSettings::default_tile_heart_show_as_percent).toBool()) {
|
||||
double maxHR = heartRateMax();
|
||||
double avgHRPercent = ((bluetoothManager->device())->currentHeart().average() / maxHR) * 100.0;
|
||||
double maxHRPercent = ((bluetoothManager->device())->currentHeart().max() / maxHR) * 100.0;
|
||||
heart->setSecondLine(Z + QStringLiteral(" AVG: ") +
|
||||
QString::number(avgHRPercent, 'f', 0) + "%" +
|
||||
QStringLiteral(" MAX: ") +
|
||||
QString::number(maxHRPercent, 'f', 0) + "%");
|
||||
} else {
|
||||
heart->setSecondLine(Z + QStringLiteral(" AVG: ") +
|
||||
QString::number((bluetoothManager->device())->currentHeart().average(), 'f', 0) +
|
||||
QStringLiteral(" MAX: ") +
|
||||
QString::number((bluetoothManager->device())->currentHeart().max(), 'f', 0));
|
||||
}
|
||||
|
||||
/*
|
||||
if(trainProgram)
|
||||
@@ -7685,7 +7768,8 @@ void homeform::update() {
|
||||
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());
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(), bluetoothManager->device()->HeatStrainIndex.value(),
|
||||
0.0, QList<double>());
|
||||
|
||||
Session.append(gapFill);
|
||||
qDebug() << "Added gap-filling SessionLine for elapsed time:" << (lastRecordedTime + i);
|
||||
@@ -7721,7 +7805,9 @@ void homeform::update() {
|
||||
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());
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(), bluetoothManager->device()->HeatStrainIndex.value(),
|
||||
bluetoothManager->device()->currentHRV().value(),
|
||||
bluetoothManager->device()->getRRIntervalsAndClear());
|
||||
|
||||
Session.append(s);
|
||||
|
||||
|
||||
@@ -821,6 +821,7 @@ class homeform : public QObject {
|
||||
DataObject *autoVirtualShiftingClimb;
|
||||
DataObject *autoVirtualShiftingSprint;
|
||||
DataObject *powerAvg;
|
||||
DataObject *hrv;
|
||||
|
||||
private:
|
||||
static homeform *m_singleton;
|
||||
@@ -1056,6 +1057,10 @@ class homeform : public QObject {
|
||||
void sortTilesTimeout();
|
||||
void gearUp();
|
||||
void gearDown();
|
||||
void speedPlus();
|
||||
void speedMinus();
|
||||
void inclinationPlus();
|
||||
void inclinationMinus();
|
||||
void changeTimestamp(QTime source, QTime actual);
|
||||
void pelotonOffset_Plus();
|
||||
void pelotonOffset_Minus();
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.treadmill-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -73,20 +77,24 @@
|
||||
<tr class="speed" sort-order="0">
|
||||
<td class="icon">🏃</td>
|
||||
<td style="text-align: left">SPEED</td>
|
||||
<td class="speed-avg-title"><small>AVG</small></td>
|
||||
<td class="speed-avg">0.0</td>
|
||||
<td class="speed-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedMinus()">-</button></td>
|
||||
<td class="speed-avg"><span class="speed-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="speed-value values"><b>0.0</b></td>
|
||||
<td class="speed-max-title"><small>MAX</small></td>
|
||||
<td class="speed-max">0.0</td>
|
||||
<td class="speed-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedPlus()">+</button></td>
|
||||
<td class="speed-max"><span class="speed-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="inclination" sort-order="1">
|
||||
<td class="icon">📐</td>
|
||||
<td style="text-align: left">INCLINE</td>
|
||||
<td><small>AVG</small></td>
|
||||
<td class="inclination-avg">0.0</td>
|
||||
<td class="inclination-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationMinus()">-</button></td>
|
||||
<td class="inclination-avg"><span class="inclination-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="inclination-value values"><b>0.0</b></td>
|
||||
<td><small>MAX</small></td>
|
||||
<td class="inclination-max">0.0</td>
|
||||
<td class="inclination-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationPlus()">+</button></td>
|
||||
<td class="inclination-max"><span class="inclination-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="pace" sort-order="2">
|
||||
<td class="icon">🏃</td>
|
||||
@@ -306,6 +314,62 @@
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function Lap() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'lap',
|
||||
@@ -611,6 +675,7 @@
|
||||
'speed_color', 'pace_color', 'power_zone_color', 'target_power_zone_color', 'cadence_color', 'heart_color', 'watts_color',
|
||||
'peloton_resistance_color', 'target_resistance', 'target_peloton_resistance',
|
||||
'target_cadence', 'target_power', 'peloton_offset', 'peloton_ask_start', 'target_speed', 'target_pace_h', 'target_pace_m', 'target_pace_s',
|
||||
'deviceType', 'TREADMILL_TYPE',
|
||||
'inclination', 'inclination_lapavg',
|
||||
'inclination_lapmax', 'target_inclination', 'power_zone', 'power_zone_lapavg', 'power_zone_lapmax', 'target_power_zone', 'jouls',
|
||||
'row_remaining_time_s', 'row_remaining_time_m', 'row_remaining_time_h' , 'autoresistance', 'gears', 'elevation', 'pace_s' , 'pace_m',
|
||||
@@ -673,6 +738,8 @@
|
||||
var peloton_offset = 0;
|
||||
var gears = 0;
|
||||
var nextrow = "";
|
||||
var deviceType = -1;
|
||||
var TREADMILL_TYPE = -1;
|
||||
|
||||
for (let key of keys_arr) {
|
||||
if (msg.content[key] === undefined || msg.content[key] === null)
|
||||
@@ -789,6 +856,10 @@
|
||||
peloton_offset = msg.content[key];
|
||||
} else if (key === 'gears') {
|
||||
gears = msg.content[key];
|
||||
} else if (key === 'deviceType') {
|
||||
deviceType = msg.content[key];
|
||||
} else if (key === 'TREADMILL_TYPE') {
|
||||
TREADMILL_TYPE = msg.content[key];
|
||||
} else if (key === 'peloton_resistance_color') {
|
||||
$('.pelotonresistance-value').css('color', msg.content[key]);
|
||||
} else if (key === 'heart_color') {
|
||||
@@ -837,14 +908,24 @@
|
||||
|
||||
$('.speed-value').html("<b>" + speed.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.speed-avg').html(speed_lapavg.toFixed(1));
|
||||
$('.speed-max').html(speed_lapmax.toFixed(1));
|
||||
$('.speed-avg-value').html(speed_lapavg.toFixed(1));
|
||||
$('.speed-max-value').html(speed_lapmax.toFixed(1));
|
||||
if (tile_target_inclination_enabled && target_inclination > 0)
|
||||
$('.inclination-value').html("<b>" + inclination.toFixed(1) + "/" + target_inclination.toFixed(1) + "</b>");
|
||||
else
|
||||
$('.inclination-value').html("<b>" + inclination.toFixed(1) + "</b>");
|
||||
$('.inclination-avg').html(inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max').html(inclination_lapmax.toFixed(1));
|
||||
$('.inclination-avg-value').html(inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max-value').html(inclination_lapmax.toFixed(1));
|
||||
|
||||
// Show/hide treadmill-only controls based on device type
|
||||
if (deviceType === TREADMILL_TYPE && TREADMILL_TYPE !== -1) {
|
||||
$('.treadmill-only').show();
|
||||
$('.non-treadmill').hide();
|
||||
} else {
|
||||
$('.treadmill-only').hide();
|
||||
$('.non-treadmill').show();
|
||||
}
|
||||
|
||||
$('.elevation-value').html("<b>" + elevation.toFixed(1) + "</b>");
|
||||
if (tile_target_cadence_enabled && target_cadence > 0)
|
||||
$('.cadence-value').html("<b>" + cadence.toFixed(0) + "/" + target_cadence.toFixed(0) + "</b>");
|
||||
|
||||
@@ -197,6 +197,10 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.treadmill-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Metric selector panel */
|
||||
.metric-selector-panel {
|
||||
display: none;
|
||||
@@ -247,20 +251,24 @@
|
||||
<tr class="speed" sort-order="0">
|
||||
<td class="icon">🏃</td>
|
||||
<td style="text-align: left">SPEED</td>
|
||||
<td class="speed-avg-title"><small>AVG</small></td>
|
||||
<td class="speed-avg">0.0</td>
|
||||
<td class="speed-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedMinus()">-</button></td>
|
||||
<td class="speed-avg"><span class="speed-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="speed-value values"><b>0.0</b></td>
|
||||
<td class="speed-max-title"><small>MAX</small></td>
|
||||
<td class="speed-max">0.0</td>
|
||||
<td class="speed-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedPlus()">+</button></td>
|
||||
<td class="speed-max"><span class="speed-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="inclination" sort-order="1">
|
||||
<td class="icon">📐</td>
|
||||
<td style="text-align: left">INCLINE</td>
|
||||
<td><small>AVG</small></td>
|
||||
<td class="inclination-avg">0.0</td>
|
||||
<td class="inclination-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationMinus()">-</button></td>
|
||||
<td class="inclination-avg"><span class="inclination-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="inclination-value values"><b>0.0</b></td>
|
||||
<td><small>MAX</small></td>
|
||||
<td class="inclination-max">0.0</td>
|
||||
<td class="inclination-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationPlus()">+</button></td>
|
||||
<td class="inclination-max"><span class="inclination-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="pace" sort-order="2">
|
||||
<td class="icon">🏃</td>
|
||||
@@ -826,6 +834,62 @@
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to clear/lap
|
||||
function Lap() {
|
||||
let el = new MainWSQueueElement({
|
||||
@@ -1072,6 +1136,7 @@
|
||||
'speed_color', 'pace_color', 'power_zone_color', 'target_power_zone_color', 'cadence_color', 'heart_color', 'watts_color',
|
||||
'peloton_resistance_color', 'target_resistance', 'target_peloton_resistance',
|
||||
'target_cadence', 'target_power', 'peloton_offset', 'peloton_ask_start', 'target_speed', 'target_pace_h', 'target_pace_m', 'target_pace_s',
|
||||
'deviceType', 'TREADMILL_TYPE',
|
||||
'inclination', 'inclination_lapavg',
|
||||
'inclination_lapmax', 'target_inclination', 'power_zone', 'power_zone_lapavg', 'power_zone_lapmax', 'target_power_zone', 'jouls',
|
||||
'row_remaining_time_s', 'row_remaining_time_m', 'row_remaining_time_h' , 'autoresistance', 'gears', 'elevation', 'pace_s' , 'pace_m',
|
||||
@@ -1137,6 +1202,8 @@
|
||||
var peloton_offset = 0;
|
||||
var gears = 0;
|
||||
var nextrow = "";
|
||||
var deviceType = -1;
|
||||
var TREADMILL_TYPE = -1;
|
||||
|
||||
// Get values from message
|
||||
for (let key of keys_arr) {
|
||||
@@ -1255,6 +1322,10 @@
|
||||
peloton_offset = msg.content[key];
|
||||
} else if (key === 'gears') {
|
||||
gears = msg.content[key];
|
||||
} else if (key === 'deviceType') {
|
||||
deviceType = msg.content[key];
|
||||
} else if (key === 'TREADMILL_TYPE') {
|
||||
TREADMILL_TYPE = msg.content[key];
|
||||
} else if (key === 'peloton_resistance_color') {
|
||||
$('.pelotonresistance-value').css('color', msg.content[key]);
|
||||
} else if (key === 'heart_color') {
|
||||
@@ -1336,7 +1407,9 @@
|
||||
target_power: target_power,
|
||||
peloton_offset: peloton_offset,
|
||||
gears: gears,
|
||||
nextrow: nextrow
|
||||
nextrow: nextrow,
|
||||
deviceType: deviceType,
|
||||
TREADMILL_TYPE: TREADMILL_TYPE
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@@ -1356,8 +1429,8 @@
|
||||
} else {
|
||||
$('.speed-value').html("<b>" + data.speed.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.speed-avg').html(data.speed_lapavg.toFixed(1));
|
||||
$('.speed-max').html(data.speed_lapmax.toFixed(1));
|
||||
$('.speed-avg-value').html(data.speed_lapavg.toFixed(1));
|
||||
$('.speed-max-value').html(data.speed_lapmax.toFixed(1));
|
||||
|
||||
// Inclination
|
||||
if (tile_target_inclination_enabled && data.target_inclination > 0) {
|
||||
@@ -1365,8 +1438,17 @@
|
||||
} else {
|
||||
$('.inclination-value').html("<b>" + data.inclination.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.inclination-avg').html(data.inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max').html(data.inclination_lapmax.toFixed(1));
|
||||
$('.inclination-avg-value').html(data.inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max-value').html(data.inclination_lapmax.toFixed(1));
|
||||
|
||||
// Show/hide treadmill-only controls based on device type
|
||||
if (data.deviceType === data.TREADMILL_TYPE && data.TREADMILL_TYPE !== undefined) {
|
||||
$('.treadmill-only').show();
|
||||
$('.non-treadmill').hide();
|
||||
} else {
|
||||
$('.treadmill-only').hide();
|
||||
$('.non-treadmill').show();
|
||||
}
|
||||
|
||||
// Elevation
|
||||
$('.elevation-value').html("<b>" + data.elevation.toFixed(1) + "</b>");
|
||||
|
||||
@@ -71,13 +71,13 @@ viewer.trackedEntity = bike;
|
||||
</body>
|
||||
<body>
|
||||
<div id="cesiumContainer" class="cesiumContainer"></div>
|
||||
<div id="metricsContainer" style="position: absolute; bottom: 0px; right: 0px; width: 200px; height: 250px; touch-action: none; user-select: none;">
|
||||
<div class="metrics" style="color: #FFFFFF; width: 100%; height: 100%; margin: 0; border-radius: 25px; border: 2px solid #73AD21; background: #73AD21; padding: 8px; box-sizing: border-box; overflow: hidden; position: relative;">
|
||||
<div id="metricsContainer" style="position: absolute; bottom: 0px; right: 0px; width: 200px; height: 250px; touch-action: none; user-select: none; z-index: 1000;">
|
||||
<div class="metrics" style="color: #FFFFFF; width: 100%; height: 100%; margin: 0; border-radius: 25px; border: 2px solid #73AD21; background: #73AD21; padding: 8px; box-sizing: border-box; overflow: hidden; position: relative; touch-action: none;">
|
||||
<div id="metricsText" style="font-size: 12px; line-height: 1.4;">🏃Speed: 0.00<br>🚴Cadence:0<br>💓Heart:0<br>🔥Calories:0.0<br>📏Odometer:0.00<br>⚡Watt:0<br>⏲️Elapsed:0:00:00<br>📐Inclination:0.0<br>🧲Resistance:0<br>✈️Altitude:0.0<br>⛰️Elevation:0.0</div>
|
||||
<div id="resizeHandle" style="position: absolute; bottom: 0; right: 0; width: 20px; height: 20px; background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.5) 50%); cursor: nwse-resize; border-bottom-right-radius: 23px;"></div>
|
||||
<div id="resizeHandle" style="position: absolute; bottom: 0; right: 0; width: 50px; height: 50px; background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.8) 50%); cursor: nwse-resize; border-bottom-right-radius: 23px; display: flex; align-items: flex-end; justify-content: flex-end; font-size: 24px; color: rgba(0,0,0,0.6); padding-bottom: 2px; padding-right: 4px;">⤡</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border: 0px solid #aaa; border-radius: 10px; overflow: hidden; position:absolute; bottom: 0px; right: 150px; width=150px; height=75px"><canvas id="canvasChart" style="width=150px; height=75px; border-right: 0pt solid #ffff00;"></canvas></div>
|
||||
<div id="chartContainer" style="border: 0px solid #aaa; border-radius: 10px; overflow: hidden; position:absolute; bottom: 0px; right: 150px; width=150px; height=75px; z-index: 999; touch-action: none;"><canvas id="canvasChart" style="width=150px; height=75px; border-right: 0pt solid #ffff00;"></canvas></div>
|
||||
<script type="text/javascript">
|
||||
let cameraComplete = true
|
||||
let lastAzimuth = 0
|
||||
@@ -325,12 +325,29 @@ console.error('Error is ' + err);
|
||||
const container = document.getElementById('metricsContainer');
|
||||
const resizeHandle = document.getElementById('resizeHandle');
|
||||
const metricsText = document.getElementById('metricsText');
|
||||
const chartContainer = document.getElementById('chartContainer');
|
||||
|
||||
let isDragging = false;
|
||||
let isResizing = false;
|
||||
let startX, startY, startLeft, startTop, startWidth, startHeight;
|
||||
let resizeTimeout = null;
|
||||
|
||||
// Update chart position to follow metrics container
|
||||
function updateChartPosition() {
|
||||
// Position chart to the left of metrics container, aligned at bottom
|
||||
const metricsLeft = container.offsetLeft;
|
||||
const metricsTop = container.offsetTop;
|
||||
const metricsHeight = container.offsetHeight;
|
||||
const chartWidth = chartContainer.offsetWidth;
|
||||
const chartHeight = chartContainer.offsetHeight;
|
||||
|
||||
// Position chart: 10px to the left of metrics, aligned at bottom
|
||||
chartContainer.style.left = (metricsLeft - chartWidth - 10) + 'px';
|
||||
chartContainer.style.top = (metricsTop + metricsHeight - chartHeight) + 'px';
|
||||
chartContainer.style.right = 'auto';
|
||||
chartContainer.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
// Load saved position and size
|
||||
function loadState() {
|
||||
const saved = localStorage.getItem('metricsContainerState');
|
||||
@@ -343,6 +360,7 @@ console.error('Error is ' + err);
|
||||
if (state.bottom !== undefined) container.style.bottom = state.bottom + 'px';
|
||||
if (state.width) container.style.width = state.width + 'px';
|
||||
if (state.height) container.style.height = state.height + 'px';
|
||||
updateChartPosition();
|
||||
updateFontSize();
|
||||
} catch (e) {
|
||||
console.error('Error loading metrics state:', e);
|
||||
@@ -438,6 +456,18 @@ console.error('Error is ' + err);
|
||||
return { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
|
||||
// Check if touch/click is in resize handle area (bottom-right 50x50px)
|
||||
function isInResizeHandle(x, y) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const handleSize = 50;
|
||||
return (
|
||||
x >= rect.right - handleSize &&
|
||||
x <= rect.right &&
|
||||
y >= rect.bottom - handleSize &&
|
||||
y <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Start dragging
|
||||
function startDrag(e) {
|
||||
if (isResizing) return;
|
||||
@@ -449,6 +479,15 @@ console.error('Error is ' + err);
|
||||
startTop = container.offsetTop;
|
||||
container.style.right = 'auto';
|
||||
container.style.bottom = 'auto';
|
||||
|
||||
// Add global listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
document.addEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -460,12 +499,25 @@ console.error('Error is ' + err);
|
||||
startY = coords.y;
|
||||
startWidth = container.offsetWidth;
|
||||
startHeight = container.offsetHeight;
|
||||
|
||||
// Add global listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
document.addEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Handle move
|
||||
function handleMove(e) {
|
||||
// Only handle if we're actually dragging or resizing
|
||||
if (!isDragging && !isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = getCoordinates(e);
|
||||
|
||||
if (isDragging) {
|
||||
@@ -473,6 +525,8 @@ console.error('Error is ' + err);
|
||||
const deltaY = coords.y - startY;
|
||||
container.style.left = (startLeft + deltaX) + 'px';
|
||||
container.style.top = (startTop + deltaY) + 'px';
|
||||
updateChartPosition();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else if (isResizing) {
|
||||
const deltaX = coords.x - startX;
|
||||
@@ -481,17 +535,34 @@ console.error('Error is ' + err);
|
||||
const newHeight = Math.max(100, startHeight + deltaY);
|
||||
container.style.width = newWidth + 'px';
|
||||
container.style.height = newHeight + 'px';
|
||||
updateChartPosition();
|
||||
debouncedUpdateFontSize();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// End drag or resize
|
||||
function endDragOrResize(e) {
|
||||
if (isDragging || isResizing) {
|
||||
const wasDragging = isDragging;
|
||||
const wasResizing = isResizing;
|
||||
|
||||
// Always reset state first
|
||||
isDragging = false;
|
||||
isResizing = false;
|
||||
|
||||
// Always remove listeners to prevent leaks
|
||||
document.removeEventListener('mousemove', handleMove);
|
||||
document.removeEventListener('touchmove', handleMove);
|
||||
document.removeEventListener('mouseup', endDragOrResize);
|
||||
document.removeEventListener('touchend', endDragOrResize);
|
||||
document.removeEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
// Only save state if we were actually dragging/resizing
|
||||
if (wasDragging || wasResizing) {
|
||||
saveState();
|
||||
// If we were resizing, clear pending timeout and update immediately
|
||||
if (isResizing) {
|
||||
if (wasResizing) {
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = null;
|
||||
@@ -499,31 +570,49 @@ console.error('Error is ' + err);
|
||||
updateFontSize();
|
||||
}
|
||||
}
|
||||
isDragging = false;
|
||||
isResizing = false;
|
||||
}
|
||||
|
||||
// Add event listeners for dragging (on container, but not on resize handle)
|
||||
// Check if touch/click is inside container
|
||||
function isInsideContainer(x, y) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
return (
|
||||
x >= rect.left &&
|
||||
x <= rect.right &&
|
||||
y >= rect.top &&
|
||||
y <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Add event listeners for dragging/resizing on container
|
||||
container.addEventListener('mousedown', function(e) {
|
||||
if (e.target !== resizeHandle) startDrag(e);
|
||||
const coords = getCoordinates(e);
|
||||
// Double check we're actually inside the container
|
||||
if (!isInsideContainer(coords.x, coords.y)) {
|
||||
return;
|
||||
}
|
||||
if (isInResizeHandle(coords.x, coords.y)) {
|
||||
startResize(e);
|
||||
} else {
|
||||
startDrag(e);
|
||||
}
|
||||
});
|
||||
container.addEventListener('touchstart', function(e) {
|
||||
if (e.target !== resizeHandle) startDrag(e);
|
||||
});
|
||||
|
||||
// Add event listeners for resizing (on resize handle)
|
||||
resizeHandle.addEventListener('mousedown', startResize);
|
||||
resizeHandle.addEventListener('touchstart', startResize);
|
||||
|
||||
// Add global move and end listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
const coords = getCoordinates(e);
|
||||
// Double check we're actually inside the container
|
||||
if (!isInsideContainer(coords.x, coords.y)) {
|
||||
return;
|
||||
}
|
||||
if (isInResizeHandle(coords.x, coords.y)) {
|
||||
startResize(e);
|
||||
} else {
|
||||
startDrag(e);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Load saved state and set initial font size
|
||||
loadState();
|
||||
updateFontSize();
|
||||
updateChartPosition();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -818,7 +818,8 @@
|
||||
if (field.max !== undefined) input.max = field.max;
|
||||
input.value = value !== undefined ? value : '';
|
||||
}
|
||||
input.addEventListener(field.type === 'duration' || field.type === 'pace' ? 'change' : 'input', handleFieldChange);
|
||||
// Use 'change' event for duration, pace, and number fields to prevent keyboard from closing during typing
|
||||
input.addEventListener(field.type === 'duration' || field.type === 'pace' || field.type === 'number' ? 'change' : 'input', handleFieldChange);
|
||||
|
||||
// Add +/- buttons for duration, number, and pace fields
|
||||
if (field.type === 'duration' || field.type === 'number' || field.type === 'pace') {
|
||||
@@ -921,7 +922,7 @@
|
||||
state.intervals[index][field.syncWith] = speed;
|
||||
}
|
||||
}
|
||||
// Re-render to update both fields
|
||||
// Re-render to update speed field (pace uses 'change' event so keyboard is already closed)
|
||||
renderIntervals();
|
||||
updateChart();
|
||||
updateStatus();
|
||||
@@ -929,7 +930,7 @@
|
||||
} else if (type === 'number') {
|
||||
const raw = target.value;
|
||||
state.intervals[index][key] = raw === '' ? undefined : Number(raw);
|
||||
// If this is a speed field, re-render to update pace
|
||||
// If this is a speed field, re-render to update pace (uses 'change' event so keyboard is already closed)
|
||||
if (key === 'speed') {
|
||||
renderIntervals();
|
||||
}
|
||||
|
||||
@@ -257,77 +257,102 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
// Qt WebChannel integration (if available)
|
||||
if (typeof window.qt !== 'undefined') {
|
||||
window.qt.webChannelTransport = new QWebChannel(qt.webChannelTransport, function(channel) {
|
||||
window.rootItem = channel.objects.rootItem;
|
||||
// WebSocket for communication with Qt
|
||||
let ws = null;
|
||||
let requestTimer = null;
|
||||
let lastDataHash = null; // Track last received data to avoid unnecessary redraws
|
||||
|
||||
// Listen for preview updates
|
||||
if (window.rootItem && window.rootItem.previewWorkoutPointsChanged) {
|
||||
window.rootItem.previewWorkoutPointsChanged.connect(updateFromRootItem);
|
||||
// Send request for workout preview data
|
||||
function requestWorkoutPreview() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
msg: 'getworkoutpreview',
|
||||
content: {}
|
||||
};
|
||||
|
||||
console.log('Requesting workout preview data');
|
||||
ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
// WebSocket setup
|
||||
function setupWebSocket() {
|
||||
const host = (!location.host || location.host.length == 0) ? 'localhost:6666' : location.host;
|
||||
const wsUrl = `ws://${host}/workoutpreview-ws`;
|
||||
|
||||
console.log('Connecting to WebSocket:', wsUrl);
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket connected');
|
||||
|
||||
// Request initial data
|
||||
requestWorkoutPreview();
|
||||
|
||||
// Set up periodic updates (every 500ms to catch changes)
|
||||
if (requestTimer) {
|
||||
clearInterval(requestTimer);
|
||||
}
|
||||
requestTimer = setInterval(requestWorkoutPreview, 500);
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
handleMessage(data);
|
||||
} catch (e) {
|
||||
console.error('Error parsing message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket closed, attempting reconnect in 5 seconds');
|
||||
if (requestTimer) {
|
||||
clearInterval(requestTimer);
|
||||
requestTimer = null;
|
||||
}
|
||||
setTimeout(setupWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
// Handle incoming WebSocket messages
|
||||
function handleMessage(data) {
|
||||
console.log('Received message:', data);
|
||||
|
||||
const msgType = data.type || data.msg;
|
||||
|
||||
if (msgType === 'workoutpreview' || msgType === 'R_workoutpreview') {
|
||||
const content = data.content || data;
|
||||
updateWorkoutFromData(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Update chart from WebSocket data
|
||||
function updateWorkoutFromData(data) {
|
||||
// Create a hash of the data to detect changes
|
||||
const dataHash = JSON.stringify({
|
||||
points: data.points,
|
||||
watts: data.watts,
|
||||
speed: data.speed,
|
||||
inclination: data.inclination,
|
||||
resistance: data.resistance,
|
||||
cadence: data.cadence,
|
||||
deviceType: data.deviceType
|
||||
});
|
||||
}
|
||||
|
||||
// Update chart from rootItem data
|
||||
function updateFromRootItem() {
|
||||
if (!window.rootItem) return;
|
||||
|
||||
// Get miles_unit setting if available
|
||||
if (window.rootItem.miles_unit !== undefined) {
|
||||
miles = window.rootItem.miles_unit ? 0.621371 : 1;
|
||||
// Only update if data has changed
|
||||
if (dataHash === lastDataHash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const points = window.rootItem.preview_workout_points;
|
||||
if (!points || points === 0) return;
|
||||
lastDataHash = dataHash;
|
||||
|
||||
// Build workout data from rootItem arrays
|
||||
const watts = [];
|
||||
const speed = [];
|
||||
const inclination = [];
|
||||
const resistance = [];
|
||||
const cadence = [];
|
||||
|
||||
for (let i = 0; i < points; i++) {
|
||||
const time = i;
|
||||
|
||||
// Get watt value
|
||||
if (window.rootItem.preview_workout_watt && window.rootItem.preview_workout_watt[i] !== undefined) {
|
||||
watts.push({ x: time, y: window.rootItem.preview_workout_watt[i] });
|
||||
}
|
||||
|
||||
// Get speed value
|
||||
if (window.rootItem.preview_workout_speed && window.rootItem.preview_workout_speed[i] !== undefined) {
|
||||
speed.push({ x: time, y: window.rootItem.preview_workout_speed[i] });
|
||||
}
|
||||
|
||||
// Get inclination value
|
||||
if (window.rootItem.preview_workout_inclination && window.rootItem.preview_workout_inclination[i] !== undefined) {
|
||||
inclination.push({ x: time, y: window.rootItem.preview_workout_inclination[i] });
|
||||
}
|
||||
|
||||
// Get resistance value
|
||||
if (window.rootItem.preview_workout_resistance && window.rootItem.preview_workout_resistance[i] !== undefined) {
|
||||
resistance.push({ x: time, y: window.rootItem.preview_workout_resistance[i] });
|
||||
}
|
||||
|
||||
// Get cadence value
|
||||
if (window.rootItem.preview_workout_cadence && window.rootItem.preview_workout_cadence[i] !== undefined) {
|
||||
cadence.push({ x: time, y: window.rootItem.preview_workout_cadence[i] });
|
||||
}
|
||||
}
|
||||
|
||||
// Determine device type (default to bike if we have watts)
|
||||
let deviceType = 'bike';
|
||||
if (speed.length > 0 && watts.length === 0) {
|
||||
deviceType = 'treadmill';
|
||||
}
|
||||
|
||||
updateWorkoutChart({ watts, speed, inclination, resistance, cadence }, deviceType);
|
||||
}
|
||||
|
||||
// External API for QML to call directly
|
||||
window.setWorkoutData = function(data) {
|
||||
const { points, watts, speed, inclination, resistance, cadence, deviceType, miles_unit } = data;
|
||||
|
||||
// Update miles setting if provided
|
||||
@@ -343,8 +368,15 @@
|
||||
cadence: cadence || []
|
||||
};
|
||||
|
||||
console.log('Updating chart with new data');
|
||||
updateWorkoutChart(workoutData, deviceType || 'bike');
|
||||
};
|
||||
}
|
||||
|
||||
// External API for backwards compatibility (if needed)
|
||||
window.setWorkoutData = updateWorkoutFromData;
|
||||
|
||||
// Initialize WebSocket connection
|
||||
setupWebSocket();
|
||||
|
||||
// Initialize with empty chart
|
||||
window.addEventListener('load', function() {
|
||||
|
||||
@@ -38,7 +38,9 @@ class lockscreen {
|
||||
|
||||
// virtualrower
|
||||
void virtualrower_ios();
|
||||
void virtualrower_ios_pm5(bool pm5Mode);
|
||||
void virtualrower_setHeartRate(unsigned char heartRate);
|
||||
void virtualrower_setPM5Mode(bool enabled);
|
||||
bool virtualrower_updateFTMS(unsigned short normalizeSpeed, unsigned char currentResistance,
|
||||
unsigned short currentCadence, unsigned short currentWatt,
|
||||
unsigned short CrankRevolutions, unsigned short LastCrankEventTime,
|
||||
|
||||
@@ -226,6 +226,11 @@ void lockscreen::virtualrower_ios()
|
||||
_virtualrower = [[virtualrower_zwift alloc] init];
|
||||
}
|
||||
|
||||
void lockscreen::virtualrower_ios_pm5(bool pm5Mode)
|
||||
{
|
||||
_virtualrower = [[virtualrower_zwift alloc] initWithPm5Mode:pm5Mode];
|
||||
}
|
||||
|
||||
double lockscreen::virtualbike_getCurrentSlope()
|
||||
{
|
||||
if(_virtualbike_zwift != nil)
|
||||
@@ -288,6 +293,12 @@ void lockscreen::virtualrower_setHeartRate(unsigned char heartRate)
|
||||
[_virtualrower updateHeartRateWithHeartRate:heartRate];
|
||||
}
|
||||
|
||||
void lockscreen::virtualrower_setPM5Mode(bool enabled)
|
||||
{
|
||||
if(_virtualrower != nil)
|
||||
[_virtualrower setPM5ModeWithEnabled:enabled];
|
||||
}
|
||||
|
||||
|
||||
// virtual treadmill
|
||||
void lockscreen::virtualtreadmill_zwift_ios(bool garmin_bluetooth_compatibility, bool bike_cadence_sensor)
|
||||
|
||||
@@ -2,19 +2,51 @@ import CoreBluetooth
|
||||
|
||||
let rowerUuid = CBUUID(string: "0x2AD1");
|
||||
|
||||
// PM5 Concept2 UUIDs
|
||||
let PM5_DISCOVERY_SERVICE_UUID = CBUUID(string: "CE060000-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_DEVICE_INFO_SERVICE_UUID = CBUUID(string: "CE060010-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_CONTROL_SERVICE_UUID = CBUUID(string: "CE060020-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_ROWING_SERVICE_UUID = CBUUID(string: "CE060030-43E5-11E4-916C-0800200C9A66")
|
||||
|
||||
// PM5 Device Info characteristics
|
||||
let PM5_MODEL_UUID = CBUUID(string: "CE060011-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_SERIAL_UUID = CBUUID(string: "CE060012-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_HARDWARE_REV_UUID = CBUUID(string: "CE060013-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_FIRMWARE_REV_UUID = CBUUID(string: "CE060014-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_MANUFACTURER_UUID = CBUUID(string: "CE060015-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_ERG_MACHINE_TYPE_UUID = CBUUID(string: "CE060016-43E5-11E4-916C-0800200C9A66")
|
||||
|
||||
// PM5 Control characteristics
|
||||
let PM5_CONTROL_RECEIVE_UUID = CBUUID(string: "CE060021-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_CONTROL_TRANSMIT_UUID = CBUUID(string: "CE060022-43E5-11E4-916C-0800200C9A66")
|
||||
|
||||
// PM5 Rowing characteristics
|
||||
let PM5_GENERAL_STATUS_UUID = CBUUID(string: "CE060031-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_ADDITIONAL_STATUS_UUID = CBUUID(string: "CE060032-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_ADDITIONAL_STATUS2_UUID = CBUUID(string: "CE060033-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_SAMPLE_RATE_UUID = CBUUID(string: "CE060034-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_STROKE_DATA_UUID = CBUUID(string: "CE060035-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_ADDITIONAL_STROKE_DATA_UUID = CBUUID(string: "CE060036-43E5-11E4-916C-0800200C9A66")
|
||||
let PM5_MULTIPLEXED_INFO_UUID = CBUUID(string: "CE060080-43E5-11E4-916C-0800200C9A66")
|
||||
|
||||
@objc public class virtualrower_zwift: NSObject {
|
||||
private var peripheralManager: rowerBLEPeripheralManagerZwift!
|
||||
|
||||
|
||||
@objc public override init() {
|
||||
super.init()
|
||||
peripheralManager = rowerBLEPeripheralManagerZwift()
|
||||
peripheralManager = rowerBLEPeripheralManagerZwift(pm5Mode: false)
|
||||
}
|
||||
|
||||
|
||||
@objc public init(pm5Mode: Bool) {
|
||||
super.init()
|
||||
peripheralManager = rowerBLEPeripheralManagerZwift(pm5Mode: pm5Mode)
|
||||
}
|
||||
|
||||
@objc public func updateHeartRate(HeartRate: UInt8)
|
||||
{
|
||||
peripheralManager.heartRate = HeartRate
|
||||
}
|
||||
|
||||
|
||||
@objc public func readCurrentSlope() -> Double
|
||||
{
|
||||
return peripheralManager.CurrentSlope;
|
||||
@@ -24,7 +56,12 @@ let rowerUuid = CBUUID(string: "0x2AD1");
|
||||
{
|
||||
return peripheralManager.PowerRequested;
|
||||
}
|
||||
|
||||
|
||||
@objc public func setPM5Mode(enabled: Bool)
|
||||
{
|
||||
peripheralManager.pm5Mode = enabled
|
||||
}
|
||||
|
||||
@objc public func updateFTMS(normalizeSpeed: UInt16, currentCadence: UInt16, currentResistance: UInt8, currentWatt: UInt16, CrankRevolutions: UInt16, LastCrankEventTime: UInt16, StrokesCount: UInt16, Distance: UInt32, KCal: UInt16, Pace: UInt16 ) -> Bool
|
||||
{
|
||||
peripheralManager.NormalizeSpeed = normalizeSpeed
|
||||
@@ -40,7 +77,7 @@ let rowerUuid = CBUUID(string: "0x2AD1");
|
||||
|
||||
return peripheralManager.connected;
|
||||
}
|
||||
|
||||
|
||||
@objc public func getLastFTMSMessage() -> Data? {
|
||||
peripheralManager.LastFTMSMessageReceivedAndPassed = peripheralManager.LastFTMSMessageReceived
|
||||
peripheralManager.LastFTMSMessageReceived?.removeAll()
|
||||
@@ -72,7 +109,7 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
public var Distance: UInt32! = 0
|
||||
public var KCal: UInt16! = 0
|
||||
public var StrokesCount: UInt16! = 0
|
||||
|
||||
|
||||
private var CSCService: CBMutableService!
|
||||
private var CSCFeatureCharacteristic: CBMutableCharacteristic!
|
||||
private var SensorLocationCharacteristic: CBMutableCharacteristic!
|
||||
@@ -80,19 +117,59 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
private var SCControlPointCharacteristic: CBMutableCharacteristic!
|
||||
public var crankRevolutions: UInt16! = 0
|
||||
public var lastCrankEventTime: UInt16! = 0
|
||||
|
||||
|
||||
public var LastFTMSMessageReceived: Data?
|
||||
public var LastFTMSMessageReceivedAndPassed: Data?
|
||||
|
||||
|
||||
public var serviceToggle: UInt8 = 0
|
||||
public var pm5ServiceToggle: UInt8 = 0
|
||||
|
||||
public var connected: Bool = false
|
||||
|
||||
private var notificationTimer: Timer! = nil
|
||||
//var delegate: BLEPeripheralManagerDelegate?
|
||||
|
||||
// PM5 Mode
|
||||
public var pm5Mode: Bool = false
|
||||
private var startTime: Date = Date()
|
||||
private var pm5SampleRate: UInt8 = 0x01
|
||||
|
||||
// PM5 Services
|
||||
private var PM5DeviceInfoService: CBMutableService!
|
||||
private var PM5ControlService: CBMutableService!
|
||||
private var PM5RowingService: CBMutableService!
|
||||
|
||||
// PM5 Device Info Characteristics
|
||||
private var PM5ModelCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5SerialCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5HardwareRevCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5FirmwareRevCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5ManufacturerCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5ErgMachineTypeCharacteristic: CBMutableCharacteristic!
|
||||
|
||||
// PM5 Control Characteristics
|
||||
private var PM5ControlReceiveCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5ControlTransmitCharacteristic: CBMutableCharacteristic!
|
||||
|
||||
// PM5 Rowing Characteristics
|
||||
private var PM5GeneralStatusCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5AdditionalStatusCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5AdditionalStatus2Characteristic: CBMutableCharacteristic!
|
||||
private var PM5SampleRateCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5StrokeDataCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5AdditionalStrokeDataCharacteristic: CBMutableCharacteristic!
|
||||
private var PM5MultiplexedInfoCharacteristic: CBMutableCharacteristic!
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
pm5Mode = false
|
||||
startTime = Date()
|
||||
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
init(pm5Mode: Bool) {
|
||||
super.init()
|
||||
self.pm5Mode = pm5Mode
|
||||
startTime = Date()
|
||||
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
@@ -100,28 +177,42 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
switch peripheral.state {
|
||||
case .poweredOn:
|
||||
print("Peripheral manager is up and running")
|
||||
|
||||
|
||||
|
||||
// Heart Rate Service (always added)
|
||||
self.heartRateService = CBMutableService(type: heartRateServiceUUID, primary: true)
|
||||
let characteristicProperties: CBCharacteristicProperties = [.notify, .read, .write]
|
||||
let characteristicPermissions: CBAttributePermissions = [.readable]
|
||||
self.heartRateCharacteristic = CBMutableCharacteristic(type: heartRateCharacteristicUUID,
|
||||
self.heartRateCharacteristic = CBMutableCharacteristic(type: heartRateCharacteristicUUID,
|
||||
properties: characteristicProperties,
|
||||
value: nil,
|
||||
permissions: characteristicPermissions)
|
||||
|
||||
|
||||
heartRateService.characteristics = [heartRateCharacteristic]
|
||||
self.peripheralManager.add(heartRateService)
|
||||
|
||||
if pm5Mode {
|
||||
// PM5 Mode - Setup PM5 Services
|
||||
setupPM5Services()
|
||||
} else {
|
||||
// FTMS Mode - Original implementation
|
||||
setupFTMSServices()
|
||||
}
|
||||
|
||||
default:
|
||||
print("Peripheral manager is down")
|
||||
}
|
||||
}
|
||||
|
||||
func setupFTMSServices() {
|
||||
self.FitnessMachineService = CBMutableService(type: FitnessMachineServiceUuid, primary: true)
|
||||
|
||||
let FitnessMachineFeatureProperties: CBCharacteristicProperties = [.read]
|
||||
let FitnessMachineFeaturePermissions: CBAttributePermissions = [.readable]
|
||||
self.FitnessMachineFeatureCharacteristic = CBMutableCharacteristic(type: FitnessMachineFeatureCharacteristicUuid,
|
||||
properties: FitnessMachineFeatureProperties,
|
||||
value: Data (bytes: [0x83, 0x14, 0x00, 0x00, 0x0c, 0xe0, 0x00, 0x00]),
|
||||
value: Data (bytes: [0x83, 0x14, 0x00, 0x00, 0x0c, 0xe0, 0x00, 0x00]),
|
||||
permissions: FitnessMachineFeaturePermissions)
|
||||
|
||||
|
||||
let supported_resistance_level_rangeProperties: CBCharacteristicProperties = [.read]
|
||||
let supported_resistance_level_rangePermissions: CBAttributePermissions = [.readable]
|
||||
self.supported_resistance_level_rangeCharacteristic = CBMutableCharacteristic(type: supported_resistance_level_rangeCharacteristicUuid,
|
||||
@@ -142,14 +233,14 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
properties: rowerProperties,
|
||||
value: nil,
|
||||
permissions: rowerPermissions)
|
||||
|
||||
|
||||
let FitnessMachinestatusProperties: CBCharacteristicProperties = [.notify]
|
||||
let FitnessMachinestatusPermissions: CBAttributePermissions = [.readable]
|
||||
self.FitnessMachinestatusCharacteristic = CBMutableCharacteristic(type: FitnessMachinestatusUuid,
|
||||
properties: FitnessMachinestatusProperties,
|
||||
value: nil,
|
||||
permissions: FitnessMachinestatusPermissions)
|
||||
|
||||
|
||||
let TrainingStatusProperties: CBCharacteristicProperties = [.read]
|
||||
let TrainingStatusPermissions: CBAttributePermissions = [.readable]
|
||||
self.TrainingStatusCharacteristic = CBMutableCharacteristic(type: TrainingStatusUuid,
|
||||
@@ -163,9 +254,9 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
rowerCharacteristic,
|
||||
FitnessMachinestatusCharacteristic,
|
||||
TrainingStatusCharacteristic ]
|
||||
|
||||
|
||||
self.peripheralManager.add(FitnessMachineService)
|
||||
|
||||
|
||||
self.CSCService = CBMutableService(type: CSCServiceUUID, primary: true)
|
||||
|
||||
let CSCFeatureProperties: CBCharacteristicProperties = [.read]
|
||||
@@ -201,10 +292,108 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
CSCMeasurementCharacteristic,
|
||||
SCControlPointCharacteristic]
|
||||
self.peripheralManager.add(CSCService)
|
||||
}
|
||||
|
||||
default:
|
||||
print("Peripheral manager is down")
|
||||
}
|
||||
func setupPM5Services() {
|
||||
print("Setting up PM5 services")
|
||||
|
||||
// PM5 Device Info Service (CE060010)
|
||||
self.PM5DeviceInfoService = CBMutableService(type: PM5_DEVICE_INFO_SERVICE_UUID, primary: true)
|
||||
|
||||
self.PM5ModelCharacteristic = CBMutableCharacteristic(type: PM5_MODEL_UUID,
|
||||
properties: [.read],
|
||||
value: "PM5".data(using: .utf8),
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5SerialCharacteristic = CBMutableCharacteristic(type: PM5_SERIAL_UUID,
|
||||
properties: [.read],
|
||||
value: "430000000".data(using: .utf8),
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5HardwareRevCharacteristic = CBMutableCharacteristic(type: PM5_HARDWARE_REV_UUID,
|
||||
properties: [.read],
|
||||
value: "802".data(using: .utf8),
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5FirmwareRevCharacteristic = CBMutableCharacteristic(type: PM5_FIRMWARE_REV_UUID,
|
||||
properties: [.read],
|
||||
value: "2.18".data(using: .utf8),
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5ManufacturerCharacteristic = CBMutableCharacteristic(type: PM5_MANUFACTURER_UUID,
|
||||
properties: [.read],
|
||||
value: "Concept2".data(using: .utf8),
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5ErgMachineTypeCharacteristic = CBMutableCharacteristic(type: PM5_ERG_MACHINE_TYPE_UUID,
|
||||
properties: [.read],
|
||||
value: Data([0x00]), // 0 = Rower
|
||||
permissions: [.readable])
|
||||
|
||||
PM5DeviceInfoService.characteristics = [PM5ModelCharacteristic, PM5SerialCharacteristic,
|
||||
PM5HardwareRevCharacteristic, PM5FirmwareRevCharacteristic,
|
||||
PM5ManufacturerCharacteristic, PM5ErgMachineTypeCharacteristic]
|
||||
self.peripheralManager.add(PM5DeviceInfoService)
|
||||
|
||||
// PM5 Control Service (CE060020)
|
||||
self.PM5ControlService = CBMutableService(type: PM5_CONTROL_SERVICE_UUID, primary: true)
|
||||
|
||||
self.PM5ControlReceiveCharacteristic = CBMutableCharacteristic(type: PM5_CONTROL_RECEIVE_UUID,
|
||||
properties: [.write, .writeWithoutResponse],
|
||||
value: nil,
|
||||
permissions: [.writeable])
|
||||
|
||||
self.PM5ControlTransmitCharacteristic = CBMutableCharacteristic(type: PM5_CONTROL_TRANSMIT_UUID,
|
||||
properties: [.indicate],
|
||||
value: nil,
|
||||
permissions: [.readable])
|
||||
|
||||
PM5ControlService.characteristics = [PM5ControlReceiveCharacteristic, PM5ControlTransmitCharacteristic]
|
||||
self.peripheralManager.add(PM5ControlService)
|
||||
|
||||
// PM5 Rowing Service (CE060030)
|
||||
self.PM5RowingService = CBMutableService(type: PM5_ROWING_SERVICE_UUID, primary: true)
|
||||
|
||||
self.PM5GeneralStatusCharacteristic = CBMutableCharacteristic(type: PM5_GENERAL_STATUS_UUID,
|
||||
properties: [.notify],
|
||||
value: nil,
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5AdditionalStatusCharacteristic = CBMutableCharacteristic(type: PM5_ADDITIONAL_STATUS_UUID,
|
||||
properties: [.notify],
|
||||
value: nil,
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5AdditionalStatus2Characteristic = CBMutableCharacteristic(type: PM5_ADDITIONAL_STATUS2_UUID,
|
||||
properties: [.notify],
|
||||
value: nil,
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5SampleRateCharacteristic = CBMutableCharacteristic(type: PM5_SAMPLE_RATE_UUID,
|
||||
properties: [.read, .write],
|
||||
value: nil,
|
||||
permissions: [.readable, .writeable])
|
||||
|
||||
self.PM5StrokeDataCharacteristic = CBMutableCharacteristic(type: PM5_STROKE_DATA_UUID,
|
||||
properties: [.notify],
|
||||
value: nil,
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5AdditionalStrokeDataCharacteristic = CBMutableCharacteristic(type: PM5_ADDITIONAL_STROKE_DATA_UUID,
|
||||
properties: [.notify],
|
||||
value: nil,
|
||||
permissions: [.readable])
|
||||
|
||||
self.PM5MultiplexedInfoCharacteristic = CBMutableCharacteristic(type: PM5_MULTIPLEXED_INFO_UUID,
|
||||
properties: [.notify],
|
||||
value: nil,
|
||||
permissions: [.readable])
|
||||
|
||||
PM5RowingService.characteristics = [PM5GeneralStatusCharacteristic, PM5AdditionalStatusCharacteristic,
|
||||
PM5AdditionalStatus2Characteristic, PM5SampleRateCharacteristic,
|
||||
PM5StrokeDataCharacteristic, PM5AdditionalStrokeDataCharacteristic,
|
||||
PM5MultiplexedInfoCharacteristic]
|
||||
self.peripheralManager.add(PM5RowingService)
|
||||
}
|
||||
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
||||
@@ -212,10 +401,19 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
print("Failed to add service with error: \(uwError.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
let advertisementData = [CBAdvertisementDataLocalNameKey: "QZ",
|
||||
CBAdvertisementDataServiceUUIDsKey: [heartRateServiceUUID, FitnessMachineServiceUuid, CSCServiceUUID]] as [String : Any]
|
||||
|
||||
|
||||
var advertisementData: [String: Any]
|
||||
|
||||
if pm5Mode {
|
||||
// PM5 advertising - device name + PM5 discovery service UUID
|
||||
advertisementData = [CBAdvertisementDataLocalNameKey: "PM5 430000000",
|
||||
CBAdvertisementDataServiceUUIDsKey: [PM5_DISCOVERY_SERVICE_UUID]] as [String : Any]
|
||||
} else {
|
||||
// FTMS advertising
|
||||
advertisementData = [CBAdvertisementDataLocalNameKey: "QZ",
|
||||
CBAdvertisementDataServiceUUIDsKey: [heartRateServiceUUID, FitnessMachineServiceUuid, CSCServiceUUID]] as [String : Any]
|
||||
}
|
||||
|
||||
peripheralManager.startAdvertising(advertisementData)
|
||||
print("Successfully added service")
|
||||
}
|
||||
@@ -232,6 +430,14 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
}
|
||||
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
||||
if requests.first!.characteristic == self.PM5SampleRateCharacteristic {
|
||||
if let value = requests.first!.value, value.count > 0 {
|
||||
self.pm5SampleRate = value[0]
|
||||
print("PM5 sample rate set to \(self.pm5SampleRate)")
|
||||
}
|
||||
self.peripheralManager.respond(to: requests.first!, withResult: .success)
|
||||
return
|
||||
}
|
||||
if requests.first!.characteristic == self.FitnessMachineControlPointCharacteristic {
|
||||
if(LastFTMSMessageReceived == nil || LastFTMSMessageReceived?.count == 0) {
|
||||
LastFTMSMessageReceived = requests.first!.value
|
||||
@@ -274,6 +480,11 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
self.peripheralManager.respond(to: request, withResult: .success)
|
||||
print("Responded successfully to a read request")
|
||||
}
|
||||
else if request.characteristic == self.PM5SampleRateCharacteristic {
|
||||
request.value = Data([self.pm5SampleRate])
|
||||
self.peripheralManager.respond(to: request, withResult: .success)
|
||||
print("Responded successfully to PM5 sample rate read request")
|
||||
}
|
||||
}
|
||||
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
||||
@@ -326,10 +537,18 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
}
|
||||
|
||||
@objc func updateSubscribers() {
|
||||
if pm5Mode {
|
||||
updatePM5Subscribers()
|
||||
} else {
|
||||
updateFTMSSubscribers()
|
||||
}
|
||||
}
|
||||
|
||||
func updateFTMSSubscribers() {
|
||||
let heartRateData = self.calculateHeartRate()
|
||||
let rowerData = self.calculateRower()
|
||||
let cadenceData = self.calculateCadence()
|
||||
|
||||
|
||||
if(self.serviceToggle == 2)
|
||||
{
|
||||
let ok = self.peripheralManager.updateValue(cadenceData, for: self.CSCMeasurementCharacteristic, onSubscribedCentrals: nil)
|
||||
@@ -352,5 +571,337 @@ class rowerBLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updatePM5Subscribers() {
|
||||
guard PM5GeneralStatusCharacteristic != nil else { return }
|
||||
|
||||
var ok = false
|
||||
|
||||
switch pm5ServiceToggle {
|
||||
case 0:
|
||||
// Send General Status
|
||||
let generalStatus = buildPM5GeneralStatus()
|
||||
ok = self.peripheralManager.updateValue(generalStatus, for: self.PM5GeneralStatusCharacteristic, onSubscribedCentrals: nil)
|
||||
if ok && PM5MultiplexedInfoCharacteristic != nil {
|
||||
var muxGeneralStatus = Data([0x31])
|
||||
muxGeneralStatus.append(generalStatus)
|
||||
_ = self.peripheralManager.updateValue(muxGeneralStatus, for: self.PM5MultiplexedInfoCharacteristic, onSubscribedCentrals: nil)
|
||||
}
|
||||
|
||||
case 1:
|
||||
// Send Additional Status
|
||||
let additionalStatus = buildPM5AdditionalStatus()
|
||||
ok = self.peripheralManager.updateValue(additionalStatus, for: self.PM5AdditionalStatusCharacteristic, onSubscribedCentrals: nil)
|
||||
if ok && PM5MultiplexedInfoCharacteristic != nil {
|
||||
var muxAdditionalStatus = Data([0x32])
|
||||
muxAdditionalStatus.append(additionalStatus)
|
||||
_ = self.peripheralManager.updateValue(muxAdditionalStatus, for: self.PM5MultiplexedInfoCharacteristic, onSubscribedCentrals: nil)
|
||||
}
|
||||
|
||||
case 2:
|
||||
// Send Additional Status 2
|
||||
let additionalStatus2 = buildPM5AdditionalStatus2()
|
||||
ok = self.peripheralManager.updateValue(additionalStatus2, for: self.PM5AdditionalStatus2Characteristic, onSubscribedCentrals: nil)
|
||||
if ok && PM5MultiplexedInfoCharacteristic != nil {
|
||||
var muxAdditionalStatus2 = Data([0x33])
|
||||
muxAdditionalStatus2.append(additionalStatus2)
|
||||
_ = self.peripheralManager.updateValue(muxAdditionalStatus2, for: self.PM5MultiplexedInfoCharacteristic, onSubscribedCentrals: nil)
|
||||
}
|
||||
|
||||
case 3:
|
||||
// Send Stroke Data
|
||||
let strokeData = buildPM5StrokeData()
|
||||
ok = self.peripheralManager.updateValue(strokeData, for: self.PM5StrokeDataCharacteristic, onSubscribedCentrals: nil)
|
||||
if ok && PM5MultiplexedInfoCharacteristic != nil {
|
||||
var muxStrokeData = Data([0x35])
|
||||
muxStrokeData.append(strokeData)
|
||||
_ = self.peripheralManager.updateValue(muxStrokeData, for: self.PM5MultiplexedInfoCharacteristic, onSubscribedCentrals: nil)
|
||||
}
|
||||
|
||||
case 4:
|
||||
// Send Additional Stroke Data
|
||||
let additionalStrokeData = buildPM5AdditionalStrokeData()
|
||||
ok = self.peripheralManager.updateValue(additionalStrokeData, for: self.PM5AdditionalStrokeDataCharacteristic, onSubscribedCentrals: nil)
|
||||
if ok && PM5MultiplexedInfoCharacteristic != nil {
|
||||
var muxAdditionalStrokeData = Data([0x36])
|
||||
muxAdditionalStrokeData.append(additionalStrokeData)
|
||||
_ = self.peripheralManager.updateValue(muxAdditionalStrokeData, for: self.PM5MultiplexedInfoCharacteristic, onSubscribedCentrals: nil)
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Advance to next characteristic if update was successful
|
||||
if ok {
|
||||
pm5ServiceToggle += 1
|
||||
if pm5ServiceToggle > 4 {
|
||||
pm5ServiceToggle = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getElapsedCentiseconds() -> UInt32 {
|
||||
let elapsed = Date().timeIntervalSince(startTime)
|
||||
return UInt32(elapsed * 100)
|
||||
}
|
||||
|
||||
func buildPM5GeneralStatus() -> Data {
|
||||
// 19 bytes
|
||||
var value = Data(count: 19)
|
||||
let elapsed = getElapsedCentiseconds()
|
||||
|
||||
// Elapsed time (24-bit LE)
|
||||
value[0] = UInt8(elapsed & 0xFF)
|
||||
value[1] = UInt8((elapsed >> 8) & 0xFF)
|
||||
value[2] = UInt8((elapsed >> 16) & 0xFF)
|
||||
|
||||
// Distance in 0.1m units (24-bit LE)
|
||||
let distanceDecimeters = UInt32(Double(Distance) / 100.0) // Distance is in mm, convert to 0.1m
|
||||
value[3] = UInt8(distanceDecimeters & 0xFF)
|
||||
value[4] = UInt8((distanceDecimeters >> 8) & 0xFF)
|
||||
value[5] = UInt8((distanceDecimeters >> 16) & 0xFF)
|
||||
|
||||
// Workout Type - 0 = Just Row Free
|
||||
value[6] = 0x00
|
||||
// Interval Type - 0 = None
|
||||
value[7] = 0x00
|
||||
// Workout State - 1 = Working
|
||||
value[8] = (CurrentWatt > 0 || CurrentCadence > 0) ? 0x01 : 0x00
|
||||
// Rowing State - 1 = Active
|
||||
value[9] = (CurrentWatt > 0 || CurrentCadence > 0) ? 0x01 : 0x00
|
||||
// Stroke State - 1 = Driving
|
||||
value[10] = (CurrentWatt > 0) ? 0x01 : 0x00
|
||||
|
||||
// Total Work Distance in meters (24-bit LE)
|
||||
let totalDistanceMeters = UInt32(Double(Distance) / 1000.0)
|
||||
value[11] = UInt8(totalDistanceMeters & 0xFF)
|
||||
value[12] = UInt8((totalDistanceMeters >> 8) & 0xFF)
|
||||
value[13] = UInt8((totalDistanceMeters >> 16) & 0xFF)
|
||||
|
||||
// Workout Duration (target) - 0
|
||||
value[14] = 0x00
|
||||
value[15] = 0x00
|
||||
value[16] = 0x00
|
||||
value[17] = 0x00 // Duration type
|
||||
value[18] = 110 // Drag factor
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func buildPM5AdditionalStatus() -> Data {
|
||||
// 19 bytes
|
||||
var value = Data(count: 19)
|
||||
let elapsed = getElapsedCentiseconds()
|
||||
|
||||
// Elapsed time (24-bit LE)
|
||||
value[0] = UInt8(elapsed & 0xFF)
|
||||
value[1] = UInt8((elapsed >> 8) & 0xFF)
|
||||
value[2] = UInt8((elapsed >> 16) & 0xFF)
|
||||
|
||||
// Speed in 0.001 m/s units (16-bit LE)
|
||||
let speedMmPerSec = UInt16(Double(NormalizeSpeed) * 1000.0 / 3600.0)
|
||||
value[3] = UInt8(speedMmPerSec & 0xFF)
|
||||
value[4] = UInt8((speedMmPerSec >> 8) & 0xFF)
|
||||
|
||||
// Stroke Rate (SPM)
|
||||
value[5] = UInt8(CurrentCadence & 0xFF)
|
||||
|
||||
// Heart Rate
|
||||
value[6] = heartRate
|
||||
|
||||
// Current Pace in 0.01 sec/500m (16-bit LE)
|
||||
value[7] = UInt8(Pace & 0xFF)
|
||||
value[8] = UInt8((Pace >> 8) & 0xFF)
|
||||
|
||||
// Average Pace (same as current)
|
||||
value[9] = value[7]
|
||||
value[10] = value[8]
|
||||
|
||||
// Rest Distance - 0
|
||||
value[11] = 0x00
|
||||
value[12] = 0x00
|
||||
|
||||
// Rest Time - 0
|
||||
value[13] = 0x00
|
||||
value[14] = 0x00
|
||||
value[15] = 0x00
|
||||
|
||||
// Average Power (16-bit LE)
|
||||
value[16] = UInt8(CurrentWatt & 0xFF)
|
||||
value[17] = UInt8((CurrentWatt >> 8) & 0xFF)
|
||||
|
||||
// Erg Machine Type - 0 = Rower
|
||||
value[18] = 0x00
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func buildPM5AdditionalStatus2() -> Data {
|
||||
// 20 bytes
|
||||
var value = Data(count: 20)
|
||||
let elapsed = getElapsedCentiseconds()
|
||||
|
||||
// Elapsed time (24-bit LE)
|
||||
value[0] = UInt8(elapsed & 0xFF)
|
||||
value[1] = UInt8((elapsed >> 8) & 0xFF)
|
||||
value[2] = UInt8((elapsed >> 16) & 0xFF)
|
||||
|
||||
// Interval Count
|
||||
value[3] = 0x01
|
||||
|
||||
// Average Power (16-bit LE)
|
||||
value[4] = UInt8(CurrentWatt & 0xFF)
|
||||
value[5] = UInt8((CurrentWatt >> 8) & 0xFF)
|
||||
|
||||
// Total Calories (16-bit LE)
|
||||
value[6] = UInt8(KCal & 0xFF)
|
||||
value[7] = UInt8((KCal >> 8) & 0xFF)
|
||||
|
||||
// Split Average Pace (16-bit LE)
|
||||
value[8] = UInt8(Pace & 0xFF)
|
||||
value[9] = UInt8((Pace >> 8) & 0xFF)
|
||||
|
||||
// Split Average Power (16-bit LE)
|
||||
value[10] = value[4]
|
||||
value[11] = value[5]
|
||||
|
||||
// Split Average Calories (kCal/hour) - 0
|
||||
value[12] = 0x00
|
||||
value[13] = 0x00
|
||||
|
||||
// Last Split Time - 0
|
||||
value[14] = 0x00
|
||||
value[15] = 0x00
|
||||
value[16] = 0x00
|
||||
|
||||
// Last Split Distance - 0
|
||||
value[17] = 0x00
|
||||
value[18] = 0x00
|
||||
value[19] = 0x00
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func buildPM5StrokeData() -> Data {
|
||||
// 20 bytes
|
||||
var value = Data(count: 20)
|
||||
let elapsed = getElapsedCentiseconds()
|
||||
|
||||
// Elapsed time (24-bit LE)
|
||||
value[0] = UInt8(elapsed & 0xFF)
|
||||
value[1] = UInt8((elapsed >> 8) & 0xFF)
|
||||
value[2] = UInt8((elapsed >> 16) & 0xFF)
|
||||
|
||||
// Distance in 0.1m units (24-bit LE)
|
||||
let distanceDecimeters = UInt32(Double(Distance) / 100.0)
|
||||
value[3] = UInt8(distanceDecimeters & 0xFF)
|
||||
value[4] = UInt8((distanceDecimeters >> 8) & 0xFF)
|
||||
value[5] = UInt8((distanceDecimeters >> 16) & 0xFF)
|
||||
|
||||
// Drive Length (0.01m) - typical 1.4m
|
||||
value[6] = 140
|
||||
// Drive Time (0.01s) - typical 0.8s
|
||||
value[7] = 80
|
||||
|
||||
// Stroke Recovery Time (16-bit LE, 0.01s)
|
||||
var recoveryTime: UInt16 = 170 // default 1.7s
|
||||
if CurrentCadence > 0 {
|
||||
let strokeTime = 60.0 / Double(CurrentCadence)
|
||||
let recoveryTimeDouble = max(0.5, strokeTime - 0.8) * 100.0
|
||||
recoveryTime = UInt16(recoveryTimeDouble)
|
||||
if recoveryTime < 50 { recoveryTime = 50 }
|
||||
}
|
||||
value[8] = UInt8(recoveryTime & 0xFF)
|
||||
value[9] = UInt8((recoveryTime >> 8) & 0xFF)
|
||||
|
||||
// Stroke Distance (16-bit LE, 0.01m)
|
||||
var strokeDistance: UInt16 = 1000 // default 10m
|
||||
if CurrentCadence > 0 && NormalizeSpeed > 0 {
|
||||
let speedMs = Double(NormalizeSpeed) / 3.6
|
||||
let strokeTime = 60.0 / Double(CurrentCadence)
|
||||
strokeDistance = UInt16(speedMs * strokeTime * 100.0)
|
||||
}
|
||||
value[10] = UInt8(strokeDistance & 0xFF)
|
||||
value[11] = UInt8((strokeDistance >> 8) & 0xFF)
|
||||
|
||||
// Peak Drive Force (16-bit LE, 0.1 lbs) - estimate from power
|
||||
var peakForce: UInt16 = 0
|
||||
if CurrentWatt > 0 && NormalizeSpeed > 0 {
|
||||
let speedMs = Double(NormalizeSpeed) / 3.6
|
||||
let forceN = Double(CurrentWatt) / speedMs
|
||||
peakForce = UInt16(forceN * 0.2248 * 10.0 * 1.5)
|
||||
}
|
||||
value[12] = UInt8(peakForce & 0xFF)
|
||||
value[13] = UInt8((peakForce >> 8) & 0xFF)
|
||||
|
||||
// Average Drive Force (16-bit LE, 0.1 lbs)
|
||||
let avgForce = peakForce / 2
|
||||
value[14] = UInt8(avgForce & 0xFF)
|
||||
value[15] = UInt8((avgForce >> 8) & 0xFF)
|
||||
|
||||
// Work Per Stroke (16-bit LE, Joules)
|
||||
var workPerStroke: UInt16 = 0
|
||||
if CurrentCadence > 0 && CurrentWatt > 0 {
|
||||
let strokeTime = 60.0 / Double(CurrentCadence)
|
||||
workPerStroke = UInt16(Double(CurrentWatt) * strokeTime)
|
||||
}
|
||||
value[16] = UInt8(workPerStroke & 0xFF)
|
||||
value[17] = UInt8((workPerStroke >> 8) & 0xFF)
|
||||
|
||||
// Stroke Count (16-bit LE)
|
||||
value[18] = UInt8(StrokesCount & 0xFF)
|
||||
value[19] = UInt8((StrokesCount >> 8) & 0xFF)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func buildPM5AdditionalStrokeData() -> Data {
|
||||
// 17 bytes
|
||||
var value = Data(count: 17)
|
||||
let elapsed = getElapsedCentiseconds()
|
||||
|
||||
// Elapsed time (24-bit LE)
|
||||
value[0] = UInt8(elapsed & 0xFF)
|
||||
value[1] = UInt8((elapsed >> 8) & 0xFF)
|
||||
value[2] = UInt8((elapsed >> 16) & 0xFF)
|
||||
|
||||
// Stroke Power (16-bit LE, watts)
|
||||
value[3] = UInt8(CurrentWatt & 0xFF)
|
||||
value[4] = UInt8((CurrentWatt >> 8) & 0xFF)
|
||||
|
||||
// Stroke Calories (16-bit LE)
|
||||
var strokeCalories: UInt16 = 0
|
||||
if CurrentCadence > 0 && StrokesCount > 0 {
|
||||
strokeCalories = UInt16(Double(KCal) * 1000.0 / Double(StrokesCount))
|
||||
}
|
||||
value[5] = UInt8(strokeCalories & 0xFF)
|
||||
value[6] = UInt8((strokeCalories >> 8) & 0xFF)
|
||||
|
||||
// Stroke Count (16-bit LE)
|
||||
value[7] = UInt8(StrokesCount & 0xFF)
|
||||
value[8] = UInt8((StrokesCount >> 8) & 0xFF)
|
||||
|
||||
// Projected Work Time - 0
|
||||
value[9] = 0x00
|
||||
value[10] = 0x00
|
||||
value[11] = 0x00
|
||||
|
||||
// Projected Work Distance - 0
|
||||
value[12] = 0x00
|
||||
value[13] = 0x00
|
||||
value[14] = 0x00
|
||||
|
||||
// Work Per Stroke (16-bit LE, Joules)
|
||||
var workPerStroke: UInt16 = 0
|
||||
if CurrentCadence > 0 && CurrentWatt > 0 {
|
||||
let strokeTime = 60.0 / Double(CurrentCadence)
|
||||
workPerStroke = UInt16(Double(CurrentWatt) * strokeTime)
|
||||
}
|
||||
value[15] = UInt8(workPerStroke & 0xFF)
|
||||
value[16] = UInt8((workPerStroke >> 8) & 0xFF)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
} /// class-end
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "mqttpublisher.h"
|
||||
#include "androidstatusbar.h"
|
||||
#include "fontmanager.h"
|
||||
#include "filesearcher.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
@@ -657,7 +658,7 @@ int main(int argc, char *argv[]) {
|
||||
qInstallMessageHandler(myMessageOutput);
|
||||
qDebug() << QStringLiteral("version ") << app->applicationVersion();
|
||||
foreach (QString s, settings.allKeys()) {
|
||||
if (!s.contains(QStringLiteral("password")) && !s.contains("user_email") && !s.contains("username") && !s.contains("token")) {
|
||||
if (!s.contains(QStringLiteral("password")) && !s.contains("user_email") && !s.contains("username") && !s.contains("token") && !s.contains("garmin_device_serial") && !s.contains("garmin_email")) {
|
||||
|
||||
qDebug() << s << settings.value(s);
|
||||
}
|
||||
@@ -879,6 +880,10 @@ int main(int argc, char *argv[]) {
|
||||
#ifdef Q_OS_ANDROID
|
||||
engine.rootContext()->setContextProperty("fontManager", &fontManager);
|
||||
#endif
|
||||
// Expose FileSearcher for fast recursive file searching
|
||||
FileSearcher fileSearcher;
|
||||
engine.rootContext()->setContextProperty("fileSearcher", &fileSearcher);
|
||||
|
||||
engine.load(url);
|
||||
homeform *h = new homeform(&engine, &bl);
|
||||
QObject::connect(app.data(), &QCoreApplication::aboutToQuit, h,
|
||||
|
||||
@@ -935,7 +935,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
text: "version 2.20.23"
|
||||
text: "version 2.20.26"
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
|
||||
@@ -186,8 +186,9 @@ void MainWindow::update() {
|
||||
verticalOscillation, stepCount,
|
||||
target_cadence, target_watt, target_resistance, target_inclination, target_speed,
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(),
|
||||
bluetoothManager->device()->HeatStrainIndex.value() // TODO add lap
|
||||
);
|
||||
bluetoothManager->device()->HeatStrainIndex.value(),
|
||||
bluetoothManager->device()->currentHRV().value(),
|
||||
bluetoothManager->device()->getRRIntervalsAndClear());
|
||||
|
||||
Session.append(s);
|
||||
|
||||
|
||||
@@ -101,16 +101,20 @@ SOURCES += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.cpp \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.cpp \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.cpp \
|
||||
$$PWD/devices/sportstechrower/sportstechrower.cpp \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.cpp \
|
||||
$$PWD/devices/thinkridercontroller/thinkridercontroller.cpp \
|
||||
$$PWD/devices/stairclimber.cpp \
|
||||
$$PWD/devices/echelonstairclimber/echelonstairclimber.cpp \
|
||||
$$PWD/devices/sunnyfitstepper/sunnyfitstepper.cpp \
|
||||
$$PWD/devices/technogymbike/technogymbike.cpp \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.cpp \
|
||||
$$PWD/fitdatabaseprocessor.cpp \
|
||||
$$PWD/devices/trxappgateusbrower/trxappgateusbrower.cpp \
|
||||
$$PWD/logwriter.cpp \
|
||||
$$PWD/fitbackupwriter.cpp \
|
||||
$$PWD/filesearcher.cpp \
|
||||
$$PWD/mqtt/qmqttauthenticationproperties.cpp \
|
||||
$$PWD/mqtt/qmqttclient.cpp \
|
||||
$$PWD/mqtt/qmqttconnection.cpp \
|
||||
@@ -366,6 +370,7 @@ HEADERS += \
|
||||
$$PWD/devices/cycleopsphantombike/cycleopsphantombike.h \
|
||||
$$PWD/devices/deeruntreadmill/deerruntreadmill.h \
|
||||
$$PWD/devices/echelonstairclimber/echelonstairclimber.h \
|
||||
$$PWD/devices/sunnyfitstepper/sunnyfitstepper.h \
|
||||
$$PWD/devices/elitesquarecontroller/elitesquarecontroller.h \
|
||||
$$PWD/devices/focustreadmill/focustreadmill.h \
|
||||
$$PWD/devices/jumprope.h \
|
||||
@@ -378,8 +383,10 @@ HEADERS += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.h \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.h \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.h \
|
||||
$$PWD/devices/sportstechrower/sportstechrower.h \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.h \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.h \
|
||||
$$PWD/devices/thinkridercontroller/thinkridercontroller.h \
|
||||
$$PWD/devices/stairclimber.h \
|
||||
$$PWD/devices/technogymbike/technogymbike.h \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.h \
|
||||
@@ -389,6 +396,7 @@ HEADERS += \
|
||||
$$PWD/inclinationresistancetable.h \
|
||||
$$PWD/logwriter.h \
|
||||
$$PWD/fitbackupwriter.h \
|
||||
$$PWD/filesearcher.h \
|
||||
$$PWD/osc.h \
|
||||
$$PWD/oscpp/client.hpp \
|
||||
$$PWD/oscpp/detail/endian.hpp \
|
||||
@@ -770,6 +778,7 @@ fit-sdk/fit_zones_target_mesg.hpp \
|
||||
fit-sdk/fit_zones_target_mesg_listener.hpp \
|
||||
devices/flywheelbike/flywheelbike.h \
|
||||
devices/ftmsbike/ftmsbike.h \
|
||||
devices/ftmsbike/speedracex_defaults.h \
|
||||
devices/heartratebelt/heartratebelt.h \
|
||||
homeform.h \
|
||||
garminconnect.h \
|
||||
@@ -985,6 +994,9 @@ ios {
|
||||
|
||||
TARGET = qdomyoszwift
|
||||
QMAKE_TARGET_BUNDLE_PREFIX = org.cagnulein
|
||||
|
||||
# iOS Code Signing Configuration - handled manually in Xcode project
|
||||
|
||||
DEFINES+=_Nullable_result=_Nullable NS_FORMAT_ARGUMENT\\(A\\)=
|
||||
}
|
||||
|
||||
@@ -1004,4 +1016,4 @@ INCLUDEPATH += purchasing/inapp
|
||||
|
||||
WINRT_MANIFEST = AppxManifest.xml
|
||||
|
||||
VERSION = 2.20.23
|
||||
VERSION = 2.20.26
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
include(qdomyos-zwift.pri)
|
||||
|
||||
QMAKE_IOS_DEPLOYMENT_TARGET = 12.0
|
||||
QMAKE_DEVELOPMENT_TEAM = 6335M7T29D
|
||||
QMAKE_CODE_SIGN_IDENTITY = "iPhone Developer"
|
||||
QMAKE_CODE_SIGN_STYLE = Automatic
|
||||
23
src/qfit.cpp
23
src/qfit.cpp
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "fit_date_time.hpp"
|
||||
#include "fit_encode.hpp"
|
||||
#include "fit_hrv_mesg.hpp"
|
||||
|
||||
#include "fit_decode.hpp"
|
||||
#include "fit_developer_field_description.hpp"
|
||||
@@ -389,6 +390,9 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
sessionMesg.SetTotalMovingTime(session.last().elapsedTime);
|
||||
sessionMesg.SetTotalAscent(session.last().elevationGain); // Total elevation gain (meters)
|
||||
sessionMesg.SetTotalDescent(session.last().negativeElevationGain); // Total elevation loss/descent (meters)
|
||||
if (speed_avg > 0) {
|
||||
sessionMesg.SetAvgSpeed(speed_avg / 3.6); // Convert from km/h to m/s
|
||||
}
|
||||
sessionMesg.SetMinAltitude(min_alt);
|
||||
sessionMesg.SetMaxAltitude(max_alt);
|
||||
sessionMesg.SetEvent(FIT_EVENT_SESSION);
|
||||
@@ -454,7 +458,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
sessionMesg.SetAvgStrokeDistance(session.last().avgStrokesLength);
|
||||
} else if (type == STAIRCLIMBER) {
|
||||
|
||||
sessionMesg.SetSport(FIT_SPORT_GENERIC);
|
||||
sessionMesg.SetSport(FIT_SPORT_FITNESS_EQUIPMENT);
|
||||
sessionMesg.SetSubSport(FIT_SUB_SPORT_STAIR_CLIMBING);
|
||||
} else if (type == JUMPROPE) {
|
||||
|
||||
@@ -699,7 +703,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
lapMesg.SetSport(FIT_SPORT_JUMP_ROPE);
|
||||
} else if (type == STAIRCLIMBER) {
|
||||
|
||||
lapMesg.SetSport(FIT_SPORT_GENERIC);
|
||||
lapMesg.SetSport(FIT_SPORT_FITNESS_EQUIPMENT);
|
||||
lapMesg.SetSubSport(FIT_SUB_SPORT_STAIR_CLIMBING);
|
||||
} else {
|
||||
|
||||
@@ -804,6 +808,19 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
newRecord.SetTimestamp(date.GetTimeStamp() + i);
|
||||
encode.Write(newRecord);
|
||||
|
||||
// Write HRV messages with RR-intervals (standard FIT format)
|
||||
// Each HrvMesg can contain up to 5 RR-interval values
|
||||
if (!sl.rrIntervals.isEmpty()) {
|
||||
for (int rrIdx = 0; rrIdx < sl.rrIntervals.size(); rrIdx += 5) {
|
||||
fit::HrvMesg hrvMesg;
|
||||
for (int j = 0; j < 5 && (rrIdx + j) < sl.rrIntervals.size(); j++) {
|
||||
// Convert from milliseconds to seconds for FIT format
|
||||
hrvMesg.SetTime(j, (float)(sl.rrIntervals.at(rrIdx + j) / 1000.0));
|
||||
}
|
||||
encode.Write(hrvMesg);
|
||||
}
|
||||
}
|
||||
|
||||
if (sl.lapTrigger) {
|
||||
|
||||
lapMesg.SetTotalDistance((sl.distance - lastLapOdometer) * 1000.0); // meters
|
||||
@@ -814,7 +831,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
lapMesg.SetMessageIndex(lap_index++);
|
||||
lapMesg.SetLapTrigger(FIT_LAP_TRIGGER_DISTANCE);
|
||||
if (type == JUMPROPE)
|
||||
lapMesg.SetRepetitionNum(session.at(i - 1).inclination);
|
||||
lapMesg.SetRepetitionNum(lap_index);
|
||||
lastLapTimer = sl.elapsedTime;
|
||||
lastLapOdometer = sl.distance;
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ const QString QZSettings::default_user_email = QLatin1String("");
|
||||
const QString QZSettings::user_nickname = QStringLiteral("user_nickname");
|
||||
const QString QZSettings::default_user_nickname = QStringLiteral("");
|
||||
const QString QZSettings::miles_unit = QStringLiteral("miles_unit");
|
||||
const QString QZSettings::weight_kg_unit = QStringLiteral("weight_kg_unit");
|
||||
const QString QZSettings::pause_on_start = QStringLiteral("pause_on_start");
|
||||
const QString QZSettings::treadmill_force_speed = QStringLiteral("treadmill_force_speed");
|
||||
const QString QZSettings::pause_on_start_treadmill = QStringLiteral("pause_on_start_treadmill");
|
||||
@@ -154,6 +155,7 @@ const QString QZSettings::tile_ftp_enabled = QStringLiteral("tile_ftp_enabled");
|
||||
const QString QZSettings::tile_ftp_order = QStringLiteral("tile_ftp_order");
|
||||
const QString QZSettings::tile_heart_enabled = QStringLiteral("tile_heart_enabled");
|
||||
const QString QZSettings::tile_heart_order = QStringLiteral("tile_heart_order");
|
||||
const QString QZSettings::tile_heart_show_as_percent = QStringLiteral("tile_heart_show_as_percent");
|
||||
const QString QZSettings::tile_fan_enabled = QStringLiteral("tile_fan_enabled");
|
||||
const QString QZSettings::tile_fan_order = QStringLiteral("tile_fan_order");
|
||||
const QString QZSettings::tile_jouls_enabled = QStringLiteral("tile_jouls_enabled");
|
||||
@@ -355,6 +357,7 @@ const QString QZSettings::virtual_device_onlyheart = QStringLiteral("virtual_dev
|
||||
const QString QZSettings::virtual_device_echelon = QStringLiteral("virtual_device_echelon");
|
||||
const QString QZSettings::virtual_device_ifit = QStringLiteral("virtual_device_ifit");
|
||||
const QString QZSettings::virtual_device_rower = QStringLiteral("virtual_device_rower");
|
||||
const QString QZSettings::virtual_device_rower_pm5 = QStringLiteral("virtual_device_rower_pm5");
|
||||
const QString QZSettings::virtual_device_force_bike = QStringLiteral("virtual_device_force_bike");
|
||||
const QString QZSettings::virtual_device_force_treadmill = QStringLiteral("virtual_device_force_treadmill");
|
||||
const QString QZSettings::volume_change_gears = QStringLiteral("volume_change_gears");
|
||||
@@ -744,6 +747,7 @@ const QString QZSettings::autolap_distance = QStringLiteral("autolap_distance");
|
||||
const QString QZSettings::nordictrack_s20_treadmill = QStringLiteral("nordictrack_s20_treadmill");
|
||||
const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotion_coachbike_b22_7");
|
||||
const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci");
|
||||
const QString QZSettings::nordictrack_gx_4_5_pro = QStringLiteral("nordictrack_gx_4_5_pro");
|
||||
const QString QZSettings::kingsmith_encrypt_g1_walking_pad = QStringLiteral("kingsmith_encrypt_g1_walking_pad");
|
||||
const QString QZSettings::proformtdf1ip = QStringLiteral("proformtdf1ip");
|
||||
const QString QZSettings::default_proformtdf1ip = QStringLiteral("");
|
||||
@@ -776,6 +780,7 @@ const QString QZSettings::proform_treadmill_505_cst = QStringLiteral("proform_tr
|
||||
const QString QZSettings::nordictrack_treadmill_t8_5s = QStringLiteral("nordictrack_treadmill_t8_5s");
|
||||
const QString QZSettings::proform_treadmill_705_cst = QStringLiteral("proform_treadmill_705_cst");
|
||||
const QString QZSettings::zwift_click = QStringLiteral("zwift_click");
|
||||
const QString QZSettings::thinkrider_controller = QStringLiteral("thinkrider_controller");
|
||||
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");
|
||||
@@ -1038,6 +1043,8 @@ const QString QZSettings::tile_power_avg_enabled = QStringLiteral("tile_power_av
|
||||
const QString QZSettings::tile_power_avg_order = QStringLiteral("tile_power_avg_order");
|
||||
const QString QZSettings::tile_negative_inclination_enabled = QStringLiteral("tile_negative_inclination_enabled");
|
||||
const QString QZSettings::tile_negative_inclination_order = QStringLiteral("tile_negative_inclination_order");
|
||||
const QString QZSettings::tile_hrv_enabled = QStringLiteral("tile_hrv_enabled");
|
||||
const QString QZSettings::tile_hrv_order = QStringLiteral("tile_hrv_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");
|
||||
@@ -1050,7 +1057,7 @@ const QString QZSettings::trainprogram_auto_lap_on_segment = QStringLiteral("tra
|
||||
const QString QZSettings::kingsmith_r2_enable_hw_buttons = QStringLiteral("kingsmith_r2_enable_hw_buttons");
|
||||
|
||||
|
||||
const uint32_t allSettingsCount = 856;
|
||||
const uint32_t allSettingsCount = 861;
|
||||
|
||||
QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
|
||||
@@ -1110,6 +1117,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::user_email, QZSettings::default_user_email},
|
||||
{QZSettings::user_nickname, QZSettings::default_user_nickname},
|
||||
{QZSettings::miles_unit, QZSettings::default_miles_unit},
|
||||
{QZSettings::weight_kg_unit, QZSettings::default_weight_kg_unit},
|
||||
{QZSettings::pause_on_start, QZSettings::default_pause_on_start},
|
||||
{QZSettings::treadmill_force_speed, QZSettings::default_treadmill_force_speed},
|
||||
{QZSettings::pause_on_start_treadmill, QZSettings::default_pause_on_start_treadmill},
|
||||
@@ -1160,6 +1168,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::tile_ftp_order, QZSettings::default_tile_ftp_order},
|
||||
{QZSettings::tile_heart_enabled, QZSettings::default_tile_heart_enabled},
|
||||
{QZSettings::tile_heart_order, QZSettings::default_tile_heart_order},
|
||||
{QZSettings::tile_heart_show_as_percent, QZSettings::default_tile_heart_show_as_percent},
|
||||
{QZSettings::tile_fan_enabled, QZSettings::default_tile_fan_enabled},
|
||||
{QZSettings::tile_fan_order, QZSettings::default_tile_fan_order},
|
||||
{QZSettings::tile_jouls_enabled, QZSettings::default_tile_jouls_enabled},
|
||||
@@ -1339,6 +1348,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::virtual_device_echelon, QZSettings::default_virtual_device_echelon},
|
||||
{QZSettings::virtual_device_ifit, QZSettings::default_virtual_device_ifit},
|
||||
{QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower},
|
||||
{QZSettings::virtual_device_rower_pm5, QZSettings::default_virtual_device_rower_pm5},
|
||||
{QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike},
|
||||
{QZSettings::virtual_device_force_treadmill, QZSettings::default_virtual_device_force_treadmill},
|
||||
{QZSettings::volume_change_gears, QZSettings::default_volume_change_gears},
|
||||
@@ -1667,6 +1677,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill},
|
||||
{QZSettings::freemotion_coachbike_b22_7, QZSettings::default_freemotion_coachbike_b22_7},
|
||||
{QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci},
|
||||
{QZSettings::nordictrack_gx_4_5_pro, QZSettings::default_nordictrack_gx_4_5_pro},
|
||||
{QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad},
|
||||
{QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx},
|
||||
{QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s},
|
||||
@@ -1696,6 +1707,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::nordictrack_treadmill_t8_5s, QZSettings::default_nordictrack_treadmill_t8_5s},
|
||||
{QZSettings::proform_treadmill_705_cst, QZSettings::default_proform_treadmill_705_cst},
|
||||
{QZSettings::zwift_click, QZSettings::default_zwift_click},
|
||||
{QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller},
|
||||
{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},
|
||||
@@ -1911,6 +1923,8 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::tile_power_avg_order, QZSettings::default_tile_power_avg_order},
|
||||
{QZSettings::tile_negative_inclination_enabled, QZSettings::default_tile_negative_inclination_enabled},
|
||||
{QZSettings::tile_negative_inclination_order, QZSettings::default_tile_negative_inclination_order},
|
||||
{QZSettings::tile_hrv_enabled, QZSettings::default_tile_hrv_enabled},
|
||||
{QZSettings::tile_hrv_order, QZSettings::default_tile_hrv_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},
|
||||
|
||||
@@ -272,6 +272,12 @@ class QZSettings {
|
||||
static const QString miles_unit;
|
||||
static constexpr bool default_miles_unit = false;
|
||||
|
||||
/**
|
||||
*@brief Use kg for weight even when miles_unit is true (for UK users).
|
||||
*/
|
||||
static const QString weight_kg_unit;
|
||||
static constexpr bool default_weight_kg_unit = false;
|
||||
|
||||
static const QString pause_on_start;
|
||||
static constexpr bool default_pause_on_start = false;
|
||||
|
||||
@@ -448,6 +454,9 @@ class QZSettings {
|
||||
static const QString tile_heart_order;
|
||||
static constexpr int default_tile_heart_order = 11;
|
||||
|
||||
static const QString tile_heart_show_as_percent;
|
||||
static constexpr bool default_tile_heart_show_as_percent = false;
|
||||
|
||||
static const QString tile_fan_enabled;
|
||||
static constexpr bool default_tile_fan_enabled = true;
|
||||
|
||||
@@ -1052,6 +1061,12 @@ class QZSettings {
|
||||
*/
|
||||
static const QString virtual_device_rower;
|
||||
static constexpr bool default_virtual_device_rower = false;
|
||||
/**
|
||||
*@brief When virtual_device_rower is enabled, use the Concept2 PM5 protocol instead of FTMS.
|
||||
* This enables compatibility with apps like Mywhoosh that only support PM5 rowers.
|
||||
*/
|
||||
static const QString virtual_device_rower_pm5;
|
||||
static constexpr bool default_virtual_device_rower_pm5 = false;
|
||||
/**
|
||||
*@brief Used to force a non-bike device to be presented to client apps as a bike.
|
||||
*/
|
||||
@@ -2054,7 +2069,9 @@ class QZSettings {
|
||||
static constexpr bool default_freemotion_coachbike_b22_7 = false;
|
||||
|
||||
static const QString proform_cycle_trainer_300_ci;
|
||||
static const QString nordictrack_gx_4_5_pro;
|
||||
static constexpr bool default_proform_cycle_trainer_300_ci = false;
|
||||
static constexpr bool default_nordictrack_gx_4_5_pro = false;
|
||||
|
||||
static const QString kingsmith_encrypt_g1_walking_pad;
|
||||
static constexpr bool default_kingsmith_encrypt_g1_walking_pad = false;
|
||||
@@ -2140,6 +2157,9 @@ class QZSettings {
|
||||
static const QString zwift_click;
|
||||
static constexpr bool default_zwift_click = false;
|
||||
|
||||
static const QString thinkrider_controller;
|
||||
static constexpr bool default_thinkrider_controller = false;
|
||||
|
||||
static const QString proform_treadmill_705_cst;
|
||||
static constexpr bool default_proform_treadmill_705_cst = false;
|
||||
|
||||
@@ -2819,6 +2839,18 @@ class QZSettings {
|
||||
static const QString tile_negative_inclination_order;
|
||||
static constexpr int default_tile_negative_inclination_order = 75;
|
||||
|
||||
/**
|
||||
* @brief Enable HRV (Heart Rate Variability) tile
|
||||
*/
|
||||
static const QString tile_hrv_enabled;
|
||||
static constexpr bool default_tile_hrv_enabled = false;
|
||||
|
||||
/**
|
||||
* @brief Order of HRV tile
|
||||
*/
|
||||
static const QString tile_hrv_order;
|
||||
static constexpr int default_tile_hrv_order = 78;
|
||||
|
||||
/**
|
||||
* @brief Chart display mode: 0 = both charts, 1 = heart rate only, 2 = power only
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,8 @@ SessionLine::SessionLine(double speed, int8_t inclination, double distance, uint
|
||||
double instantaneousStrideLengthCM, double groundContactMS, double verticalOscillationMM, double stepCount,
|
||||
double target_cadence, double target_watt, double target_resistance,
|
||||
double target_inclination, double target_speed,
|
||||
double coreTemp, double bodyTemp, double heatStrainIndex,
|
||||
double coreTemp, double bodyTemp, double heatStrainIndex, double hrv,
|
||||
const QList<double> &rrIntervals,
|
||||
const QDateTime &time) {
|
||||
this->speed = speed;
|
||||
this->inclination = inclination;
|
||||
@@ -41,6 +42,8 @@ SessionLine::SessionLine(double speed, int8_t inclination, double distance, uint
|
||||
this->coreTemp = coreTemp;
|
||||
this->bodyTemp = bodyTemp;
|
||||
this->heatStrainIndex = heatStrainIndex;
|
||||
this->hrv = hrv;
|
||||
this->rrIntervals = rrIntervals;
|
||||
}
|
||||
|
||||
SessionLine::SessionLine() {}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QGeoCoordinate>
|
||||
#include <QTimer>
|
||||
#include <QMetaType>
|
||||
#include <QList>
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
@@ -43,6 +44,8 @@ class SessionLine {
|
||||
double coreTemp;
|
||||
double bodyTemp;
|
||||
double heatStrainIndex;
|
||||
double hrv;
|
||||
QList<double> rrIntervals; // RR-intervals in milliseconds for HRV
|
||||
|
||||
SessionLine();
|
||||
SessionLine(double speed, int8_t inclination, double distance, uint16_t watt, resistance_t resistance,
|
||||
@@ -52,7 +55,8 @@ class SessionLine {
|
||||
double instantaneousStrideLengthCM, double groundContactMS, double verticalOscillationMM, double stepCount,
|
||||
double target_cadence, double target_watt, double target_resistance,
|
||||
double target_inclination, double target_speed,
|
||||
double coreTemp, double bodyTemp, double heatStrainIndex,
|
||||
double coreTemp, double bodyTemp, double heatStrainIndex, double hrv,
|
||||
const QList<double> &rrIntervals,
|
||||
const QDateTime &time = QDateTime::currentDateTime());
|
||||
};
|
||||
|
||||
|
||||
@@ -276,6 +276,9 @@ ScrollView {
|
||||
property int tile_avg_pace_order: 76
|
||||
property bool tile_power_avg_enabled: false
|
||||
property int tile_power_avg_order: 77
|
||||
property bool tile_heart_show_as_percent: false
|
||||
property bool tile_hrv_enabled: false
|
||||
property int tile_hrv_order: 78
|
||||
}
|
||||
|
||||
|
||||
@@ -941,29 +944,59 @@ ScrollView {
|
||||
title: qsTr("Heart")
|
||||
linkedBoolSetting: "tile_heart_enabled"
|
||||
settings: settings
|
||||
accordionContent: RowLayout {
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelheartrateOrder
|
||||
text: qsTr("order index:")
|
||||
accordionContent: ColumnLayout {
|
||||
SwitchDelegate {
|
||||
id: heartShowAsPercentSwitch
|
||||
text: qsTr("Show as %FC Max")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.tile_heart_show_as_percent
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
onClicked: settings.tile_heart_show_as_percent = checked
|
||||
}
|
||||
ComboBox {
|
||||
id: heartrateOrderTextField
|
||||
model: rootItem.tile_order
|
||||
displayText: settings.tile_heart_order
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onActivated: {
|
||||
displayText = heartrateOrderTextField.currentValue
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("When enabled, displays heart rate as percentage of maximum heart rate (%FC Max) instead of BPM. AVG and MAX values will also show percentages.")
|
||||
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)
|
||||
}
|
||||
Button {
|
||||
id: okheartrateOrderButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: {settings.tile_heart_order = heartrateOrderTextField.displayText; toast.show("Setting saved!"); }
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelheartrateOrder
|
||||
text: qsTr("order index:")
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
ComboBox {
|
||||
id: heartrateOrderTextField
|
||||
model: rootItem.tile_order
|
||||
displayText: settings.tile_heart_order
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onActivated: {
|
||||
displayText = heartrateOrderTextField.currentValue
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: okheartrateOrderButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: {settings.tile_heart_order = heartrateOrderTextField.displayText; toast.show("Setting saved!"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5535,5 +5568,50 @@ ScrollView {
|
||||
Layout.fillWidth: true
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
AccordionCheckElement {
|
||||
id: hrvEnabledAccordion
|
||||
title: qsTr("HRV (Heart Rate Variability)")
|
||||
linkedBoolSetting: "tile_hrv_enabled"
|
||||
settings: settings
|
||||
accordionContent: RowLayout {
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelhrvOrder
|
||||
text: qsTr("order index:")
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
ComboBox {
|
||||
id: hrvOrderTextField
|
||||
model: rootItem.tile_order
|
||||
displayText: settings.tile_hrv_order
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onActivated: {
|
||||
displayText = hrvOrderTextField.currentValue
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: okhrvOrderButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: {settings.tile_hrv_order = hrvOrderTextField.displayText; toast.show("Setting saved!"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Shows Heart Rate Variability (HRV) from a compatible heart rate belt. Displays RMSSD value in milliseconds.")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
154
src/settings.qml
154
src/settings.qml
@@ -1265,14 +1265,24 @@ import Qt.labs.platform 1.1
|
||||
property real peloton_treadmill_walking_min_speed: 0.0
|
||||
property real peloton_treadmill_running_min_speed: 0.0
|
||||
property bool trainprogram_auto_lap_on_segment: false
|
||||
|
||||
property bool power_avg_3s: false
|
||||
property bool tile_power_avg_enabled: false
|
||||
property int tile_power_avg_order: 77
|
||||
property bool life_fitness_ic5: false
|
||||
property bool technogym_bike: false
|
||||
|
||||
property bool kingsmith_r2_enable_hw_buttons: false
|
||||
property bool treadmill_direct_distance: false
|
||||
|
||||
property bool domyos_treadmill_ts100: false
|
||||
property bool thinkrider_controller: false
|
||||
property bool weight_kg_unit: false
|
||||
property bool virtual_device_rower_pm5: false
|
||||
property bool tile_heart_show_as_percent: false
|
||||
property bool tile_hrv_enabled: false
|
||||
property int tile_hrv_order: 78
|
||||
property bool nordictrack_gx_4_5_pro: false
|
||||
}
|
||||
|
||||
|
||||
@@ -1359,12 +1369,12 @@ import Qt.labs.platform 1.1
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelWeight
|
||||
text: qsTr("Player Weight") + "(" + (settings.miles_unit?"lbs":"kg") + ")"
|
||||
text: qsTr("Player Weight") + "(" + ((settings.miles_unit && !settings.weight_kg_unit)?"lbs":"kg") + ")"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
TextField {
|
||||
id: weightTextField
|
||||
text: (settings.miles_unit?settings.weight * 2.20462:settings.weight)
|
||||
text: ((settings.miles_unit && !settings.weight_kg_unit)?settings.weight * 2.20462:settings.weight)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
@@ -1376,11 +1386,11 @@ import Qt.labs.platform 1.1
|
||||
id: okWeightButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: { settings.weight = (settings.miles_unit?weightTextField.text / 2.20462:weightTextField.text); toast.show("Setting saved!"); }
|
||||
onClicked: { settings.weight = ((settings.miles_unit && !settings.weight_kg_unit)?weightTextField.text / 2.20462:weightTextField.text); toast.show("Setting saved!"); }
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: qsTr("Enter your weight in kilograms so QZ can more accurately calculate calories burned. NOTE: If you choose to use miles as the unit for distance traveled, you will be asked to enter your weight in pounds (lbs).")
|
||||
text: qsTr("Enter your weight in kilograms so QZ can more accurately calculate calories burned. NOTE: If you choose to use miles as the unit for distance traveled, you will be asked to enter your weight in pounds (lbs) unless you enable 'Use kg for weight'.")
|
||||
font.bold: true
|
||||
font.italic: true
|
||||
font.pixelSize: Qt.application.font.pixelSize - 2
|
||||
@@ -1706,6 +1716,36 @@ import Qt.labs.platform 1.1
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: weightKgUnitDelegate
|
||||
text: qsTr("Use kg for weight")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.weight_kg_unit
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: settings.weight_kg_unit = checked
|
||||
visible: settings.miles_unit
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Turn on if you want to use kilograms (kg) for weight instead of pounds (lbs). Useful for UK users who use miles for distance but kg for weight.")
|
||||
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)
|
||||
visible: settings.miles_unit
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: pauseOnStartDelegate
|
||||
text: qsTr("Pause when App Starts")
|
||||
@@ -2498,12 +2538,12 @@ import Qt.labs.platform 1.1
|
||||
spacing: 10
|
||||
Label {
|
||||
id: labelBikeWeight
|
||||
text: qsTr("Bike Weight") + "(" + (settings.miles_unit?"lbs":"kg") + ")"
|
||||
text: qsTr("Bike Weight") + "(" + ((settings.miles_unit && !settings.weight_kg_unit)?"lbs":"kg") + ")"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
TextField {
|
||||
id: bikeweightTextField
|
||||
text: (settings.miles_unit?settings.bike_weight * 2.20462:settings.bike_weight)
|
||||
text: ((settings.miles_unit && !settings.weight_kg_unit)?settings.bike_weight * 2.20462:settings.bike_weight)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
@@ -2515,12 +2555,12 @@ import Qt.labs.platform 1.1
|
||||
id: okBikeWeightButton
|
||||
text: "OK"
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
onClicked: { settings.bike_weight = (settings.miles_unit?bikeweightTextField.text / 2.20462:bikeweightTextField.text); toast.show("Setting saved!"); }
|
||||
onClicked: { settings.bike_weight = ((settings.miles_unit && !settings.weight_kg_unit)?bikeweightTextField.text / 2.20462:bikeweightTextField.text); toast.show("Setting saved!"); }
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Enables QZ to include the weight of your bike when calculating speed. For example, if you are competing against yourself on VZfit, adding bike weight will “level the playing field” against your virtual self. If you have set QZ to calculate distance in miles, enter the bike weight in pounds (lbs). Default unit is kilograms (kgs).")
|
||||
text: qsTr("Enables QZ to include the weight of your bike when calculating speed. For example, if you are competing against yourself on VZfit, adding bike weight will 'level the playing field' against your virtual self. If you have set QZ to calculate distance in miles, enter the bike weight in pounds (lbs) unless you enable 'Use kg for weight'. Default unit is kilograms (kgs).")
|
||||
font.bold: true
|
||||
font.italic: true
|
||||
font.pixelSize: Qt.application.font.pixelSize - 2
|
||||
@@ -4170,7 +4210,8 @@ import Qt.labs.platform 1.1
|
||||
"TDF 1.0 PFEVEX71316.0",
|
||||
"Proform XBike",
|
||||
"Proform 225 CSX PFEX32925 INT.0",
|
||||
"Proform CSX210"
|
||||
"Proform CSX210",
|
||||
"Nordictrack GX 4.5 Pro"
|
||||
]
|
||||
|
||||
// Initialize when the accordion content becomes visible
|
||||
@@ -4206,7 +4247,8 @@ import Qt.labs.platform 1.1
|
||||
settings.proform_bike_PFEVEX71316_0 ? 16 :
|
||||
settings.proform_xbike ? 17 :
|
||||
settings.proform_225_csx_PFEX32925_INT_0 ? 18 :
|
||||
settings.proform_csx210 ? 19 : 0;
|
||||
settings.proform_csx210 ? 19 :
|
||||
settings.nordictrack_gx_4_5_pro ? 20 : 0;
|
||||
|
||||
console.log("bikeModelComboBox selected model: " + selectedModel);
|
||||
if (selectedModel >= 0) {
|
||||
@@ -4240,6 +4282,7 @@ import Qt.labs.platform 1.1
|
||||
settings.proform_xbike = false;
|
||||
settings.proform_225_csx_PFEX32925_INT_0 = false;
|
||||
settings.proform_csx210 = false;
|
||||
settings.nordictrack_gx_4_5_pro = false;
|
||||
|
||||
// Set corresponding setting for selected model
|
||||
switch (currentIndex) {
|
||||
@@ -4262,6 +4305,7 @@ import Qt.labs.platform 1.1
|
||||
case 17: settings.proform_xbike = true; break;
|
||||
case 18: settings.proform_225_csx_PFEX32925_INT_0 = true; break;
|
||||
case 19: settings.proform_csx210 = true; break;
|
||||
case 20: settings.nordictrack_gx_4_5_pro = true; break;
|
||||
}
|
||||
|
||||
window.settings_restart_to_apply = true;
|
||||
@@ -6726,6 +6770,29 @@ import Qt.labs.platform 1.1
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
Label {
|
||||
text: qsTr("Garmin Server:")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ComboBox {
|
||||
id: garminServerComboBox
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
model: ["Global (garmin.com)", "China (garmin.cn)"]
|
||||
currentIndex: settings.garmin_domain === "garmin.cn" ? 1 : 0
|
||||
onCurrentIndexChanged: {
|
||||
var newDomain = currentIndex === 1 ? "garmin.cn" : "garmin.com";
|
||||
if (newDomain !== settings.garmin_domain) {
|
||||
rootItem.garmin_connect_logout();
|
||||
settings.garmin_domain = newDomain;
|
||||
window.settings_restart_to_apply = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Test Garmin Login"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -12744,6 +12811,43 @@ import Qt.labs.platform 1.1
|
||||
}
|
||||
}*/
|
||||
|
||||
AccordionElement {
|
||||
title: qsTr("Thinkrider Options")
|
||||
indicatRectColor: Material.color(Material.Grey)
|
||||
textColor: Material.color(Material.Yellow)
|
||||
color: Material.backgroundColor
|
||||
|
||||
accordionContent: ColumnLayout {
|
||||
spacing: 0
|
||||
IndicatorOnlySwitch {
|
||||
text: qsTr("Thinkrider Controller")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.thinkrider_controller
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: { settings.thinkrider_controller = checked; window.settings_restart_to_apply = true; }
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Thinkrider VS200 remote controller. Use it to change gears on QZ!")
|
||||
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 {
|
||||
title: qsTr("Zwift Devices Options")
|
||||
indicatRectColor: Material.color(Material.Grey)
|
||||
@@ -13262,6 +13366,36 @@ import Qt.labs.platform 1.1
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: virtualDeviceRowerPm5Delegate
|
||||
text: qsTr("Virtual Rower as PM5")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.virtual_device_rower_pm5
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
visible: settings.virtual_device_rower
|
||||
onClicked: { settings.virtual_device_rower_pm5 = checked; window.settings_restart_to_apply = true; }
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("When enabled, the virtual rower will use the Concept2 PM5 protocol instead of FTMS. This provides compatibility with apps like Mywhoosh that only support PM5 rowers. Default is off.")
|
||||
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
|
||||
visible: settings.virtual_device_rower
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: virtualDeviceForceTreadmillDelegate
|
||||
text: qsTr("Force Virtual Treadmill")
|
||||
|
||||
@@ -651,6 +651,101 @@ void TemplateInfoSenderBuilder::onTrainingProgramPreview(const QJsonValue &msgCo
|
||||
tempSender->send(response.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onGetWorkoutPreview(TemplateInfoSender *tempSender) {
|
||||
if (!homeform::singleton()) {
|
||||
return;
|
||||
}
|
||||
|
||||
homeform *hf = homeform::singleton();
|
||||
|
||||
// Build workout preview data from homeform properties
|
||||
QJsonObject main;
|
||||
QJsonObject outObj;
|
||||
QJsonArray watts, speed, inclination, resistance, cadence;
|
||||
|
||||
int points = hf->preview_workout_points();
|
||||
|
||||
if (points > 0) {
|
||||
QList<double> wattsData = hf->preview_workout_watt();
|
||||
QList<double> speedData = hf->preview_workout_speed();
|
||||
QList<double> inclinationData = hf->preview_workout_inclination();
|
||||
QList<double> resistanceData = hf->preview_workout_resistance();
|
||||
QList<double> cadenceData = hf->preview_workout_cadence();
|
||||
|
||||
outObj[QStringLiteral("points")] = points;
|
||||
outObj[QStringLiteral("description")] = hf->previewWorkoutDescription();
|
||||
outObj[QStringLiteral("tags")] = hf->previewWorkoutTags();
|
||||
|
||||
// Build data arrays with x,y points
|
||||
for (int i = 0; i < points; i++) {
|
||||
// Watts
|
||||
if (i < wattsData.size()) {
|
||||
QJsonObject wattPoint;
|
||||
wattPoint[QStringLiteral("x")] = i;
|
||||
wattPoint[QStringLiteral("y")] = wattsData[i];
|
||||
watts.append(wattPoint);
|
||||
}
|
||||
|
||||
// Speed
|
||||
if (i < speedData.size()) {
|
||||
QJsonObject speedPoint;
|
||||
speedPoint[QStringLiteral("x")] = i;
|
||||
speedPoint[QStringLiteral("y")] = speedData[i];
|
||||
speed.append(speedPoint);
|
||||
}
|
||||
|
||||
// Inclination
|
||||
if (i < inclinationData.size()) {
|
||||
QJsonObject incPoint;
|
||||
incPoint[QStringLiteral("x")] = i;
|
||||
incPoint[QStringLiteral("y")] = inclinationData[i];
|
||||
inclination.append(incPoint);
|
||||
}
|
||||
|
||||
// Resistance
|
||||
if (i < resistanceData.size()) {
|
||||
QJsonObject resPoint;
|
||||
resPoint[QStringLiteral("x")] = i;
|
||||
resPoint[QStringLiteral("y")] = resistanceData[i];
|
||||
resistance.append(resPoint);
|
||||
}
|
||||
|
||||
// Cadence
|
||||
if (i < cadenceData.size()) {
|
||||
QJsonObject cadPoint;
|
||||
cadPoint[QStringLiteral("x")] = i;
|
||||
cadPoint[QStringLiteral("y")] = cadenceData[i];
|
||||
cadence.append(cadPoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine device type
|
||||
QString deviceType = QStringLiteral("bike");
|
||||
if (speed.size() > 0 && watts.size() == 0) {
|
||||
deviceType = QStringLiteral("treadmill");
|
||||
} else if (watts.size() == 0 && resistance.size() > 0) {
|
||||
deviceType = QStringLiteral("elliptical");
|
||||
}
|
||||
|
||||
outObj[QStringLiteral("watts")] = watts;
|
||||
outObj[QStringLiteral("speed")] = speed;
|
||||
outObj[QStringLiteral("inclination")] = inclination;
|
||||
outObj[QStringLiteral("resistance")] = resistance;
|
||||
outObj[QStringLiteral("cadence")] = cadence;
|
||||
outObj[QStringLiteral("deviceType")] = deviceType;
|
||||
|
||||
// Add miles_unit setting
|
||||
QSettings settings;
|
||||
outObj[QStringLiteral("miles_unit")] = settings.value(QStringLiteral("miles_unit"), false).toBool();
|
||||
}
|
||||
|
||||
main[QStringLiteral("content")] = outObj;
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_workoutpreview");
|
||||
main[QStringLiteral("type")] = QStringLiteral("workoutpreview");
|
||||
QJsonDocument response(main);
|
||||
tempSender->send(response.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onTrainingProgramOpen(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
QJsonObject content = msgContent.toObject();
|
||||
QString urlString = content.value(QStringLiteral("url")).toString();
|
||||
@@ -1084,6 +1179,42 @@ void TemplateInfoSenderBuilder::onGearsMinus(const QJsonValue &msgContent, Templ
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onSpeedPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit speed_Plus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_speed_plus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onSpeedMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit speed_Minus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_speed_minus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onInclinationPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit inclination_Plus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_inclination_plus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onInclinationMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
emit inclination_Minus();
|
||||
main[QStringLiteral("msg")] = QStringLiteral("R_inclination_minus");
|
||||
QJsonDocument out(main);
|
||||
tempSender->send(out.toJson());
|
||||
}
|
||||
|
||||
void TemplateInfoSenderBuilder::onPelotonStartWorkout(const QJsonValue &msgContent, TemplateInfoSender *tempSender) {
|
||||
Q_UNUSED(msgContent);
|
||||
QJsonObject main, outObj;
|
||||
@@ -1210,6 +1341,9 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) {
|
||||
} else if (msg == QStringLiteral("trainprogram_preview")) {
|
||||
onTrainingProgramPreview(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("getworkoutpreview")) {
|
||||
onGetWorkoutPreview(sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("trainprogram_open_clicked")) {
|
||||
onTrainingProgramOpen(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
@@ -1255,6 +1389,18 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) {
|
||||
} else if (msg == QStringLiteral("gears_minus")) {
|
||||
onGearsMinus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("speed_plus")) {
|
||||
onSpeedPlus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("speed_minus")) {
|
||||
onSpeedMinus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("inclination_plus")) {
|
||||
onInclinationPlus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("inclination_minus")) {
|
||||
onInclinationMinus(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
} else if (msg == QStringLiteral("peloton_start_workout")) {
|
||||
onPelotonStartWorkout(jsonObject[QStringLiteral("content")], sender);
|
||||
return;
|
||||
|
||||
@@ -34,6 +34,10 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
void pelotonOffset_Minus();
|
||||
void gears_Plus();
|
||||
void gears_Minus();
|
||||
void speed_Plus();
|
||||
void speed_Minus();
|
||||
void inclination_Plus();
|
||||
void inclination_Minus();
|
||||
int pelotonOffset();
|
||||
bool pelotonAskStart();
|
||||
void peloton_start_workout();
|
||||
@@ -79,6 +83,10 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
void onPelotonOffsetMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onGearsPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onGearsMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onSpeedPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onSpeedMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onInclinationPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onInclinationMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onPelotonStartWorkout(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onPelotonAbortWorkout(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onFloatingClose(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
@@ -88,6 +96,7 @@ class TemplateInfoSenderBuilder : public QObject {
|
||||
void onLoadTrainingPrograms(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onGetTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onTrainingProgramPreview(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onGetWorkoutPreview(TemplateInfoSender *tempSender);
|
||||
void onTrainingProgramOpen(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onTrainingProgramAutostart(const QJsonValue &msgContent, TemplateInfoSender *tempSender);
|
||||
void onWorkoutEditorEnv(TemplateInfoSender *tempSender);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,12 +40,22 @@ class virtualrower : public virtualdevice {
|
||||
QLowEnergyService *serviceHR = nullptr;
|
||||
QLowEnergyService *serviceBattery = nullptr;
|
||||
QLowEnergyService *serviceFIT = nullptr;
|
||||
QLowEnergyService *servicePM5Rowing = nullptr;
|
||||
QLowEnergyService *servicePM5DeviceInfo = nullptr;
|
||||
QLowEnergyService *servicePM5GAP = nullptr;
|
||||
QLowEnergyService *servicePM5Control = nullptr;
|
||||
QLowEnergyAdvertisingData advertisingData;
|
||||
QLowEnergyServiceData serviceDataHR;
|
||||
QLowEnergyServiceData serviceDataFIT;
|
||||
QLowEnergyServiceData serviceDataPM5Rowing;
|
||||
QLowEnergyServiceData serviceDataPM5DeviceInfo;
|
||||
QLowEnergyServiceData serviceDataPM5GAP;
|
||||
QLowEnergyServiceData serviceDataPM5Control;
|
||||
QTimer rowerTimer;
|
||||
bluetoothdevice *Rower;
|
||||
|
||||
bool pm5Mode = false;
|
||||
|
||||
uint16_t lastWheelTime = 0;
|
||||
uint32_t wheelRevs = 0;
|
||||
qint64 lastFTMSFrameReceived = 0;
|
||||
@@ -54,6 +64,12 @@ class virtualrower : public virtualdevice {
|
||||
|
||||
void writeCharacteristic(QLowEnergyService *service, const QLowEnergyCharacteristic &characteristic,
|
||||
const QByteArray &value);
|
||||
void setupPM5Services();
|
||||
QByteArray buildPM5GeneralStatus();
|
||||
QByteArray buildPM5AdditionalStatus();
|
||||
QByteArray buildPM5AdditionalStatus2();
|
||||
QByteArray buildPM5StrokeData();
|
||||
QByteArray buildPM5AdditionalStrokeData();
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
225
tst/Devices/TestSunnyfitStepper.h
Normal file
225
tst/Devices/TestSunnyfitStepper.h
Normal file
@@ -0,0 +1,225 @@
|
||||
#pragma once
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QByteArray>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief Sunnyfit Mini Stepper (SF-S) BLE Packet Test Data
|
||||
*
|
||||
* Extracted from btsnoop_hci.log capture of actual device communication.
|
||||
* These are the 20-byte data frames (0x5a 0x05) from the capture file.
|
||||
*/
|
||||
class SunnyfitStepperTestData {
|
||||
public:
|
||||
/**
|
||||
* @brief Raw 20-byte data frames captured from actual device
|
||||
* Format: 0x5a (start) + 0x05 (command) + 18 bytes of data
|
||||
*
|
||||
* Byte positions:
|
||||
* [0]: 0x5a (start marker)
|
||||
* [1]: 0x05 (command type - data frame)
|
||||
* [6]: Cadence (SPM) - single byte
|
||||
* [16]: Step Counter (increments 0, 1, 2, 3...)
|
||||
*/
|
||||
static const std::vector<QByteArray> getTestFrames() {
|
||||
return {
|
||||
// Frame 0: cadence=0, step=0
|
||||
QByteArray::fromHex("5a05001a032200000524000000000003260000052900"),
|
||||
|
||||
// Frame 1: cadence=0, step=1
|
||||
QByteArray::fromHex("5a05001a032200000524010000000003260100052900"),
|
||||
|
||||
// Frame 2: cadence=0, step=2
|
||||
QByteArray::fromHex("5a05001a032200000524020000000003260200052900"),
|
||||
|
||||
// Frame 3: cadence=32, step=3
|
||||
QByteArray::fromHex("5a05001a032220000524020000000003260300052900"),
|
||||
|
||||
// Frame 4: cadence=67, step=4
|
||||
QByteArray::fromHex("5a05001a032243000524040000000003260400052900"),
|
||||
|
||||
// Frame 5: cadence=67, step=5
|
||||
QByteArray::fromHex("5a05001a032243000524040000000003260500052900"),
|
||||
|
||||
// Frame 6: cadence=67, step=6
|
||||
QByteArray::fromHex("5a05001a032243000524040000000003260600052900"),
|
||||
|
||||
// Frame 7: cadence=20, step=7
|
||||
QByteArray::fromHex("5a05001a032214000524050000000003260700052900"),
|
||||
|
||||
// Frame 8: cadence=53, step=8
|
||||
QByteArray::fromHex("5a05001a032235000524070000000003260800052900"),
|
||||
|
||||
// Frame 9: cadence=63, step=9
|
||||
QByteArray::fromHex("5a05001a03223f000524080000000003260900052900"),
|
||||
|
||||
// Frame 10: cadence=63, step=10
|
||||
QByteArray::fromHex("5a05001a03223f000524080000000003260a00052900"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Expected extracted values from each test frame
|
||||
*/
|
||||
struct ExpectedMetrics {
|
||||
int frameIndex;
|
||||
double expectedCadence;
|
||||
int expectedStepCount;
|
||||
double expectedSpeed; // cadence / 3.2
|
||||
};
|
||||
|
||||
static const std::vector<ExpectedMetrics> getExpectedValues() {
|
||||
return {
|
||||
{0, 0.0, 0, 0.0}, // cadence=0, step=0
|
||||
{1, 0.0, 1, 0.0}, // cadence=0, step=1
|
||||
{2, 0.0, 2, 0.0}, // cadence=0, step=2
|
||||
{3, 32.0, 3, 10.0}, // cadence=32, step=3, speed=32/3.2=10
|
||||
{4, 67.0, 4, 20.9375}, // cadence=67, step=4, speed=67/3.2≈20.94
|
||||
{5, 67.0, 5, 20.9375}, // cadence=67, step=5
|
||||
{6, 67.0, 6, 20.9375}, // cadence=67, step=6
|
||||
{7, 20.0, 7, 6.25}, // cadence=20, step=7, speed=20/3.2=6.25
|
||||
{8, 53.0, 8, 16.5625}, // cadence=53, step=8, speed=53/3.2≈16.56
|
||||
{9, 63.0, 9, 19.6875}, // cadence=63, step=9, speed=63/3.2≈19.69
|
||||
{10, 63.0, 10, 19.6875}, // cadence=63, step=10
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a single 20-byte frame and extract metrics
|
||||
* @return pair<cadence, stepCount> or returns {-1, -1} on error
|
||||
*/
|
||||
static std::pair<double, int> parseFrame(const QByteArray& frame) {
|
||||
if (frame.length() != 20) {
|
||||
return {-1, -1};
|
||||
}
|
||||
|
||||
if ((uint8_t)frame[0] != 0x5a) {
|
||||
return {-1, -1};
|
||||
}
|
||||
|
||||
// Extract cadence from byte 6 (single byte)
|
||||
double cadence = (double)(uint8_t)frame[6];
|
||||
|
||||
// Extract step counter from byte 16 (single byte, little-endian)
|
||||
int stepCount = (uint8_t)frame[16];
|
||||
|
||||
return {cadence, stepCount};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test suite for Sunnyfit Stepper frame parsing
|
||||
*/
|
||||
class SunnyfitStepperParsingTest : public testing::Test {
|
||||
protected:
|
||||
SunnyfitStepperTestData testData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test parsing of individual frames
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, ParseFrames) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
auto expectedValues = SunnyfitStepperTestData::getExpectedValues();
|
||||
|
||||
ASSERT_EQ(frames.size(), expectedValues.size())
|
||||
<< "Test data mismatch: frames and expected values should have same size";
|
||||
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frames[i]);
|
||||
|
||||
EXPECT_EQ(cadence, expectedValues[i].expectedCadence)
|
||||
<< "Frame " << i << ": Cadence mismatch";
|
||||
|
||||
EXPECT_EQ(stepCount, expectedValues[i].expectedStepCount)
|
||||
<< "Frame " << i << ": Step count mismatch";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test speed calculation from cadence
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, CalculateSpeed) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
auto expectedValues = SunnyfitStepperTestData::getExpectedValues();
|
||||
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frames[i]);
|
||||
|
||||
double calculatedSpeed = cadence / 3.2;
|
||||
|
||||
EXPECT_DOUBLE_EQ(calculatedSpeed, expectedValues[i].expectedSpeed)
|
||||
<< "Frame " << i << ": Speed calculation mismatch (cadence=" << cadence << ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test step counter increments
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, StepCounterIncrement) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
auto expectedValues = SunnyfitStepperTestData::getExpectedValues();
|
||||
|
||||
int previousSteps = -1;
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frames[i]);
|
||||
|
||||
if (previousSteps >= 0) {
|
||||
int increment = stepCount - previousSteps;
|
||||
EXPECT_EQ(increment, 1)
|
||||
<< "Frame " << i << ": Step counter should increment by 1 (was "
|
||||
<< previousSteps << ", now " << stepCount << ")";
|
||||
}
|
||||
previousSteps = stepCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test cadence variation detection
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, CadenceVariation) {
|
||||
auto frames = SunnyfitStepperTestData::getTestFrames();
|
||||
|
||||
std::vector<double> cadences;
|
||||
for (const auto& frame : frames) {
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(frame);
|
||||
cadences.push_back(cadence);
|
||||
}
|
||||
|
||||
// Verify we have cadence variation in the test data
|
||||
double minCadence = *std::min_element(cadences.begin(), cadences.end());
|
||||
double maxCadence = *std::max_element(cadences.begin(), cadences.end());
|
||||
|
||||
EXPECT_LT(minCadence, maxCadence)
|
||||
<< "Test data should have cadence variation";
|
||||
|
||||
EXPECT_EQ(minCadence, 0.0)
|
||||
<< "Minimum cadence should be 0";
|
||||
|
||||
EXPECT_EQ(maxCadence, 67.0)
|
||||
<< "Maximum cadence should be 67";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test frame validation
|
||||
*/
|
||||
TEST_F(SunnyfitStepperParsingTest, FrameValidation) {
|
||||
// Invalid length
|
||||
QByteArray shortFrame = QByteArray::fromHex("5a05001a0322");
|
||||
auto [cadence, stepCount] = SunnyfitStepperTestData::parseFrame(shortFrame);
|
||||
EXPECT_EQ(cadence, -1) << "Should reject short frames";
|
||||
EXPECT_EQ(stepCount, -1) << "Should reject short frames";
|
||||
|
||||
// Invalid start marker
|
||||
QByteArray invalidStart = QByteArray::fromHex("0105001a032200000524000000000003260000052900");
|
||||
std::tie(cadence, stepCount) = SunnyfitStepperTestData::parseFrame(invalidStart);
|
||||
EXPECT_EQ(cadence, -1) << "Should reject frames with invalid start marker";
|
||||
EXPECT_EQ(stepCount, -1) << "Should reject frames with invalid start marker";
|
||||
|
||||
// Valid frame
|
||||
QByteArray validFrame = SunnyfitStepperTestData::getTestFrames()[3];
|
||||
std::tie(cadence, stepCount) = SunnyfitStepperTestData::parseFrame(validFrame);
|
||||
EXPECT_EQ(cadence, 32.0) << "Should parse valid frame";
|
||||
EXPECT_EQ(stepCount, 3) << "Should parse valid frame";
|
||||
}
|
||||
@@ -54,6 +54,7 @@ HEADERS += \
|
||||
Devices/deviceindex.h \
|
||||
Devices/devicenamepatterngroup.h \
|
||||
Devices/devicetestdataindex.h \
|
||||
Devices/TestSunnyfitStepper.h \
|
||||
Erg/ergtabletestsuite.h \
|
||||
GarminConnect/garminconnecttestsuite.h \
|
||||
ToolTests/qfittestsuite.h \
|
||||
|
||||
Reference in New Issue
Block a user