mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
43 Commits
424ed85179
...
9447edb415
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9447edb415 | ||
|
|
e74378acd9 | ||
|
|
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 |
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git log:*)"
|
||||
"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"
|
||||
@@ -559,6 +559,12 @@
|
||||
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 */; };
|
||||
@@ -1665,6 +1671,15 @@
|
||||
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; };
|
||||
@@ -2340,6 +2355,12 @@
|
||||
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 */,
|
||||
@@ -2891,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>";
|
||||
@@ -3873,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 */,
|
||||
@@ -3983,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 */,
|
||||
@@ -4143,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 */,
|
||||
@@ -4583,7 +4613,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4784,7 +4814,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5021,7 +5051,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5117,7 +5147,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5209,7 +5239,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5325,7 +5355,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5435,7 +5465,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5465,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 = (
|
||||
@@ -5526,7 +5556,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1278;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
@@ -5552,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) + ");");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ Page {
|
||||
property int age: 35
|
||||
property string sex: "Male"
|
||||
property bool miles_unit: false
|
||||
property bool weight_kg_unit: false
|
||||
property string heart_rate_belt_name: "Disabled"
|
||||
property bool garmin_companion: false
|
||||
property string filter_device: "Disabled"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1126,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()) ||
|
||||
@@ -1402,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);
|
||||
@@ -1504,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)))) ||
|
||||
@@ -1551,6 +1554,7 @@ 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("SCHWINN 510T")) ||
|
||||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
|
||||
@@ -1563,6 +1567,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))) ||
|
||||
@@ -1842,6 +1847,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
|
||||
@@ -2033,6 +2039,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-")) ||
|
||||
@@ -2247,6 +2266,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
|
||||
@@ -2871,6 +2901,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)
|
||||
@@ -2895,6 +2926,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!");
|
||||
@@ -3650,6 +3682,11 @@ void bluetooth::restart() {
|
||||
delete echelonStairclimber;
|
||||
echelonStairclimber = nullptr;
|
||||
}
|
||||
if (sunnyfitStepper) {
|
||||
|
||||
delete sunnyfitStepper;
|
||||
sunnyfitStepper = nullptr;
|
||||
}
|
||||
if (octaneTreadmill) {
|
||||
|
||||
delete octaneTreadmill;
|
||||
@@ -3784,6 +3821,11 @@ void bluetooth::restart() {
|
||||
delete sportsTechElliptical;
|
||||
sportsTechElliptical = nullptr;
|
||||
}
|
||||
if (sportsTechRower) {
|
||||
|
||||
delete sportsTechRower;
|
||||
sportsTechRower = nullptr;
|
||||
}
|
||||
if (sportsPlusBike) {
|
||||
|
||||
delete sportsPlusBike;
|
||||
@@ -4079,6 +4121,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return echelonStride;
|
||||
} else if (echelonStairclimber) {
|
||||
return echelonStairclimber;
|
||||
} else if (sunnyfitStepper) {
|
||||
return sunnyfitStepper;
|
||||
} else if (octaneTreadmill) {
|
||||
return octaneTreadmill;
|
||||
} else if (ziproTreadmill) {
|
||||
@@ -4133,6 +4177,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"
|
||||
|
||||
@@ -249,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;
|
||||
@@ -270,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;
|
||||
|
||||
@@ -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,7 +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 (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 {
|
||||
@@ -1461,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;
|
||||
}
|
||||
|
||||
@@ -1529,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";
|
||||
@@ -1747,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;
|
||||
@@ -1825,6 +1871,13 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("S18 found");
|
||||
S18 = true;
|
||||
max_resistance = 24;
|
||||
} 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1896,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;
|
||||
@@ -173,6 +176,7 @@ class ftmsbike : public bike {
|
||||
bool FS_YK = false;
|
||||
bool S18 = false;
|
||||
bool ZIPRO_RAVE = false;
|
||||
bool SPEEDRACEX = false;
|
||||
|
||||
uint8_t secondsToResetTimer = 5;
|
||||
|
||||
@@ -181,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) /
|
||||
|
||||
@@ -1966,84 +1966,95 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
break;
|
||||
case 0x07:
|
||||
case 0x08:
|
||||
case 0x09:
|
||||
Resistance = 5;
|
||||
m_pelotonResistance = 33;
|
||||
break;
|
||||
case 0x0A:
|
||||
case 0x0b:
|
||||
case 0x09:
|
||||
Resistance = 6;
|
||||
m_pelotonResistance = 35;
|
||||
break;
|
||||
case 0x0c:
|
||||
case 0x0d:
|
||||
case 0x0A:
|
||||
case 0x0b:
|
||||
Resistance = 7;
|
||||
m_pelotonResistance = 38;
|
||||
break;
|
||||
case 0x0e:
|
||||
case 0x0f:
|
||||
case 0x0c:
|
||||
case 0x0d:
|
||||
Resistance = 8;
|
||||
m_pelotonResistance = 40;
|
||||
break;
|
||||
case 0x10:
|
||||
case 0x11:
|
||||
case 0x0e:
|
||||
Resistance = 9;
|
||||
m_pelotonResistance = 45;
|
||||
break;
|
||||
case 0x12:
|
||||
case 0x13:
|
||||
case 0x0f:
|
||||
case 0x10:
|
||||
Resistance = 10;
|
||||
m_pelotonResistance = 50;
|
||||
break;
|
||||
case 0x14:
|
||||
case 0x15:
|
||||
case 0x11:
|
||||
Resistance = 11;
|
||||
m_pelotonResistance = 55;
|
||||
break;
|
||||
case 0x16:
|
||||
case 0x17:
|
||||
case 0x12:
|
||||
case 0x13:
|
||||
Resistance = 12;
|
||||
m_pelotonResistance = 60;
|
||||
break;
|
||||
case 0x18:
|
||||
case 0x19:
|
||||
case 0x14:
|
||||
Resistance = 13;
|
||||
m_pelotonResistance = 63;
|
||||
break;
|
||||
case 0x1a:
|
||||
case 0x1b:
|
||||
case 0x15:
|
||||
case 0x16:
|
||||
Resistance = 14;
|
||||
m_pelotonResistance = 65;
|
||||
break;
|
||||
case 0x1c:
|
||||
case 0x1d:
|
||||
case 0x17:
|
||||
Resistance = 15;
|
||||
m_pelotonResistance = 68;
|
||||
case 0x1e:
|
||||
case 0x1f:
|
||||
case 0x18:
|
||||
case 0x19:
|
||||
Resistance = 16;
|
||||
m_pelotonResistance = 70;
|
||||
break;
|
||||
case 0x20:
|
||||
case 0x21:
|
||||
case 0x1a:
|
||||
case 0x1b:
|
||||
Resistance = 17;
|
||||
m_pelotonResistance = 75;
|
||||
break;
|
||||
case 0x22:
|
||||
case 0x23:
|
||||
case 0x1c:
|
||||
Resistance = 18;
|
||||
m_pelotonResistance = 80;
|
||||
break;
|
||||
case 0x24:
|
||||
case 0x25:
|
||||
case 0x1d:
|
||||
Resistance = 19;
|
||||
m_pelotonResistance = 85;
|
||||
break;
|
||||
case 0x26:
|
||||
case 0x27:
|
||||
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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -5510,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));
|
||||
@@ -6753,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)
|
||||
@@ -7686,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);
|
||||
@@ -7722,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>");
|
||||
|
||||
@@ -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,17 +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 \
|
||||
@@ -367,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 \
|
||||
@@ -379,6 +383,7 @@ 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 \
|
||||
@@ -391,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 \
|
||||
@@ -772,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 \
|
||||
@@ -987,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\\)=
|
||||
}
|
||||
|
||||
@@ -1006,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
|
||||
21
src/qfit.cpp
21
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
|
||||
|
||||
@@ -155,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");
|
||||
@@ -356,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");
|
||||
@@ -1041,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");
|
||||
@@ -1053,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 = 858;
|
||||
const uint32_t allSettingsCount = 861;
|
||||
|
||||
QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
|
||||
@@ -1164,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},
|
||||
@@ -1343,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},
|
||||
@@ -1917,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},
|
||||
|
||||
@@ -454,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;
|
||||
|
||||
@@ -1058,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.
|
||||
*/
|
||||
@@ -2830,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1276,8 +1276,12 @@ import Qt.labs.platform 1.1
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -13362,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