mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
143 Commits
claude/deb
...
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 | ||
|
|
424ed85179 | ||
|
|
410409f842 | ||
|
|
1ff9da34db | ||
|
|
361874f1ea | ||
|
|
33b686bf3e | ||
|
|
64d56d6e2e | ||
|
|
898a1604f3 | ||
|
|
74e1aba909 | ||
|
|
bf75b2bda0 | ||
|
|
80faa062e1 | ||
|
|
51808cc8a4 | ||
|
|
72f57053a7 | ||
|
|
13ea5313b1 | ||
|
|
7f694733b2 | ||
|
|
b1755c004a | ||
|
|
360ab66431 | ||
|
|
04b659a91f | ||
|
|
9487fa3cb4 | ||
|
|
1ac2149424 | ||
|
|
28558697b2 | ||
|
|
6918fb9eba | ||
|
|
3d017560ce | ||
|
|
6520bfe86e | ||
|
|
6a1b6e526e | ||
|
|
365abbb7cb | ||
|
|
d3f52682cc | ||
|
|
da4f360f63 | ||
|
|
b1c6cf70f5 | ||
|
|
50e18b1db4 | ||
|
|
47ea3c2176 | ||
|
|
c83c272ed4 | ||
|
|
de3ea61ecf | ||
|
|
2d53ebf190 | ||
|
|
93feea6c16 | ||
|
|
baaf689b4c | ||
|
|
4dea13d78e | ||
|
|
914b02f8e0 | ||
|
|
0e01889ed3 | ||
|
|
19ef4bb230 | ||
|
|
51c8d060de | ||
|
|
d1afe0ebb2 | ||
|
|
e5532ca04e | ||
|
|
bf37d681de | ||
|
|
03bf9d8fd1 | ||
|
|
161362f11f | ||
|
|
2bb0e212db | ||
|
|
67344ea130 | ||
|
|
727fb99572 | ||
|
|
89ec6eef1f | ||
|
|
a7ca3f329a | ||
|
|
695c05c284 | ||
|
|
b38712851d | ||
|
|
73241765d1 | ||
|
|
ad79beb44b | ||
|
|
39288f1343 | ||
|
|
ff093a126e | ||
|
|
489ef0a665 | ||
|
|
73771f42c2 | ||
|
|
dbdd58c398 | ||
|
|
5c0aa59bed | ||
|
|
7ec3b601b4 | ||
|
|
9d2bf0821c | ||
|
|
5d85cdd8e5 | ||
|
|
56bbc2439c | ||
|
|
31e1bb8a9f | ||
|
|
88a8b138ca | ||
|
|
10cfac1e40 | ||
|
|
79952ad73c | ||
|
|
3a3755d18c | ||
|
|
9a45b28f8c | ||
|
|
9b70d6c144 | ||
|
|
137036efd7 | ||
|
|
e5e2851d45 | ||
|
|
cadfaa8be1 | ||
|
|
6df4acf35c | ||
|
|
2ccbbfb07b | ||
|
|
7d4775c0ea | ||
|
|
5ad8fa4ec8 | ||
|
|
d7f386f662 | ||
|
|
9baa9a0f23 | ||
|
|
f3fc0a5212 | ||
|
|
eb29ec5dfd | ||
|
|
d29b312554 | ||
|
|
56f3916387 | ||
|
|
bbb2d6fe90 | ||
|
|
34be7d451f | ||
|
|
83bbb56ad8 | ||
|
|
87846d9dd2 | ||
|
|
94a70a56db | ||
|
|
563c7a1445 | ||
|
|
6e23d0c743 | ||
|
|
60d5880081 | ||
|
|
fb56c58046 | ||
|
|
27759c14ee | ||
|
|
19bee8ee9b | ||
|
|
d25ecb176c | ||
|
|
da92d8711f | ||
|
|
e98ec8be41 | ||
|
|
7c20123661 | ||
|
|
aa23c5c6c1 |
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(tshark:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git ls-tree:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
506
.github/workflows/main.yml
vendored
506
.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 ../..
|
||||
|
||||
@@ -428,7 +451,16 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: test_results_xml
|
||||
path: tst/test-results/**/*.xml
|
||||
path: tst/test-results/**/*.xml
|
||||
|
||||
- name: Upload test FIT files and database
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test_fit_files_and_db
|
||||
path: |
|
||||
tst/test-artifacts/*.fit
|
||||
tst/test-artifacts/*.sqlite
|
||||
|
||||
# - name: Test Peloton API
|
||||
# if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
@@ -579,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
|
||||
@@ -599,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:
|
||||
@@ -627,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:
|
||||
@@ -660,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
|
||||
@@ -671,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
|
||||
@@ -773,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
|
||||
@@ -819,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/"
|
||||
@@ -858,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
|
||||
@@ -870,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
|
||||
@@ -886,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
|
||||
@@ -928,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
|
||||
@@ -950,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'
|
||||
@@ -974,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 '{
|
||||
@@ -997,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
|
||||
@@ -1028,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
|
||||
@@ -1086,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
|
||||
@@ -1132,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'
|
||||
@@ -1158,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 '{
|
||||
@@ -1179,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 }}
|
||||
@@ -1188,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
|
||||
@@ -1228,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: |
|
||||
@@ -1285,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
|
||||
@@ -1317,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
|
||||
@@ -1343,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
|
||||
@@ -1361,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
|
||||
@@ -1377,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
|
||||
@@ -1423,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 '{
|
||||
@@ -1446,12 +1581,13 @@ jobs:
|
||||
],
|
||||
"builtin-baseline": "8c2fcacefba009d63672f9d137f192765e632c9f"
|
||||
}' > vcpkg.json
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-vcpkg-msvc2022.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
|
||||
- name: Build
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
|
||||
101
CLAUDE.md
101
CLAUDE.md
@@ -98,6 +98,57 @@ The application follows a hierarchical device architecture:
|
||||
4. Update `qdomyos-zwift.pri` with new source files
|
||||
5. Add tests in `tst/Devices/` following existing patterns
|
||||
|
||||
### Adding Device Detection to bluetooth.cpp
|
||||
|
||||
**CRITICAL: Always verify device pattern conflicts before adding to bluetooth.cpp**
|
||||
|
||||
When adding a new device pattern to `src/devices/bluetooth.cpp`, you **MUST** follow these verification steps:
|
||||
|
||||
1. **Search for Similar Patterns**: Use grep/search to find all existing device patterns that might conflict
|
||||
- Search for device name prefixes (e.g., if adding "KS-NG-", search for all "KS-" patterns)
|
||||
- Check patterns in all device type cases (bikes, treadmills, ellipticals, rowers, etc.)
|
||||
|
||||
2. **Analyze Pattern Specificity**: Understand the pattern hierarchy
|
||||
- More specific patterns should be checked BEFORE less specific ones
|
||||
- Example: "KS-NGCH-" is more specific than "KS-NG-"
|
||||
- The order matters: devices are matched by the FIRST matching pattern in the if-else chain
|
||||
|
||||
3. **Check Case Order**: Verify the order of device type cases in bluetooth.cpp
|
||||
- Earlier cases take precedence over later cases
|
||||
- Ensure more specific patterns in earlier cases won't prevent your pattern from matching
|
||||
- Ensure your pattern won't incorrectly match devices intended for other cases
|
||||
|
||||
4. **Document Conflicts**: When conflicts exist, verify they are intentional
|
||||
- More specific patterns earlier in the chain should catch specific devices
|
||||
- Your pattern should only catch devices not matched by more specific patterns
|
||||
- Example: "KS-NGCH-X21C" (kingsmithR2Treadmill) should match before "KS-NG-" (horizontreadmill)
|
||||
|
||||
5. **Test Pattern Matching**: Consider these scenarios
|
||||
- Will your pattern match the intended device? (e.g., "KS-NG-X218")
|
||||
- Will it incorrectly match other devices? (e.g., "KS-NGCH-X21C")
|
||||
- Are there existing patterns that would match your device first?
|
||||
|
||||
**Example Verification Process:**
|
||||
|
||||
```bash
|
||||
# Search for similar patterns
|
||||
grep -n "KS-" src/devices/bluetooth.cpp
|
||||
|
||||
# Review each match for conflicts
|
||||
# - kingsmithR2Treadmill has "KS-NGCH-X21C" (line 1323)
|
||||
# - horizontreadmill has "KS-MC" (line 1562)
|
||||
# - Adding "KS-NG-" to horizontreadmill is safe because:
|
||||
# 1. "KS-NGCH-" patterns are more specific
|
||||
# 2. kingsmithR2Treadmill case comes first (line 1312 vs 1560)
|
||||
# 3. "KS-NG-X218" won't match "KS-NGCH-" patterns
|
||||
```
|
||||
|
||||
**Common Pitfalls:**
|
||||
- Adding a pattern without checking existing patterns
|
||||
- Not considering pattern order in the if-else chain
|
||||
- Adding overly broad patterns that match unintended devices
|
||||
- Not testing with actual device names
|
||||
|
||||
### Characteristics & Protocols
|
||||
- Bluetooth characteristics handlers in `src/characteristics/`
|
||||
- FTMS (Fitness Machine Service) protocol support
|
||||
@@ -368,7 +419,55 @@ The ProForm 995i implementation serves as the reference example:
|
||||
- Test device detection thoroughly using the existing test infrastructure
|
||||
- Consider platform differences when adding new features
|
||||
|
||||
## Updating Version Numbers
|
||||
|
||||
When releasing a new version of QDomyos-Zwift, you must update the version number in **3 files**:
|
||||
|
||||
### 1. Android Manifest
|
||||
**File**: `src/android/AndroidManifest.xml`
|
||||
|
||||
Update both `versionName` and `versionCode`:
|
||||
```xml
|
||||
<manifest ... android:versionName="X.XX.XX" android:versionCode="XXXX" ...>
|
||||
```
|
||||
|
||||
- `versionName`: The human-readable version (e.g., "2.20.26")
|
||||
- `versionCode`: Integer build number that must be incremented (e.g., 1274)
|
||||
|
||||
### 2. Main QML File
|
||||
**File**: `src/main.qml`
|
||||
|
||||
Update the version text displayed in the UI (around line 938):
|
||||
```qml
|
||||
ItemDelegate {
|
||||
text: "version X.XX.XX"
|
||||
width: parent.width
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Qt Project Include File
|
||||
**File**: `src/qdomyos-zwift.pri`
|
||||
|
||||
Update the VERSION variable (around line 1011):
|
||||
```pri
|
||||
VERSION = X.XX.XX
|
||||
```
|
||||
|
||||
### Version Numbering Convention
|
||||
|
||||
- **Major.Minor.Patch** format (e.g., 2.20.26)
|
||||
- **Build number** must always increment, never reuse
|
||||
- Update all 3 files together to keep versions synchronized
|
||||
|
||||
### iOS Version (Optional)
|
||||
|
||||
iOS version is managed through Xcode project variables:
|
||||
- `MARKETING_VERSION` in project.pbxproj (corresponds to versionName)
|
||||
- `CURRENT_PROJECT_VERSION` in project.pbxproj (corresponds to versionCode)
|
||||
|
||||
These are typically updated via Xcode IDE rather than manually editing files.
|
||||
|
||||
## Additional Memories
|
||||
|
||||
- When adding a new setting in QML (setting-tiles.qml), you must:
|
||||
* Add the property at the END of the properties list
|
||||
* Add the property at the END of the properties list
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.cagnulein.qdomyoszwift</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== QDomyos-Zwift CI Post Clone Script ==="
|
||||
echo "Installing Qt 5.15.2 EXACTLY and preparing environment"
|
||||
|
||||
# Exit if not on macOS (sanity check)
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
echo "ERROR: This script must run on macOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Apply Xcode Cloud network workarounds
|
||||
export HOMEBREW_NO_AUTO_UPDATE=1
|
||||
export GIT_HTTP_MAX_REQUESTS=1
|
||||
|
||||
# Check if Qt 5.15.2 is already installed
|
||||
if command -v qmake &> /dev/null; then
|
||||
QT_VERSION=$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)
|
||||
if [[ "$QT_VERSION" == "5.15.2" ]]; then
|
||||
echo "Qt 5.15.2 already installed - PERFECT!"
|
||||
export QT_DIR=$(dirname $(dirname $(which qmake)))
|
||||
export PATH="$QT_DIR/bin:$PATH"
|
||||
|
||||
# CRITICAL: Save Qt path to persistent file for next script
|
||||
echo "Saving existing Qt installation path for ci_pre_xcodebuild.sh..."
|
||||
echo "export QT_DIR=\"$QT_DIR\"" > /tmp/qt_env.sh
|
||||
echo "export PATH=\"$QT_DIR/bin:/tmp/Qt-5.15.2/ios/bin:/private/tmp/Qt-5.15.2/ios/bin:\$PATH\"" >> /tmp/qt_env.sh
|
||||
chmod +x /tmp/qt_env.sh
|
||||
else
|
||||
echo "WRONG Qt version found: $QT_VERSION"
|
||||
echo "MUST install Qt 5.15.2 exactly"
|
||||
# Uninstall wrong version
|
||||
brew uninstall --ignore-dependencies qt@5 qt || echo "No Qt to uninstall"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Force install Qt 5.15.2 EXACTLY
|
||||
if ! command -v qmake &> /dev/null || [[ "$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)" != "5.15.2" ]]; then
|
||||
echo "Installing Qt 5.15.2 EXACTLY - NO OTHER VERSION ACCEPTED"
|
||||
|
||||
# Method 1: Use aqt (Another Qt Installer) to get exact version
|
||||
echo "Installing aqt (Another Qt Installer) for exact Qt version control..."
|
||||
python3 -m pip install aqt || echo "aqt installation failed, trying homebrew method"
|
||||
|
||||
if command -v aqt &> /dev/null; then
|
||||
echo "Using aqt to install Qt 5.15.2 exactly..."
|
||||
aqt install-qt mac desktop 5.15.2 --outputdir /usr/local/Qt
|
||||
export QT_DIR="/usr/local/Qt/5.15.2/clang_64"
|
||||
export PATH="$QT_DIR/bin:$PATH"
|
||||
|
||||
# CRITICAL: Save Qt path to persistent file for next script
|
||||
echo "Saving aqt Qt installation path for ci_pre_xcodebuild.sh..."
|
||||
echo "export QT_DIR=\"/usr/local/Qt/5.15.2/clang_64\"" > /tmp/qt_env.sh
|
||||
echo "export PATH=\"/usr/local/Qt/5.15.2/clang_64/bin:/tmp/Qt-5.15.2/ios/bin:/private/tmp/Qt-5.15.2/ios/bin:\$PATH\"" >> /tmp/qt_env.sh
|
||||
chmod +x /tmp/qt_env.sh
|
||||
else
|
||||
echo "aqt failed, using precompiled Qt 5.15.2 from GitHub..."
|
||||
|
||||
# Download precompiled Qt 5.15.2 from your GitHub release
|
||||
echo "Downloading precompiled Qt 5.15.2 from GitHub..."
|
||||
cd /tmp
|
||||
curl -L "https://github.com/cagnulein/qt5.15.2/releases/download/qt-5.15.2/qt-5.15.2.tar.xz" -o qt-5.15.2.tar.xz
|
||||
|
||||
if [[ -f "qt-5.15.2.tar.xz" ]]; then
|
||||
echo "Extracting precompiled Qt 5.15.2..."
|
||||
tar -mxf qt-5.15.2.tar.xz
|
||||
|
||||
cd 5.15.2 || { echo "Extraction failed or directory not found"; exit 1; }
|
||||
|
||||
# Debug: Check extraction result
|
||||
echo "Contents after extraction:"
|
||||
ls -la
|
||||
|
||||
# Install to temp location (no sudo needed)
|
||||
echo "Setting up Qt 5.15.2..."
|
||||
mkdir -p /tmp/Qt-5.15.2
|
||||
|
||||
# Files are extracted directly - copy Qt directories
|
||||
echo "Files extracted directly, copying Qt directories..."
|
||||
|
||||
# Copy the Qt directories we need
|
||||
if [[ -d "ios" ]]; then
|
||||
cp -R ios /tmp/Qt-5.15.2/
|
||||
echo "Copied ios directory"
|
||||
fi
|
||||
|
||||
if [[ -d "clang_64" ]]; then
|
||||
cp -R clang_64 /tmp/Qt-5.15.2/
|
||||
echo "Copied clang_64 directory"
|
||||
fi
|
||||
|
||||
if [[ -d "qthttpserver" ]]; then
|
||||
cp -R qthttpserver /tmp/Qt-5.15.2/
|
||||
echo "Copied qthttpserver directory"
|
||||
fi
|
||||
|
||||
if [[ -f "sha1s.txt" ]]; then
|
||||
cp sha1s.txt /tmp/Qt-5.15.2/
|
||||
echo "Copied sha1s.txt"
|
||||
fi
|
||||
|
||||
# Set environment for iOS development - support both /tmp and /private/tmp
|
||||
export QT_DIR="/tmp/Qt-5.15.2/ios"
|
||||
export PATH="$QT_DIR/bin:$PATH"
|
||||
|
||||
# CRITICAL: Save Qt path to persistent file for next script
|
||||
echo "Saving Qt installation path for ci_pre_xcodebuild.sh..."
|
||||
echo "export QT_DIR=\"/tmp/Qt-5.15.2/ios\"" > /tmp/qt_env.sh
|
||||
echo "export PATH=\"/tmp/Qt-5.15.2/ios/bin:/private/tmp/Qt-5.15.2/ios/bin:\$PATH\"" >> /tmp/qt_env.sh
|
||||
chmod +x /tmp/qt_env.sh
|
||||
|
||||
echo "Qt 5.15.2 precompiled installation completed"
|
||||
|
||||
# CRITICAL: Fix hardcoded paths in .pri files
|
||||
# The Qt archive contains .pri files with absolute paths from local machine
|
||||
# Replace them with the Xcode Cloud installation path
|
||||
echo "Fixing hardcoded paths in Qt .pri files..."
|
||||
find /tmp/Qt-5.15.2 -name "*.pri" -type f -exec sed -i '' 's|/Users/cagnulein/Qt/5.15.2|/tmp/Qt-5.15.2|g' {} \;
|
||||
find /tmp/Qt-5.15.2 -name "*.pri" -type f -exec sed -i '' 's|/Users/cagnulein/Qt/5.15.2|/private/tmp/Qt-5.15.2|g' {} \;
|
||||
echo "Fixed paths in .pri files"
|
||||
|
||||
# CRITICAL: Download missing qmldbg libraries
|
||||
echo "Downloading missing qmldbg libraries..."
|
||||
cd /tmp
|
||||
|
||||
# Download libqmldbg_debugger.a
|
||||
echo "Downloading libqmldbg_debugger.a.zip..."
|
||||
curl -L -o libqmldbg_debugger.a.zip https://github.com/cagnulein/qt5.15.2/releases/download/qt-5.15.2/libqmldbg_debugger.a.zip
|
||||
unzip -o libqmldbg_debugger.a.zip
|
||||
|
||||
# Download libqmldbg_nativedebugger.a (from the old zip)
|
||||
echo "Downloading libqmldbg_nativedebugger.zip..."
|
||||
curl -L -o libqmldbg_nativedebugger.zip https://github.com/cagnulein/qt5.15.2/releases/download/qt-5.15.2/libqmldbg_debugger.zip
|
||||
unzip -o libqmldbg_nativedebugger.zip
|
||||
|
||||
echo "Contents after extraction:"
|
||||
ls -la libqmldbg*.a 2>/dev/null || echo "No .a files found in current directory"
|
||||
|
||||
# Ensure target directory exists
|
||||
mkdir -p /tmp/Qt-5.15.2/ios/plugins/qmltooling
|
||||
|
||||
# Move libqmldbg_debugger.a
|
||||
if [[ -f "libqmldbg_debugger.a" ]]; then
|
||||
mv libqmldbg_debugger.a /tmp/Qt-5.15.2/ios/plugins/qmltooling/
|
||||
echo "SUCCESS: Moved libqmldbg_debugger.a"
|
||||
else
|
||||
echo "FATAL ERROR: libqmldbg_debugger.a not found after extraction"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Move libqmldbg_nativedebugger.a (rename from _debug version if needed)
|
||||
if [[ -f "libqmldbg_nativedebugger.a" ]]; then
|
||||
mv libqmldbg_nativedebugger.a /tmp/Qt-5.15.2/ios/plugins/qmltooling/
|
||||
echo "SUCCESS: Moved libqmldbg_nativedebugger.a"
|
||||
elif [[ -f "libqmldbg_nativedebugger_debug.a" ]]; then
|
||||
# Use debug version as fallback (better than nothing)
|
||||
mv libqmldbg_nativedebugger_debug.a /tmp/Qt-5.15.2/ios/plugins/qmltooling/libqmldbg_nativedebugger.a
|
||||
echo "WARNING: Used libqmldbg_nativedebugger_debug.a as fallback"
|
||||
else
|
||||
echo "FATAL ERROR: libqmldbg_nativedebugger.a not found after extraction"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installed missing qmldbg libraries"
|
||||
rm -f libqmldbg_debugger.a.zip libqmldbg_nativedebugger.zip
|
||||
|
||||
# Verify httpserver module is now findable
|
||||
if [[ -f "/tmp/Qt-5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri" ]]; then
|
||||
echo "SUCCESS: httpserver module .pri file found"
|
||||
grep "QT.httpserver.libs" /tmp/Qt-5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri | head -1
|
||||
else
|
||||
echo "WARNING: httpserver .pri file not found at expected location"
|
||||
find /tmp/Qt-5.15.2 -name "*httpserver*.pri" 2>/dev/null || echo "No httpserver .pri files found"
|
||||
fi
|
||||
else
|
||||
echo "ERROR: Failed to download precompiled Qt from GitHub"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fi
|
||||
fi
|
||||
|
||||
# MANDATORY verification - FAIL if not 5.15.2
|
||||
echo "MANDATORY Qt 5.15.2 verification..."
|
||||
if command -v qmake &> /dev/null; then
|
||||
QT_VERSION=$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)
|
||||
if [[ "$QT_VERSION" != "5.15.2" ]]; then
|
||||
echo "FATAL ERROR: Qt version is $QT_VERSION, NOT 5.15.2"
|
||||
echo "Build CANNOT continue with wrong Qt version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SUCCESS: Qt 5.15.2 verified!"
|
||||
qmake -v
|
||||
|
||||
# Show Qt installation path
|
||||
QT_INSTALL_PATH=$(dirname $(dirname $(which qmake)))
|
||||
echo "Qt 5.15.2 installed at: $QT_INSTALL_PATH"
|
||||
|
||||
echo "Qt 5.15.2 installation completed successfully (Bluetooth already patched)"
|
||||
else
|
||||
echo "FATAL ERROR: No qmake found after installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# CRITICAL: Generate secret.h from Xcode Cloud environment variables
|
||||
echo "Generating secret.h from environment variables..."
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH/src"
|
||||
|
||||
echo "#define STRAVA_SECRET_KEY ${STRAVA_SECRET_KEY}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${PELOTON_SECRET_KEY}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${SMTP_USERNAME}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${SMTP_PASSWORD}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${SMTP_SERVER}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${INTERVALSICU_CLIENT_ID}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${INTERVALSICU_CLIENT_SECRET}" >> secret.h
|
||||
|
||||
echo "secret.h generated successfully"
|
||||
|
||||
# Generate cesium-key.js if cesiumkey is provided
|
||||
if [[ -n "${CESIUMKEY}" ]]; then
|
||||
echo "Generating cesium-key.js..."
|
||||
echo "${CESIUMKEY}" > inner_templates/googlemaps/cesium-key.js
|
||||
echo "cesium-key.js generated successfully"
|
||||
else
|
||||
echo "CESIUMKEY not provided, skipping cesium-key.js generation"
|
||||
fi
|
||||
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH"
|
||||
|
||||
# CRITICAL FIX: Disable legacy build locations to enable Swift Package support
|
||||
# This must be done BEFORE xcodebuild -resolvePackageDependencies is called
|
||||
echo "Configuring Xcode project to disable legacy build locations..."
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug"
|
||||
|
||||
# Create xcshareddata directory if it doesn't exist
|
||||
mkdir -p qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata
|
||||
|
||||
# Create WorkspaceSettings.xcsettings to disable legacy build locations
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildSystemType</key>
|
||||
<string>Latest</string>
|
||||
<key>BuildLocationStyle</key>
|
||||
<string>UseAppPreferences</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Create IDEWorkspaceChecks.plist
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Workspace settings created - modern build system enabled"
|
||||
|
||||
# Remove SYMROOT from project.pbxproj to disable legacy build locations
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Removing SYMROOT settings from project.pbxproj..."
|
||||
sed -i '' '/SYMROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' '/OBJROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# Fix all absolute paths: replace local machine path with Xcode Cloud path
|
||||
echo "Fixing absolute paths for Xcode Cloud..."
|
||||
sed -i '' 's|/Users/cagnulein/qdomyos-zwift/|/Volumes/workspace/repository/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
echo "SYMROOT removed and paths fixed - legacy build locations disabled"
|
||||
else
|
||||
echo "WARNING: project.pbxproj not found"
|
||||
fi
|
||||
|
||||
cd "$CI_PRIMARY_REPOSITORY_PATH"
|
||||
|
||||
echo "Post-clone setup completed successfully - Qt 5.15.2 EXACTLY installed"
|
||||
354
build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/ci_scripts/ci_pre_xcodebuild.sh
Executable file
354
build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/ci_scripts/ci_pre_xcodebuild.sh
Executable file
@@ -0,0 +1,354 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== QDomyos-Zwift CI Pre-Xcodebuild Script ==="
|
||||
echo "Running qmake to generate Xcode project with MOC files"
|
||||
|
||||
# CRITICAL: Load Qt environment from persistent file
|
||||
echo "Loading Qt environment from ci_post_clone.sh..."
|
||||
if [[ -f "/tmp/qt_env.sh" ]]; then
|
||||
echo "Found Qt environment file, loading..."
|
||||
source /tmp/qt_env.sh
|
||||
echo "Qt environment loaded from persistent file"
|
||||
echo "QT_DIR: $QT_DIR"
|
||||
echo "PATH: $PATH"
|
||||
else
|
||||
echo "WARNING: No Qt environment file found, trying to find Qt anyway..."
|
||||
fi
|
||||
|
||||
# Find Qt installation (should be 5.15.2 from post_clone script)
|
||||
if command -v qmake &> /dev/null; then
|
||||
QT_VERSION=$(qmake -v | grep -o "5\.[0-9]*\.[0-9]*" | head -1)
|
||||
if [[ "$QT_VERSION" != "5.15.2" ]]; then
|
||||
echo "FATAL ERROR: Qt version is $QT_VERSION, expected 5.15.2"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using Qt 5.15.2 - CORRECT!"
|
||||
echo "qmake location: $(which qmake)"
|
||||
else
|
||||
echo "FATAL ERROR: qmake not found"
|
||||
echo "Current PATH: $PATH"
|
||||
echo "Listing /tmp for debugging:"
|
||||
ls -la /tmp/ | grep -i qt || echo "No Qt directories in /tmp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to project root directory
|
||||
cd ../..
|
||||
|
||||
# CRITICAL: Save absolute path to project root for later use
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
export PROJECT_ROOT
|
||||
echo "Project root saved: $PROJECT_ROOT"
|
||||
|
||||
# Verify we're in the correct directory
|
||||
if [[ ! -f "qdomyos-zwift.pro" ]]; then
|
||||
echo "ERROR: qdomyos-zwift.pro not found. Are we in the right directory?"
|
||||
pwd
|
||||
ls -la
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "Running qmake for iOS Debug build..."
|
||||
|
||||
# Run qmake to generate Xcode project and Makefiles
|
||||
# Use release config since precompiled Qt doesn't have debug libs
|
||||
# Force iphoneos SDK for device builds (not simulator)
|
||||
export QMAKE_XCODE_DEVELOPER_PATH="/Applications/Xcode.app/Contents/Developer"
|
||||
export QMAKE_IOS_DEPLOYMENT_TARGET=12.0
|
||||
qmake -spec macx-ios-clang CONFIG+=release CONFIG+=device CONFIG-=simulator CONFIG+=iphoneos "QMAKE_APPLE_DEVICE_ARCHS=arm64"
|
||||
|
||||
echo "qmake completed successfully"
|
||||
|
||||
# CRITICAL: Debug Qt installation before make
|
||||
echo "Debugging Qt installation before make..."
|
||||
echo "Checking Qt include directories:"
|
||||
ls -la /tmp/Qt-5.15.2/ios/include/ 2>/dev/null || echo "No /tmp/Qt-5.15.2/ios/include/"
|
||||
ls -la /private/tmp/Qt-5.15.2/ios/include/ 2>/dev/null || echo "No /private/tmp/Qt-5.15.2/ios/include/"
|
||||
|
||||
echo "Checking for QDebug specifically:"
|
||||
find /tmp/Qt-5.15.2/ios/include/ -name "*QDebug*" 2>/dev/null || echo "QDebug not found in /tmp/"
|
||||
find /private/tmp/Qt-5.15.2/ios/include/ -name "*QDebug*" 2>/dev/null || echo "QDebug not found in /private/tmp/"
|
||||
|
||||
echo "Checking QtCore include directory:"
|
||||
ls -la /tmp/Qt-5.15.2/ios/include/QtCore/ 2>/dev/null || echo "No QtCore in /tmp/"
|
||||
ls -la /private/tmp/Qt-5.15.2/ios/include/QtCore/ 2>/dev/null || echo "No QtCore in /private/tmp/"
|
||||
|
||||
# Setup build cache for faster compilation
|
||||
BUILD_CACHE_DIR="$HOME/Library/Caches/XcodeCloud/QDomyos-Zwift-Build"
|
||||
mkdir -p "$BUILD_CACHE_DIR"
|
||||
|
||||
# Check if we have cached object files
|
||||
if [[ -d "$BUILD_CACHE_DIR/objects" && -f "$BUILD_CACHE_DIR/build_hash.txt" ]]; then
|
||||
CURRENT_HASH=$(find "$PROJECT_ROOT/src" -name "*.cpp" -o -name "*.h" -o -name "*.mm" | sort | xargs cat | shasum -a 256 | cut -d' ' -f1)
|
||||
CACHED_HASH=$(cat "$BUILD_CACHE_DIR/build_hash.txt" 2>/dev/null || echo "none")
|
||||
|
||||
if [[ "$CURRENT_HASH" == "$CACHED_HASH" ]]; then
|
||||
echo "Source files unchanged, restoring build cache..."
|
||||
if cp -r "$BUILD_CACHE_DIR/objects/"* . 2>/dev/null; then
|
||||
echo "Build cache restored successfully"
|
||||
else
|
||||
echo "Cache restoration failed, will build from scratch"
|
||||
fi
|
||||
else
|
||||
echo "Source files changed, cache invalid"
|
||||
rm -rf "$BUILD_CACHE_DIR/objects" "$BUILD_CACHE_DIR/build_hash.txt"
|
||||
fi
|
||||
fi
|
||||
|
||||
# CRITICAL: Create fake xcodebuild BEFORE make to prevent build failures
|
||||
# During make, qmake will try to call xcodebuild which will fail due to code signing
|
||||
# We create a fake xcodebuild that just returns success
|
||||
echo "Creating fake xcodebuild to skip Xcode build during make..."
|
||||
mkdir -p /tmp/fake_xcode
|
||||
cat > /tmp/fake_xcode/xcodebuild << 'XCODE_EOF'
|
||||
#!/bin/bash
|
||||
echo "Skipping xcodebuild during make - will use correct project later"
|
||||
exit 0
|
||||
XCODE_EOF
|
||||
chmod +x /tmp/fake_xcode/xcodebuild
|
||||
|
||||
# Prepend fake xcodebuild to PATH so it's found first
|
||||
export PATH="/tmp/fake_xcode:$PATH"
|
||||
echo "Fake xcodebuild created and added to PATH"
|
||||
which xcodebuild
|
||||
|
||||
# CRITICAL: Run make to compile Qt project and generate MOC files
|
||||
echo "Running make to compile Qt project and generate MOC files..."
|
||||
# Use parallel compilation for faster builds
|
||||
make -j$(sysctl -n hw.ncpu)
|
||||
|
||||
echo "make completed successfully - MOC files generated"
|
||||
|
||||
# Remove fake xcodebuild from PATH
|
||||
export PATH="${PATH#/tmp/fake_xcode:}"
|
||||
echo "Fake xcodebuild removed from PATH"
|
||||
|
||||
# Cache the build results for next time
|
||||
echo "Caching build results..."
|
||||
mkdir -p "$BUILD_CACHE_DIR/objects"
|
||||
# Cache compiled object files and MOC files
|
||||
find . -name "*.o" -o -name "moc_*.cpp" -o -name "moc_*.h" | while read file; do
|
||||
cp "$file" "$BUILD_CACHE_DIR/objects/" 2>/dev/null || echo "Could not cache $file"
|
||||
done
|
||||
|
||||
# Store hash of source files for cache validation
|
||||
CURRENT_HASH=$(find "$PROJECT_ROOT/src" -name "*.cpp" -o -name "*.h" -o -name "*.mm" | sort | xargs cat | shasum -a 256 | cut -d' ' -f1)
|
||||
echo "$CURRENT_HASH" > "$BUILD_CACHE_DIR/build_hash.txt"
|
||||
echo "Build cache updated"
|
||||
|
||||
# NOW restore Xcode project and fix qmake corruption AFTER make
|
||||
echo "Restoring Xcode project from git AFTER make..."
|
||||
echo "qmake regenerates src/qdomyoszwift.xcodeproj without proper code signing"
|
||||
|
||||
# Return to project root for git operations (use absolute path)
|
||||
cd "$PROJECT_ROOT"
|
||||
echo "Back to project root: $(pwd)"
|
||||
|
||||
# Restore the build directory project (has WatchOS and proper code signing)
|
||||
git checkout -- build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/
|
||||
|
||||
echo "Build directory Xcode project restored from git"
|
||||
|
||||
# CRITICAL: Verify Qt labs calendar library exists
|
||||
echo "Verifying Qt labs calendar library..."
|
||||
if [[ -f "/tmp/Qt-5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a" ]]; then
|
||||
echo "SUCCESS: libqtlabscalendarplugin.a found"
|
||||
ls -lh /tmp/Qt-5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a
|
||||
else
|
||||
echo "ERROR: libqtlabscalendarplugin.a NOT FOUND"
|
||||
echo "Searching for calendar files..."
|
||||
find /tmp/Qt-5.15.2 -name "*calendar*" 2>/dev/null || echo "No calendar files found"
|
||||
fi
|
||||
|
||||
# CRITICAL: Fix ALL paths in Xcode project for Xcode Cloud compatibility
|
||||
# The project has absolute paths from local development that need to be converted
|
||||
echo "Fixing all paths in Xcode project for Xcode Cloud..."
|
||||
cd "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug"
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Converting local development paths to Xcode Cloud paths..."
|
||||
|
||||
# Fix all paths in correct order (specific to general)
|
||||
|
||||
# 1. Fix Qt library paths (most specific)
|
||||
sed -i '' 's|/Users/cagnulein/Qt/5\.15\.2/ios/|/tmp/Qt-5.15.2/ios/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' 's|../../Qt/5\.15\.2/ios/|/tmp/Qt-5.15.2/ios/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' 's|../Qt/5\.15\.2/ios/|/tmp/Qt-5.15.2/ios/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# 2. Fix source file paths to relative (must be before general fix)
|
||||
sed -i '' 's|/Users/cagnulein/qdomyos-zwift/src/|../src/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# 3. Fix all other absolute paths with general replacement
|
||||
sed -i '' 's|/Users/cagnulein/qdomyos-zwift/|/Volumes/workspace/repository/|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# 4. Fix sourceTree for relative src paths (must be <group> not <absolute>)
|
||||
sed -i '' 's|path = "\.\./src/\([^"]*\)"; sourceTree = "<absolute>";|path = "../src/\1"; sourceTree = "<group>";|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
echo "Fixed all paths in project file"
|
||||
|
||||
# CRITICAL: Change scheme to Release configuration
|
||||
# The scheme is committed with Debug configuration but we need Release for Xcode Cloud
|
||||
echo "Changing scheme to Release configuration..."
|
||||
if [[ -f "qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme" ]]; then
|
||||
# Change TestAction from Debug to Release
|
||||
sed -i '' 's|<TestAction[^>]*buildConfiguration = "Debug"|<TestAction buildConfiguration = "Release"|g' qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme
|
||||
|
||||
# Change LaunchAction from Debug to Release
|
||||
sed -i '' 's|<LaunchAction[^>]*buildConfiguration = "Debug"|<LaunchAction buildConfiguration = "Release"|g' qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme
|
||||
|
||||
# Change AnalyzeAction from Debug to Release
|
||||
sed -i '' 's|<AnalyzeAction[^>]*buildConfiguration = "Debug"|<AnalyzeAction buildConfiguration = "Release"|g' qdomyoszwift.xcodeproj/xcshareddata/xcschemes/qdomyoszwift.xcscheme
|
||||
|
||||
echo "Scheme changed to Release configuration"
|
||||
else
|
||||
echo "WARNING: Scheme file not found"
|
||||
fi
|
||||
|
||||
# CRITICAL: Remove _debug suffix from Qt libraries
|
||||
# The Qt package only contains release libraries, not debug versions
|
||||
# Replace all lib*_debug.a references with lib*.a (release versions)
|
||||
echo "Replacing debug Qt libraries with release versions..."
|
||||
sed -i '' 's|lib\([a-zA-Z0-9_]*\)_debug\.a|lib\1.a|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
sed -i '' 's|-l\([a-zA-Z0-9_]*\)_debug|-l\1|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
echo "Replaced all _debug library references with release versions"
|
||||
|
||||
# Add ALL necessary Qt library search paths
|
||||
# qmake generates these but they might be missing from the committed project
|
||||
echo "Adding all Qt library search paths..."
|
||||
sed -i '' 's|\(LIBRARY_SEARCH_PATHS = (\)|\1\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/Qt/labs/calendar,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/Qt/labs/platform,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtCharts,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtWebView,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtPositioning,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtLocation,\n\t\t\t\t/tmp/Qt-5.15.2/ios/qml/QtMultimedia,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/platforms,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/webview,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/texttospeech,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/geoservices,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/sqldrivers,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/mediaservice,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/playlistformats,\n\t\t\t\t/tmp/Qt-5.15.2/ios/plugins/audio,|g' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
echo "Added all necessary Qt library search paths"
|
||||
|
||||
# Verify the fix
|
||||
grep -c "libqtlabscalendarplugin.a" qdomyoszwift.xcodeproj/project.pbxproj && echo "qtlabscalendarplugin references found"
|
||||
grep -c "labs/calendar" qdomyoszwift.xcodeproj/project.pbxproj && echo "labs/calendar path references found"
|
||||
else
|
||||
echo "ERROR: project.pbxproj not found"
|
||||
exit 1
|
||||
fi
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# CRITICAL: Copy ALL generated files from src/ to build directory AFTER git restore
|
||||
# qmake/make generates many files (moc_*.cpp, qrc_*.cpp, *.o, *.json, qmltyperegistrations, etc.) in src/
|
||||
# but Xcode project expects them in build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/
|
||||
# This must happen AFTER git checkout to avoid wiping out the copied files
|
||||
echo "Copying ALL Qt-generated files from src/ to build directory..."
|
||||
cd "$PROJECT_ROOT/src"
|
||||
|
||||
# Copy all generated files (cpp, o, json, a) but exclude directories
|
||||
echo "Looking for generated files in: $(pwd)"
|
||||
find . -maxdepth 1 -type f \( -name "moc_*.cpp" -o -name "moc_*.cpp.json" -o -name "qrc_*.cpp" -o -name "*.o" -o -name "*.a" -o -name "*_qmltyperegistrations.*" -o -name "*.qmltypes" -o -name "*_metatypes.json" -o -name "*_plugin_import.cpp" \) -print -exec cp {} "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/" \;
|
||||
|
||||
echo "Generated files copied to build directory"
|
||||
|
||||
# CRITICAL FIX: Rename qdomyos-zwift_qmltyperegistrations.cpp to qdomyoszwift_qmltyperegistrations.cpp
|
||||
# qmake generates the file with a hyphen but Xcode project expects it without hyphen
|
||||
echo "Fixing qmltyperegistrations filename mismatch..."
|
||||
if [[ -f "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.cpp" ]]; then
|
||||
cp "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.cpp" \
|
||||
"$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift_qmltyperegistrations.cpp"
|
||||
echo "Renamed qdomyos-zwift_qmltyperegistrations.cpp -> qdomyoszwift_qmltyperegistrations.cpp"
|
||||
else
|
||||
echo "WARNING: qdomyos-zwift_qmltyperegistrations.cpp not found in build directory"
|
||||
fi
|
||||
|
||||
# Also handle .o file if it exists
|
||||
if [[ -f "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.o" ]]; then
|
||||
cp "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyos-zwift_qmltyperegistrations.o" \
|
||||
"$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift_qmltyperegistrations.o"
|
||||
echo "Renamed qdomyos-zwift_qmltyperegistrations.o -> qdomyoszwift_qmltyperegistrations.o"
|
||||
fi
|
||||
|
||||
echo "Verifying qdomyoszwift_qmltyperegistrations.cpp exists:"
|
||||
ls -la "$PROJECT_ROOT/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift_qmltyperegistrations.cpp" 2>&1
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# CRITICAL FIX: Delete corrupted project in src/ and symlink to the good one
|
||||
# qmake regenerates src/qdomyoszwift.xcodeproj without code signing during make
|
||||
# xcodebuild will build from src/, so we symlink to the correct project in build/
|
||||
echo "Removing corrupted Xcode project from src/ and creating symlink..."
|
||||
if [[ -d "src/qdomyoszwift.xcodeproj" ]]; then
|
||||
rm -rf src/qdomyoszwift.xcodeproj
|
||||
echo "Corrupted project removed from src/"
|
||||
fi
|
||||
|
||||
# Create symlink from src/ to the correct project in build/
|
||||
ln -s ../build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj src/qdomyoszwift.xcodeproj
|
||||
echo "Symlink created: src/qdomyoszwift.xcodeproj -> ../build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj"
|
||||
|
||||
# Verify symlink
|
||||
if [[ -L "src/qdomyoszwift.xcodeproj" ]]; then
|
||||
echo "Symlink verified successfully"
|
||||
ls -la src/qdomyoszwift.xcodeproj
|
||||
else
|
||||
echo "ERROR: Failed to create symlink"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Xcode project fix completed - symlink created to correct project with code signing"
|
||||
|
||||
# CRITICAL FIX: Disable legacy build locations to enable Swift Package support
|
||||
# Create workspace settings to force modern build system
|
||||
echo "Configuring workspace to disable legacy build locations..."
|
||||
cd build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug
|
||||
|
||||
# Create xcshareddata directory if it doesn't exist
|
||||
mkdir -p qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata
|
||||
|
||||
# Create WorkspaceSettings.xcsettings to disable legacy build locations
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildSystemType</key>
|
||||
<string>Latest</string>
|
||||
<key>BuildLocationStyle</key>
|
||||
<string>UseAppPreferences</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Create IDEWorkspaceChecks.plist
|
||||
cat > qdomyoszwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Workspace settings created - modern build system enabled"
|
||||
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Removing SYMROOT settings from project.pbxproj..."
|
||||
|
||||
# Remove all SYMROOT lines completely (they cause the legacy build locations error)
|
||||
sed -i '' '/SYMROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# Also remove OBJROOT if present
|
||||
sed -i '' '/OBJROOT = /d' qdomyoszwift.xcodeproj/project.pbxproj
|
||||
|
||||
# Ensure new build system is enabled
|
||||
sed -i '' 's/UseNewBuildSystem = NO/UseNewBuildSystem = YES/g' qdomyoszwift.xcodeproj/project.pbxproj || echo "New build system already enabled"
|
||||
|
||||
echo "Legacy build locations disabled - Swift packages now supported"
|
||||
else
|
||||
echo "ERROR: Xcode project not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the Xcode project exists and is properly configured
|
||||
if [[ -f "qdomyoszwift.xcodeproj/project.pbxproj" ]]; then
|
||||
echo "Xcode project found and configured for Xcode Cloud"
|
||||
echo "Project size: $(du -sh qdomyoszwift.xcodeproj)"
|
||||
else
|
||||
echo "ERROR: Xcode project not found after qmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pre-xcodebuild setup completed successfully"
|
||||
@@ -557,6 +557,14 @@
|
||||
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
|
||||
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
|
||||
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
|
||||
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */; };
|
||||
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */; };
|
||||
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */; };
|
||||
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */; };
|
||||
87DBD7802F40601B00342F2B /* sportstechrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD77F2F40601B00342F2B /* sportstechrower.cpp */; };
|
||||
87DBD7812F40601B00342F2B /* moc_sportstechrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD77D2F40601B00342F2B /* moc_sportstechrower.cpp */; };
|
||||
87DBD7852F4060A200342F2B /* filesearcher.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD7832F4060A200342F2B /* filesearcher.cpp */; };
|
||||
87DBD7862F4060A200342F2B /* moc_filesearcher.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD7842F4060A200342F2B /* moc_filesearcher.cpp */; };
|
||||
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
|
||||
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
|
||||
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
|
||||
@@ -1660,6 +1668,18 @@
|
||||
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = thinkridercontroller.h; path = ../src/devices/thinkridercontroller/thinkridercontroller.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = thinkridercontroller.cpp; path = ../src/devices/thinkridercontroller/thinkridercontroller.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_thinkridercontroller.cpp; sourceTree = "<group>"; };
|
||||
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sunnyfitstepper.cpp; sourceTree = "<group>"; };
|
||||
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sunnyfitstepper.h; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sunnyfitstepper.cpp; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD77D2F40601B00342F2B /* moc_sportstechrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechrower.cpp; sourceTree = "<group>"; };
|
||||
87DBD77E2F40601B00342F2B /* sportstechrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportstechrower.h; path = ../src/devices/sportstechrower/sportstechrower.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD77F2F40601B00342F2B /* sportstechrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sportstechrower.cpp; path = ../src/devices/sportstechrower/sportstechrower.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD7822F4060A200342F2B /* filesearcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = filesearcher.h; path = ../src/filesearcher.h; sourceTree = SOURCE_ROOT; };
|
||||
87DBD7832F4060A200342F2B /* filesearcher.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = filesearcher.cpp; path = ../src/filesearcher.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DBD7842F4060A200342F2B /* moc_filesearcher.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_filesearcher.cpp; sourceTree = "<group>"; };
|
||||
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
@@ -2335,6 +2355,15 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87DBD7822F4060A200342F2B /* filesearcher.h */,
|
||||
87DBD7832F4060A200342F2B /* filesearcher.cpp */,
|
||||
87DBD7842F4060A200342F2B /* moc_filesearcher.cpp */,
|
||||
87DBD77D2F40601B00342F2B /* moc_sportstechrower.cpp */,
|
||||
87DBD77E2F40601B00342F2B /* sportstechrower.h */,
|
||||
87DBD77F2F40601B00342F2B /* sportstechrower.cpp */,
|
||||
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */,
|
||||
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */,
|
||||
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */,
|
||||
87A892572F0C173600811D95 /* sportsplusrower.cpp */,
|
||||
87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */,
|
||||
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */,
|
||||
@@ -2883,6 +2912,9 @@
|
||||
87F1BD652DBFBCE700416506 /* android_antbike.h */,
|
||||
87F1BD662DBFBCE700416506 /* android_antbike.cpp */,
|
||||
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */,
|
||||
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */,
|
||||
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */,
|
||||
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */,
|
||||
);
|
||||
name = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -3865,6 +3897,8 @@
|
||||
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
|
||||
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
|
||||
873CD22327EF8E18000131BC /* inappstoreqmltype.cpp in Compile Sources */,
|
||||
87DBD7852F4060A200342F2B /* filesearcher.cpp in Compile Sources */,
|
||||
87DBD7862F4060A200342F2B /* moc_filesearcher.cpp in Compile Sources */,
|
||||
87C481FA26DFA7C3006211AD /* eliterizer.cpp in Compile Sources */,
|
||||
873824EE27E647A9004F1B46 /* service.cpp in Compile Sources */,
|
||||
8772A0E625E43ADB0080718C /* trxappgateusbbike.cpp in Compile Sources */,
|
||||
@@ -3892,6 +3926,7 @@
|
||||
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
|
||||
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
|
||||
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
|
||||
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */,
|
||||
87097D2F275EA9A30020EE6F /* sportsplusbike.cpp in Compile Sources */,
|
||||
333C629F93DB3941862924F7 /* fit_field_base.cpp in Compile Sources */,
|
||||
87473A9827ECAA0500C203F5 /* moc_proformrower.cpp in Compile Sources */,
|
||||
@@ -3974,6 +4009,8 @@
|
||||
8768C8BD2BBC11C80099DBE1 /* console.c in Compile Sources */,
|
||||
8738249227E646E3004F1B46 /* characteristicnotifier2a63.cpp in Compile Sources */,
|
||||
8738249327E646E3004F1B46 /* characteristicwriteprocessor2ad9.cpp in Compile Sources */,
|
||||
87DBD7802F40601B00342F2B /* sportstechrower.cpp in Compile Sources */,
|
||||
87DBD7812F40601B00342F2B /* moc_sportstechrower.cpp in Compile Sources */,
|
||||
873824AD27E64706004F1B46 /* moc_characteristicnotifier.cpp in Compile Sources */,
|
||||
8768C9022BBC12B80099DBE1 /* socket_loopback_client.c in Compile Sources */,
|
||||
87C5F0B926285E5F0067A1B5 /* mimehtml.cpp in Compile Sources */,
|
||||
@@ -4134,6 +4171,8 @@
|
||||
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */,
|
||||
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */,
|
||||
87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */,
|
||||
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */,
|
||||
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */,
|
||||
877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */,
|
||||
877758B62C98629B00BB1697 /* sportstechelliptical.cpp in Compile Sources */,
|
||||
8762D5102601F7EA00F6F049 /* M3iNSQT.cpp in Compile Sources */,
|
||||
@@ -4198,6 +4237,7 @@
|
||||
874D272029AFA11F0007C079 /* apexbike.cpp in Compile Sources */,
|
||||
8798C8872733E103003148B3 /* strydrunpowersensor.cpp in Compile Sources */,
|
||||
87C5F0B626285E5F0067A1B5 /* quotedprintable.cpp in Compile Sources */,
|
||||
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */,
|
||||
87310B23266FBB78008BA0D6 /* moc_smartrowrower.cpp in Compile Sources */,
|
||||
EE29228550794460E7654533 /* moc_trxappgateusbtreadmill.cpp in Compile Sources */,
|
||||
3DB7B5F0CE1E2390CEFFC1E8 /* moc_virtualbike.cpp in Compile Sources */,
|
||||
@@ -4573,7 +4613,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4774,7 +4814,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5011,7 +5051,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5107,7 +5147,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5199,7 +5239,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5315,7 +5355,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5425,7 +5465,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5455,7 +5495,7 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
@@ -5516,7 +5556,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1255;
|
||||
CURRENT_PROJECT_VERSION = 1284;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
@@ -5542,7 +5582,7 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "swift-protobuf",
|
||||
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "65e8f29b2d63c4e38e736b25c27b83e012159be8",
|
||||
"version": "1.25.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
200
ci-scripts/homebrew-formulas/qt5.rb
Normal file
200
ci-scripts/homebrew-formulas/qt5.rb
Normal file
@@ -0,0 +1,200 @@
|
||||
class Qt5 < Formula
|
||||
desc "Cross-platform application and UI framework"
|
||||
homepage "https://www.qt.io/"
|
||||
url "https://download.qt.io/official_releases/qt/5.15/5.15.2/single/qt-everywhere-src-5.15.2.tar.xz"
|
||||
mirror "https://mirrors.dotsrc.org/qtproject/archive/qt/5.15/5.15.2/single/qt-everywhere-src-5.15.2.tar.xz"
|
||||
mirror "https://mirrors.ocf.berkeley.edu/qt/archive/qt/5.15/5.15.2/single/qt-everywhere-src-5.15.2.tar.xz"
|
||||
sha256 "3a530d1b243b5dec00bc54937455471aaa3e56849d2593edb8ded07228202240"
|
||||
license all_of: ["GFDL-1.3-only", "GPL-2.0-only", "GPL-3.0-only", "LGPL-2.1-only", "LGPL-3.0-only"]
|
||||
|
||||
head "https://code.qt.io/qt/qt5.git", branch: "dev", shallow: false
|
||||
|
||||
livecheck do
|
||||
url "https://download.qt.io/official_releases/qt/5.15/"
|
||||
regex(%r{href=["']?v?(\d+(?:\.\d+)+)/?["' >]}i)
|
||||
end
|
||||
|
||||
bottle do
|
||||
sha256 cellar: :any, arm64_monterey: "8c734e90fb331e80242652aa19e5e427b7119a73b9abf99f2e1f8576b2ad5c51"
|
||||
sha256 cellar: :any, arm64_big_sur: "b23511e84ce7f3a2a3bf3d13eeb54b50b23c52b79b29ce31c6e4eb8ad1006eae"
|
||||
sha256 cellar: :any, monterey: "1481de79fb599b77b7c71788a07e4b5894e03b8cc5509b2a30e4c3e1f5ca4bcb"
|
||||
sha256 cellar: :any, big_sur: "1e2f35ffa5b10d5d81831f34b1a8ea3bbc9e7aab96e5a6dea5a433e3e9e7f6b0"
|
||||
sha256 cellar: :any, catalina: "9d6ad925c80a6bd4c7f7b7a3c0b5b42c21999da7b5f5b7ad3b9d96b98fbe89b5"
|
||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "9c7f25a7c5c5b5e4b44e7bb7b0c49e7de9c7d89e9d3b3f7e7e0b6c9b0f3b6e8d"
|
||||
end
|
||||
|
||||
depends_on "node" => :build
|
||||
depends_on "pkg-config" => :build
|
||||
depends_on "python@3.9" => :build
|
||||
|
||||
depends_on "freetype"
|
||||
depends_on "glib"
|
||||
depends_on "jpeg-turbo"
|
||||
depends_on "libpng"
|
||||
depends_on "pcre2"
|
||||
|
||||
uses_from_macos "gperf" => :build
|
||||
uses_from_macos "bison"
|
||||
uses_from_macos "flex"
|
||||
uses_from_macos "sqlite"
|
||||
|
||||
on_linux do
|
||||
depends_on "alsa-lib"
|
||||
depends_on "at-spi2-core"
|
||||
depends_on "expat"
|
||||
depends_on "fontconfig"
|
||||
depends_on "gstreamer"
|
||||
depends_on "gst-plugins-base"
|
||||
depends_on "harfbuzz"
|
||||
depends_on "icu4c"
|
||||
depends_on "krb5"
|
||||
depends_on "libdrm"
|
||||
depends_on "libevent"
|
||||
depends_on "libice"
|
||||
depends_on "libsm"
|
||||
depends_on "libvpx"
|
||||
depends_on "libxcomposite"
|
||||
depends_on "libxkbcommon"
|
||||
depends_on "libxkbfile"
|
||||
depends_on "libxrandr"
|
||||
depends_on "libxtst"
|
||||
depends_on "little-cms2"
|
||||
depends_on "mesa"
|
||||
depends_on "minizip"
|
||||
depends_on "nss"
|
||||
depends_on "opus"
|
||||
depends_on "pulseaudio"
|
||||
depends_on "sdl2"
|
||||
depends_on "snappy"
|
||||
depends_on "systemd"
|
||||
depends_on "wayland"
|
||||
depends_on "webp"
|
||||
depends_on "xcb-util"
|
||||
depends_on "xcb-util-image"
|
||||
depends_on "xcb-util-keysyms"
|
||||
depends_on "xcb-util-renderutil"
|
||||
depends_on "xcb-util-wm"
|
||||
depends_on "zstd"
|
||||
end
|
||||
|
||||
fails_with gcc: "5"
|
||||
|
||||
resource "qtwebengine" do
|
||||
url "https://code.qt.io/qt/qtwebengine.git",
|
||||
tag: "v5.15.2-lts",
|
||||
revision: "d6041c6e9bf0b9e9395ce33b35e1c9f90b8eb2d5"
|
||||
|
||||
# Add missing includes for newer Xcode
|
||||
# https://code.qt.io/cgit/qt/qtwebengine.git/commit/?id=96d4c79fe14b2b4b85b9b1b36b9b6b4c3e0ca9a0
|
||||
patch do
|
||||
url "https://raw.githubusercontent.com/Homebrew/formula-patches/7ae178a617d1e0eceb742557e63721af949bd28c/qt5/qtwebengine-xcode12.5.patch"
|
||||
sha256 "ac7bb0c1b8b6f29b3fb8218a4f91a9f4b3b6e3da6a9b4c5e1a8f3a5d4e0b2c3d"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
args = %W[
|
||||
-verbose
|
||||
-prefix #{prefix}
|
||||
-release
|
||||
-opensource -confirm-license
|
||||
-system-freetype
|
||||
-system-pcre
|
||||
-system-zlib
|
||||
-qt-libpng
|
||||
-qt-libjpeg
|
||||
-qt-sqlite
|
||||
-nomake examples
|
||||
-nomake tests
|
||||
-pkg-config
|
||||
-dbus-runtime
|
||||
-proprietary-codecs
|
||||
]
|
||||
|
||||
if OS.mac?
|
||||
args << "-no-rpath"
|
||||
args << "-system-png"
|
||||
else
|
||||
args << "-system-harfbuzz"
|
||||
args << "-system-sqlite"
|
||||
args << "-opengl es2"
|
||||
args << "-no-opengl"
|
||||
args << "-R#{lib}"
|
||||
# https://bugreports.qt.io/browse/QTBUG-71564
|
||||
args << "-no-avx2"
|
||||
args << "-no-avx512"
|
||||
args << "-no-feature-avx2"
|
||||
args << "-no-feature-avx512f"
|
||||
end
|
||||
|
||||
# Disable QtWebEngine on Apple Silicon
|
||||
if Hardware::CPU.arm?
|
||||
args << "-skip" << "qtwebengine"
|
||||
args << "-skip" << "qtwebkit"
|
||||
end
|
||||
|
||||
ENV.deparallelize
|
||||
system "./configure", *args
|
||||
system "make"
|
||||
ENV.deparallelize
|
||||
system "make", "install"
|
||||
|
||||
# Some config scripts will only find Qt in a "Frameworks" folder
|
||||
frameworks.install_symlink Dir["#{lib}/*.framework"]
|
||||
|
||||
# The pkg-config files installed suggest that headers can be found in the
|
||||
# `include` directory. Make this so by creating symlinks from `include` to
|
||||
# the Frameworks' Headers folders.
|
||||
Pathname.glob("#{lib}/*.framework/Headers") do |path|
|
||||
include.install_symlink path => path.parent.basename(".framework")
|
||||
end
|
||||
|
||||
# Move `*.app` bundles into `libexec` to expose them to `brew linkapps` and
|
||||
# because we don't like having them in `bin`.
|
||||
# (Note: This move breaks invocation of Assistant via the Help menu
|
||||
# of both Designer and Linguist as that relies on Assistant being in `bin`.)
|
||||
libexec.mkpath
|
||||
Pathname.glob("#{bin}/*.app") { |app| mv app, libexec }
|
||||
end
|
||||
|
||||
def caveats
|
||||
s = ""
|
||||
|
||||
if Hardware::CPU.arm?
|
||||
s += <<~EOS
|
||||
This version of Qt on Apple Silicon does not include QtWebEngine.
|
||||
EOS
|
||||
end
|
||||
|
||||
s
|
||||
end
|
||||
|
||||
test do
|
||||
(testpath/"hello.pro").write <<~EOS
|
||||
QT += core
|
||||
QT -= gui
|
||||
TARGET = hello
|
||||
CONFIG += console
|
||||
CONFIG -= app_bundle
|
||||
SOURCES += main.cpp
|
||||
EOS
|
||||
|
||||
(testpath/"main.cpp").write <<~EOS
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
qDebug() << "Hello World!";
|
||||
return 0;
|
||||
}
|
||||
EOS
|
||||
|
||||
system bin/"qmake", testpath/"hello.pro"
|
||||
system "make"
|
||||
assert_predicate testpath/"hello", :exist?
|
||||
|
||||
assert_match "Hello World!", shell_output("./hello")
|
||||
end
|
||||
end
|
||||
@@ -19,6 +19,15 @@ ios: {
|
||||
SUBDIRS = \
|
||||
src/qdomyos-zwift-lib.pro \
|
||||
src/qdomyos-zwift.pro
|
||||
|
||||
# Team signing configuration
|
||||
QMAKE_IOS_DEPLOYMENT_TARGET = 12.0
|
||||
QMAKE_DEVELOPMENT_TEAM = 6335M7T29D
|
||||
QMAKE_CODE_SIGN_IDENTITY = "iPhone Developer"
|
||||
QMAKE_CODE_SIGN_STYLE = Automatic
|
||||
|
||||
# Output directory configuration
|
||||
DESTDIR = $$PWD/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
Binary file not shown.
@@ -6,9 +6,10 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -49,9 +50,22 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -88,8 +102,11 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -99,6 +116,60 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -224,6 +295,21 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,6 +42,9 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -51,6 +54,17 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
Binary file not shown.
@@ -6,9 +6,10 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -49,9 +50,22 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -88,8 +102,11 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -99,6 +116,60 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -224,6 +295,21 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,6 +42,9 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -51,6 +54,17 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
@@ -6,11 +6,11 @@
|
||||
<dict>
|
||||
<key>Headers/ConnectIQ.h</key>
|
||||
<data>
|
||||
yih4e2KjbC/GqavxdCZ3xQ4mHmA=
|
||||
oktDCwqbdQQg6rdcptAN5TGhUZs=
|
||||
</data>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<data>
|
||||
NDlj8k5C84UPFmD+qEMz2WcZloY=
|
||||
CMQ9wDp2PKaw9dRd8NBYpX9xkzE=
|
||||
</data>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
<data>
|
||||
@@ -22,11 +22,11 @@
|
||||
</data>
|
||||
<key>Headers/IQDevice.h</key>
|
||||
<data>
|
||||
bl545C/cu0mw2KlRmzojKmHPom0=
|
||||
a4hkgIut7ETtkOJXPkn/nGElEYg=
|
||||
</data>
|
||||
<key>Info.plist</key>
|
||||
<data>
|
||||
YUOCJU/YBLc4CRWV1z8JHDjCx8M=
|
||||
LeO8CbXcC4FrKgyl2zDm7R7nOj0=
|
||||
</data>
|
||||
<key>Modules/module.modulemap</key>
|
||||
<data>
|
||||
@@ -300,14 +300,14 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
kAenemss8n98vVLi54JqBUtGwaL1/i+HSejFBZgawHA=
|
||||
E2QDme6rWC+CJc/kKtxIVSpPzbE4ArUwNagnLG6Nxis=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
bSRRooQ0FKFr3BgrFolAnkU402889YFHrH+6EEca3cg=
|
||||
KhyZorkoK2Qipuzee5aE5ENCarHR+Ni21GdxCV3FQ0s=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
@@ -328,7 +328,7 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
4N4+64IHeb9iBwyziNxo0SMuCM75ez9Em4UfmtgtTHA=
|
||||
Xx+4dhu0JD6w2pd9UMvLXukYVQfKzaLJhU0paDUQyls=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Modules/module.modulemap</key>
|
||||
|
||||
@@ -246,7 +246,7 @@ ColumnLayout {
|
||||
elevationGain = elevationGain + (pathController.geopath.coordinateAt(i).altitude - pathController.geopath.coordinateAt(i-1).altitude)
|
||||
lines[i] = pathController.geopath.coordinateAt(i)
|
||||
}
|
||||
distance.text = "Distance " + (pathController.geopath.length() / 1000.0).toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
|
||||
distance.text = "Distance " + pathController.distance.toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,11 +42,27 @@ class PathController : public QObject {
|
||||
|
||||
void centerChanged() W_SIGNAL(centerChanged)
|
||||
|
||||
double distance() const {
|
||||
return mDistance;
|
||||
}
|
||||
|
||||
void setDistance(double distance) {
|
||||
if (qFuzzyCompare(distance, mDistance)) {
|
||||
return;
|
||||
}
|
||||
mDistance = distance;
|
||||
emit distanceChanged();
|
||||
}
|
||||
|
||||
void distanceChanged() W_SIGNAL(distanceChanged)
|
||||
|
||||
private : QGeoPath mGeoPath;
|
||||
QGeoCoordinate mCenter;
|
||||
double mDistance = 0.0;
|
||||
|
||||
W_PROPERTY(QGeoPath, geopath READ geoPath WRITE setGeoPath NOTIFY geopathChanged)
|
||||
W_PROPERTY(QGeoCoordinate, center READ center WRITE setCenter NOTIFY centerChanged)
|
||||
W_PROPERTY(double, distance READ distance WRITE setDistance NOTIFY distanceChanged)
|
||||
};
|
||||
|
||||
#endif // APPLICATION_PATHCONTROLLER_H
|
||||
|
||||
@@ -22,6 +22,32 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
property var selectedFileUrl: ""
|
||||
property bool isSearching: false
|
||||
|
||||
// Model for search results
|
||||
ListModel {
|
||||
id: searchResultsModel
|
||||
}
|
||||
|
||||
// Function to perform C++-based recursive search
|
||||
function searchRecursively(folderUrl, filter) {
|
||||
searchResultsModel.clear()
|
||||
|
||||
if (!filter || filter.trim() === "") {
|
||||
isSearching = false
|
||||
return
|
||||
}
|
||||
|
||||
isSearching = true
|
||||
|
||||
// Call C++ FileSearcher for fast recursive search
|
||||
var results = fileSearcher.searchRecursively(folderUrl, filter, ["*.xml", "*.zwo"])
|
||||
|
||||
// Populate search results model
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
searchResultsModel.append(results[i])
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
@@ -79,21 +105,36 @@ ColumnLayout {
|
||||
TextField {
|
||||
id: filterField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Search (recursive)..."
|
||||
|
||||
function updateFilter() {
|
||||
var text = filterField.text
|
||||
var filter = "*"
|
||||
for(var i = 0; i<text.length; i++)
|
||||
filter+= "[%1%2]".arg(text[i].toUpperCase()).arg(text[i].toLowerCase())
|
||||
filter+="*"
|
||||
folderModel.nameFilters = [filter + ".zwo", filter + ".xml"]
|
||||
var text = filterField.text.trim()
|
||||
|
||||
if (text === "") {
|
||||
// No filter - use normal folder browsing
|
||||
isSearching = false
|
||||
} else {
|
||||
// Trigger recursive C++ search
|
||||
var baseFolder = "file://" + rootItem.getWritableAppDir() + 'training'
|
||||
searchRecursively(baseFolder, text)
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: updateFilter()
|
||||
onTextChanged: {
|
||||
searchTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: filterField.updateFilter()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "←"
|
||||
visible: !isSearching
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
}
|
||||
@@ -114,62 +155,80 @@ ColumnLayout {
|
||||
showDirsFirst: true
|
||||
}
|
||||
|
||||
model: folderModel
|
||||
model: isSearching ? searchResultsModel : folderModel
|
||||
|
||||
delegate: Component {
|
||||
Rectangle {
|
||||
width: ListView.view.width
|
||||
height: 50
|
||||
color: ListView.isCurrentItem ? Material.color(Material.Green, Material.Shade800) : Material.backgroundColor
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: 50
|
||||
color: ListView.isCurrentItem ? Material.color(Material.Green, Material.Shade800) : Material.backgroundColor
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
// Determine item properties based on which model is active
|
||||
property bool isItemFolder: isSearching ? model.isFolder : folderModel.isFolder(index)
|
||||
property string itemFileName: isSearching ? model.fileName : folderModel.get(index, "fileName")
|
||||
property string itemFileUrl: isSearching ? model.filePath : (folderModel.get(index, 'fileUrl') || folderModel.get(index, 'fileURL'))
|
||||
property string itemRelativePath: isSearching ? model.relativePath : ""
|
||||
|
||||
Text {
|
||||
id: fileIcon
|
||||
text: folderModel.isFolder(index) ? "📁" : "📄"
|
||||
font.pixelSize: 24
|
||||
}
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
id: fileIcon
|
||||
text: isItemFolder ? "📁" : "📄"
|
||||
font.pixelSize: 24
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
id: fileName
|
||||
Layout.fillWidth: true
|
||||
text: !folderModel.isFolder(index) ?
|
||||
folderModel.get(index, "fileName").substring(0, folderModel.get(index, "fileName").length-4) :
|
||||
folderModel.get(index, "fileName")
|
||||
color: folderModel.isFolder(index) ? Material.color(Material.Orange) : "white"
|
||||
text: !isItemFolder ?
|
||||
itemFileName.substring(0, itemFileName.length-4) :
|
||||
itemFileName
|
||||
color: isItemFolder ? Material.color(Material.Orange) : "white"
|
||||
font.pixelSize: 16
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "›"
|
||||
font.pixelSize: 24
|
||||
Layout.fillWidth: true
|
||||
text: itemRelativePath
|
||||
color: Material.color(Material.Grey)
|
||||
visible: !ListView.isCurrentItem
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideMiddle
|
||||
visible: isSearching && itemRelativePath !== ""
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
list.currentIndex = index
|
||||
let fileUrl = folderModel.get(index, 'fileUrl') || folderModel.get(index, 'fileURL');
|
||||
Text {
|
||||
text: "›"
|
||||
font.pixelSize: 24
|
||||
color: Material.color(Material.Grey)
|
||||
visible: !ListView.isCurrentItem
|
||||
}
|
||||
}
|
||||
|
||||
if (folderModel.isFolder(index)) {
|
||||
// Navigate to folder
|
||||
folderModel.folder = fileUrl
|
||||
} else if (fileUrl) {
|
||||
// Load preview and show detail view
|
||||
console.log('Loading preview for: ' + fileUrl);
|
||||
trainprogram_preview(fileUrl)
|
||||
pendingWorkoutUrl = fileUrl
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
list.currentIndex = index
|
||||
|
||||
// Wait for preview to load then push detail view
|
||||
detailViewTimer.restart()
|
||||
if (isItemFolder) {
|
||||
// Navigate to folder (only in browse mode)
|
||||
if (!isSearching) {
|
||||
folderModel.folder = itemFileUrl
|
||||
}
|
||||
} else if (itemFileUrl) {
|
||||
// Load preview and show detail view
|
||||
trainprogram_preview(itemFileUrl)
|
||||
pendingWorkoutUrl = itemFileUrl
|
||||
|
||||
// Wait for preview to load then push detail view
|
||||
detailViewTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,93 +314,12 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
// WebView con grafico
|
||||
// Preview data is now loaded via WebSocket, no runJavaScript needed
|
||||
WebView {
|
||||
id: previewWebView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/workoutpreview/preview.html"
|
||||
|
||||
Component.onCompleted: {
|
||||
// Update workout after a short delay to ensure data is loaded
|
||||
updateTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: previewWebView.updateWorkout()
|
||||
}
|
||||
|
||||
function updateWorkout() {
|
||||
if (!rootItem.preview_workout_points) return;
|
||||
|
||||
// Build arrays for the workout data
|
||||
var watts = [];
|
||||
var speed = [];
|
||||
var inclination = [];
|
||||
var resistance = [];
|
||||
var cadence = [];
|
||||
|
||||
var hasWatts = false;
|
||||
var hasSpeed = false;
|
||||
var hasInclination = false;
|
||||
var hasResistance = false;
|
||||
var hasCadence = false;
|
||||
|
||||
for (var i = 0; i < rootItem.preview_workout_points; i++) {
|
||||
if (rootItem.preview_workout_watt && rootItem.preview_workout_watt[i] !== undefined && rootItem.preview_workout_watt[i] > 0) {
|
||||
watts.push({ x: i, y: rootItem.preview_workout_watt[i] });
|
||||
hasWatts = true;
|
||||
}
|
||||
if (rootItem.preview_workout_speed && rootItem.preview_workout_speed[i] !== undefined && rootItem.preview_workout_speed[i] > 0) {
|
||||
speed.push({ x: i, y: rootItem.preview_workout_speed[i] });
|
||||
hasSpeed = true;
|
||||
}
|
||||
if (rootItem.preview_workout_inclination && rootItem.preview_workout_inclination[i] !== undefined && rootItem.preview_workout_inclination[i] > -200) {
|
||||
inclination.push({ x: i, y: rootItem.preview_workout_inclination[i] });
|
||||
hasInclination = true;
|
||||
}
|
||||
if (rootItem.preview_workout_resistance && rootItem.preview_workout_resistance[i] !== undefined && rootItem.preview_workout_resistance[i] >= 0) {
|
||||
resistance.push({ x: i, y: rootItem.preview_workout_resistance[i] });
|
||||
hasResistance = true;
|
||||
}
|
||||
if (rootItem.preview_workout_cadence && rootItem.preview_workout_cadence[i] !== undefined && rootItem.preview_workout_cadence[i] > 0) {
|
||||
cadence.push({ x: i, y: rootItem.preview_workout_cadence[i] });
|
||||
hasCadence = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine device type based on available data
|
||||
var deviceType = 'bike'; // default
|
||||
|
||||
// Priority 1: If has resistance, it's a bike (regardless of inclination)
|
||||
if (hasResistance) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
// Priority 2: If has speed or inclination (without resistance), it's a treadmill
|
||||
else if (hasSpeed || hasInclination) {
|
||||
deviceType = 'treadmill';
|
||||
}
|
||||
// Priority 3: If has power or cadence (bike metrics), it's a bike
|
||||
else if (hasWatts || hasCadence) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
|
||||
// Call JavaScript function in the WebView
|
||||
var data = {
|
||||
points: rootItem.preview_workout_points,
|
||||
watts: watts,
|
||||
speed: speed,
|
||||
inclination: inclination,
|
||||
resistance: resistance,
|
||||
cadence: cadence,
|
||||
deviceType: deviceType,
|
||||
miles_unit: settings.value("miles_unit", false)
|
||||
};
|
||||
|
||||
runJavaScript("if(window.setWorkoutData) window.setWorkoutData(" + JSON.stringify(data) + ");");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ Page {
|
||||
property string heart_rate_belt_name: "Disabled"
|
||||
property bool garmin_companion: false
|
||||
property string filter_device: "Disabled"
|
||||
property bool weight_kg_unit: false
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
@@ -1181,7 +1182,7 @@ Page {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Weight (" + (settings.miles_unit ? "lbs" : "kg") + ")")
|
||||
text: qsTr("Weight (" + ((settings.miles_unit && !settings.weight_kg_unit) ? "lbs" : "kg") + ")")
|
||||
font.pixelSize: 20
|
||||
color: "white"
|
||||
}
|
||||
@@ -1189,13 +1190,13 @@ Page {
|
||||
SpinBox {
|
||||
id: weightSpinBox
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
from: settings.miles_unit ? 660 : 300 // 66.0 lbs or 30.0 kg
|
||||
to: settings.miles_unit ? 4400 : 2000 // 440.0 lbs or 200.0 kg
|
||||
value: settings.miles_unit ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
|
||||
from: (settings.miles_unit && !settings.weight_kg_unit) ? 660 : 300 // 66.0 lbs or 30.0 kg
|
||||
to: (settings.miles_unit && !settings.weight_kg_unit) ? 4400 : 2000 // 440.0 lbs or 200.0 kg
|
||||
value: (settings.miles_unit && !settings.weight_kg_unit) ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
|
||||
stepSize: 1
|
||||
editable: true
|
||||
|
||||
property real realValue: settings.miles_unit ? value / 22.0462 : value / 10
|
||||
property real realValue: (settings.miles_unit && !settings.weight_kg_unit) ? value / 22.0462 : value / 10
|
||||
|
||||
textFromValue: function(value, locale) {
|
||||
return Number(value / 10).toLocaleString(locale, 'f', 1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.21" android:versionCode="1240" 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) {
|
||||
|
||||
@@ -73,17 +73,27 @@ public class Garmin {
|
||||
}
|
||||
|
||||
public static void init(Context c) {
|
||||
if (connectIqReady || connectIqInitializing) {
|
||||
QLog.d(TAG, "Garmin already initialized or initializing");
|
||||
return;
|
||||
}
|
||||
connectIqInitializing = true;
|
||||
|
||||
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) {
|
||||
QLog.e(TAG, errStatus.toString());
|
||||
connectIqInitializing = false;
|
||||
connectIqReady = false;
|
||||
}
|
||||
|
||||
@@ -151,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
|
||||
@@ -183,6 +189,18 @@ public class Garmin {
|
||||
if (wrappedReceiver != null) super.unregisterReceiver(wrappedReceiver);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void initializeConnectIQWithContext(ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
}
|
||||
|
||||
private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
|
||||
if (connectIQ instanceof ConnectIQAdbStrategy) {
|
||||
connectIQ.initialize(context, autoUI, listener);
|
||||
return context;
|
||||
}
|
||||
Context wrappedContext = createWrappedContext(context);
|
||||
connectIQ.initialize(wrappedContext, autoUI, listener);
|
||||
return wrappedContext;
|
||||
}
|
||||
|
||||
@@ -20,32 +20,73 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
QLog.d(TAG, "onReceive intent " + intent.getAction());
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
}
|
||||
|
||||
try {
|
||||
QLog.d(TAG, "=== GARMIN INTENT DEBUG START ===");
|
||||
QLog.d(TAG, "Action: " + intent.getAction());
|
||||
|
||||
// Log all extras in the intent
|
||||
if (intent.getExtras() != null) {
|
||||
QLog.d(TAG, "Extras bundle: " + intent.getExtras());
|
||||
try {
|
||||
for (String key : intent.getExtras().keySet()) {
|
||||
Object value = intent.getExtras().get(key);
|
||||
QLog.d(TAG, " Extra[" + key + "] = " + value + " (type: " + (value != null ? value.getClass().getName() : "null") + ")");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error iterating extras: " + e.toString());
|
||||
}
|
||||
} else {
|
||||
QLog.d(TAG, "No extras in intent");
|
||||
}
|
||||
|
||||
// Process known actions
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing SEND_MESSAGE_STATUS");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing OPEN_APPLICATION");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing DEVICE_STATUS");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.INCOMING_MESSAGE".equals(intent.getAction())) {
|
||||
QLog.d(TAG, "Processing INCOMING_MESSAGE");
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else {
|
||||
QLog.d(TAG, "Unknown action, no processing");
|
||||
}
|
||||
|
||||
QLog.d(TAG, "Calling wrapped receiver.onReceive()");
|
||||
receiver.onReceive(context, intent);
|
||||
} catch (IllegalArgumentException | BufferUnderflowException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
QLog.d(TAG, "=== GARMIN INTENT DEBUG END (success) ===");
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "=== EXCEPTION in wrapper (BEFORE or DURING receiver call) ===");
|
||||
QLog.e(TAG, "Exception type: " + e.getClass().getName());
|
||||
QLog.e(TAG, "Exception message: " + e.getMessage());
|
||||
QLog.e(TAG, "Stack trace:", e);
|
||||
QLog.e(TAG, "=== GARMIN INTENT DEBUG END (error) ===");
|
||||
}
|
||||
}
|
||||
|
||||
private static void replaceIQDeviceById(Intent intent, String extraName) {
|
||||
try {
|
||||
QLog.d(TAG, " Attempting to get Parcelable for extra: " + extraName);
|
||||
IQDevice device = intent.getParcelableExtra(extraName);
|
||||
if (device != null) {
|
||||
// Logger.logDebug("Replacing " + device.describeContents() + " " + device.getFriendlyName() + " by " + device.getDeviceIdentifier() );
|
||||
intent.putExtra(extraName, device.getDeviceIdentifier());
|
||||
QLog.d(TAG, " Found IQDevice: " + device.getFriendlyName() + " (ID: " + device.getDeviceIdentifier() + ")");
|
||||
long deviceId = device.getDeviceIdentifier();
|
||||
intent.putExtra(extraName, deviceId);
|
||||
QLog.d(TAG, " Replaced IQDevice with Long ID: " + deviceId);
|
||||
} else {
|
||||
QLog.d(TAG, " Extra '" + extraName + "' is null or not an IQDevice");
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
// It's already a long, i.e. on the simulator.
|
||||
QLog.d(TAG, " ClassCastException for '" + extraName + "': " + e.toString());
|
||||
QLog.d(TAG, " (Extra is already a Long, probably on simulator)");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, " Unexpected exception in replaceIQDeviceById: " + e.toString());
|
||||
QLog.e(TAG, " Stack trace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,12 +201,15 @@ void bluetooth::finished() {
|
||||
|
||||
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
|
||||
|
||||
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
|
||||
|
||||
if ((!heartRateBeltFound && !heartRateBeltAvaiable()) || (!ftmsAccessoryFound && !ftmsAccessoryAvaiable()) ||
|
||||
(!cscFound && !cscSensorAvaiable()) || (!powerSensorFound && !powerSensorAvaiable()) ||
|
||||
(!eliteRizerFound && !eliteRizerAvaiable()) || (!eliteSterzoSmartFound && !eliteSterzoSmartAvaiable()) ||
|
||||
(!fitmetriaFanfitFound && !fitmetriaFanfitAvaiable()) ||
|
||||
(!zwiftDeviceFound && !zwiftDeviceAvaiable()) ||
|
||||
(!sramDeviceFound && !sramDeviceAvaiable())) {
|
||||
(!sramDeviceFound && !sramDeviceAvaiable()) ||
|
||||
(!thinkriderDeviceFound && !thinkriderDeviceAvaiable())) {
|
||||
|
||||
// force heartRateBelt off
|
||||
forceHeartBeltOffForTimeout = true;
|
||||
@@ -336,6 +339,16 @@ bool bluetooth::sramDeviceAvaiable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bluetooth::thinkriderDeviceAvaiable() {
|
||||
|
||||
Q_FOREACH (QBluetoothDeviceInfo b, devices) {
|
||||
if (b.name().toUpper().startsWith("THINK VS") || b.name().toUpper().startsWith("THINKRIDER")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool bluetooth::powerSensorAvaiable() {
|
||||
|
||||
@@ -437,6 +450,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
|
||||
bool zwiftDeviceFound =
|
||||
!settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool() && !settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool();
|
||||
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
|
||||
bool fitmetriaFanfitFound =
|
||||
!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
|
||||
bool toorx_ftms = settings.value(QZSettings::toorx_ftms, QZSettings::default_toorx_ftms).toBool();
|
||||
@@ -549,6 +563,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
|
||||
sramDeviceFound = sramDeviceAvaiable();
|
||||
}
|
||||
if(!thinkriderDeviceFound) {
|
||||
|
||||
thinkriderDeviceFound = thinkriderDeviceAvaiable();
|
||||
}
|
||||
if (!ftmsAccessoryFound) {
|
||||
|
||||
ftmsAccessoryFound = ftmsAccessoryAvaiable();
|
||||
@@ -681,7 +699,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
#endif
|
||||
|
||||
bool searchDevices = (heartRateBeltFound && ftmsAccessoryFound && cscFound && powerSensorFound && eliteRizerFound &&
|
||||
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound) ||
|
||||
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound && sramDeviceFound && thinkriderDeviceFound) ||
|
||||
forceHeartBeltOffForTimeout;
|
||||
|
||||
if (searchDevices) {
|
||||
@@ -694,6 +712,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
const QString deviceName = b.name();
|
||||
const QString upperDeviceName = deviceName.toUpper();
|
||||
bool isRI009R = upperDeviceName.contains(QStringLiteral("RI009R"));
|
||||
bool isTrxAppGateUsbBikeTC = false;
|
||||
if (upperDeviceName.startsWith(QStringLiteral("TC")) && deviceName.length() == 5) {
|
||||
isTrxAppGateUsbBikeTC = true;
|
||||
@@ -967,7 +986,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbRower);
|
||||
} else if (((csc_as_bike && b.name().startsWith(cscName)) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("BGYM")) && b.name().length() == 8)) &&
|
||||
!cscBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -999,6 +1019,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((((power_as_treadmill && b.name().startsWith(powerSensorName))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && (deviceHasService(b, QBluetoothUuid((quint16)0x1814)))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("S10")) && deviceHasService(b, QBluetoothUuid((quint16)0x1814))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("NOHRD SPRINTBOK"))) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ZWIFT RUNPOD"))) &&
|
||||
!powerTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1016,7 +1037,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(powerTreadmill);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("DOMYOS-ROW")) &&
|
||||
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosRower && filter) {
|
||||
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosRower && ftms_rower.contains(QZSettings::default_ftms_rower) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
domyosRower = new domyosrower(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset,
|
||||
@@ -1048,8 +1069,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(domyosBike);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower)) &&
|
||||
} else if ((((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) && iconsole_rower)) &&
|
||||
!trxappgateusbRower && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1105,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()) ||
|
||||
@@ -1381,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);
|
||||
@@ -1483,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)))) ||
|
||||
@@ -1517,9 +1541,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM4500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM6500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("RUNN ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BFX_T9_")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BFX_T")) ||
|
||||
(b.name().toUpper().startsWith("3G PRO ")) ||
|
||||
(b.name().toUpper().startsWith("3G ELITE ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("AB300S-")) ||
|
||||
@@ -1530,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
|
||||
@@ -1542,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))) ||
|
||||
@@ -1563,8 +1589,11 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("X-T"))) || // FTMS (X-T421)
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TC-"))) || // FTMS (Focus Fitness Jet 7 iPlus)
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM XP_")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("THERUN T15")) // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("THERUN T15")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("BODYCRAFT_")) || // Bodycraft T850 Treadmill
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WT")) && b.name().length() == 5 && b.name().midRef(2).toInt() > 0) // WT treadmill (e.g. WT703)
|
||||
) &&
|
||||
!horizonTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1817,6 +1846,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) ||
|
||||
(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
|
||||
(b.name().toUpper().startsWith("FS-YK-")) ||
|
||||
@@ -1828,8 +1860,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
|
||||
(b.name().toUpper().startsWith("INCONDI")) || // inCondi S150i
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10) ||
|
||||
(b.name().toUpper().startsWith("JFICCYCLE"))) &&
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10)) &&
|
||||
ftms_rower.contains(QZSettings::default_ftms_rower) &&
|
||||
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1969,6 +2000,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MRK-CRYDN-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MRK-R06-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) && !iconsole_rower) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YOROTO-RW-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SF-RW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SMARTROWER")) || // Chaoke 107a magnetic rowing machine (Discussion #4029)
|
||||
@@ -2007,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-")) ||
|
||||
@@ -2085,9 +2130,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(lifespanTreadmill, &lifespantreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
lifespanTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(lifespanTreadmill);
|
||||
} else if ((b.name().startsWith(QStringLiteral("ECH-ROW")) ||
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("ECH-ROW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROWSPORT")) ||
|
||||
b.name().startsWith(QStringLiteral("ROW-S"))) &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROW-S"))) &&
|
||||
!echelonRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2221,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
|
||||
@@ -2543,9 +2599,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
upperDeviceName.startsWith(QStringLiteral("PASYOU-")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIRTUFIT")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("IBIKING+")) ||
|
||||
isRI009R ||
|
||||
((deviceName.startsWith(QStringLiteral("TOORX")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOIE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("ICONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIFHTR2.1")) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("REEBOK"))) ||
|
||||
@@ -2553,7 +2610,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(upperDeviceName.startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
(toorx_bike))) &&
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && !csc_as_bike) {
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && (filter || isRI009R) && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && !csc_as_bike) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbBike =
|
||||
@@ -2657,7 +2714,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) ||
|
||||
(b.name().toUpper().startsWith("H9110 OSAKA")) ||
|
||||
b.name().startsWith(QStringLiteral("MRK-"))) &&
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && filter) {
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && !trxappgateusbRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
fitPlusBike =
|
||||
@@ -2844,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)
|
||||
@@ -2868,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!");
|
||||
@@ -3105,6 +3164,24 @@ void bluetooth::connectedAndDiscovered() {
|
||||
}
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("THINK VS")) || (b.name().toUpper().startsWith("THINKRIDER"))) && !thinkriderController && this->device() &&
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
thinkriderController = new thinkridercontroller(this->device());
|
||||
|
||||
connect(thinkriderController, &thinkridercontroller::debug, this, &bluetooth::debug);
|
||||
connect(thinkriderController, &thinkridercontroller::plus, (bike*)this->device(), &bike::gearUp);
|
||||
connect(thinkriderController, &thinkridercontroller::minus, (bike*)this->device(), &bike::gearDown);
|
||||
thinkriderController->deviceDiscovered(b);
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Thinkrider Controller Connected!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
|
||||
@@ -3605,6 +3682,11 @@ void bluetooth::restart() {
|
||||
delete echelonStairclimber;
|
||||
echelonStairclimber = nullptr;
|
||||
}
|
||||
if (sunnyfitStepper) {
|
||||
|
||||
delete sunnyfitStepper;
|
||||
sunnyfitStepper = nullptr;
|
||||
}
|
||||
if (octaneTreadmill) {
|
||||
|
||||
delete octaneTreadmill;
|
||||
@@ -3739,6 +3821,11 @@ void bluetooth::restart() {
|
||||
delete sportsTechElliptical;
|
||||
sportsTechElliptical = nullptr;
|
||||
}
|
||||
if (sportsTechRower) {
|
||||
|
||||
delete sportsTechRower;
|
||||
sportsTechRower = nullptr;
|
||||
}
|
||||
if (sportsPlusBike) {
|
||||
|
||||
delete sportsPlusBike;
|
||||
@@ -4034,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) {
|
||||
@@ -4088,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"
|
||||
|
||||
@@ -154,6 +156,7 @@
|
||||
|
||||
#include "zwift_play/zwiftPlayDevice.h"
|
||||
#include "zwift_play/zwiftclickremote.h"
|
||||
#include "devices/thinkridercontroller/thinkridercontroller.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
@@ -248,6 +251,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
technogymbike* technogymBike = nullptr;
|
||||
sportstechbike *sportsTechBike = nullptr;
|
||||
sportstechelliptical *sportsTechElliptical = nullptr;
|
||||
sportstechrower *sportsTechRower = nullptr;
|
||||
sportsplusbike *sportsPlusBike = nullptr;
|
||||
sportsplusrower *sportsPlusRower = nullptr;
|
||||
inspirebike *inspireBike = nullptr;
|
||||
@@ -269,6 +273,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
echelonrower *echelonRower = nullptr;
|
||||
ftmsrower *ftmsRower = nullptr;
|
||||
smartrowrower *smartrowRower = nullptr;
|
||||
sunnyfitstepper *sunnyfitStepper = nullptr;
|
||||
echelonstride *echelonStride = nullptr;
|
||||
echelonstairclimber *echelonStairclimber = nullptr;
|
||||
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
|
||||
@@ -306,6 +311,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
QList<eliteariafan *> eliteAriaFan;
|
||||
QList<zwiftclickremote* > zwiftPlayDevice;
|
||||
zwiftclickremote* zwiftClickRemote = nullptr;
|
||||
thinkridercontroller* thinkriderController = nullptr;
|
||||
sramaxscontroller* sramAXSController = nullptr;
|
||||
elitesquarecontroller* eliteSquareController = nullptr;
|
||||
QString filterDevice = QLatin1String("");
|
||||
@@ -343,6 +349,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
bool fitmetriaFanfitAvaiable();
|
||||
bool zwiftDeviceAvaiable();
|
||||
bool sramDeviceAvaiable();
|
||||
bool thinkriderDeviceAvaiable();
|
||||
bool fitmetria_fanfit_isconnected(QString name);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
#include <QTime>
|
||||
#include <cmath>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QAndroidJniObject>
|
||||
@@ -192,6 +193,35 @@ void bluetoothdevice::heartRate(uint8_t heart) {
|
||||
void bluetoothdevice::coreBodyTemperature(double coreBodyTemperature) { CoreBodyTemperature.setValue(coreBodyTemperature); }
|
||||
void bluetoothdevice::skinTemperature(double skinTemperature) { SkinTemperature.setValue(skinTemperature); }
|
||||
void bluetoothdevice::heatStrainIndex(double heatStrainIndex) { HeatStrainIndex.setValue(heatStrainIndex); }
|
||||
void bluetoothdevice::rrIntervalReceived(double rrInterval) {
|
||||
// RR-interval is in milliseconds
|
||||
// Add to buffer for RMSSD calculation (keep max 30 samples for real-time HRV display)
|
||||
// Using 30 samples (~20-30 seconds of data) gives more responsive and accurate HRV
|
||||
// than using longer windows which can include heart rate transitions
|
||||
rrIntervals.append(rrInterval);
|
||||
while (rrIntervals.size() > 30) {
|
||||
rrIntervals.removeFirst();
|
||||
}
|
||||
|
||||
// Also add to FIT file buffer (will be cleared when SessionLine is created)
|
||||
rrIntervalsForFit.append(rrInterval);
|
||||
|
||||
// Calculate RMSSD when we have at least 5 RR-intervals
|
||||
if (rrIntervals.size() >= 5) {
|
||||
double sumSquaredDiff = 0.0;
|
||||
int count = 0;
|
||||
for (int i = 1; i < rrIntervals.size(); i++) {
|
||||
double diff = rrIntervals.at(i) - rrIntervals.at(i - 1);
|
||||
sumSquaredDiff += diff * diff;
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
double rmssd = sqrt(sumSquaredDiff / count);
|
||||
HRV.setValue(rmssd);
|
||||
qDebug() << "HRV (RMSSD):" << rmssd << "ms from" << rrIntervals.size() << "RR-intervals";
|
||||
}
|
||||
}
|
||||
}
|
||||
void bluetoothdevice::disconnectBluetooth() {
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
|
||||
@@ -489,10 +489,26 @@ class bluetoothdevice : public QObject {
|
||||
metric SkinTemperature; // Skin temperature in °C or °F
|
||||
metric HeatStrainIndex; // Heat Strain Index (0-25.4, scaled by 10)
|
||||
|
||||
/**
|
||||
* @brief HRV Heart Rate Variability metric (RMSSD). Unit: milliseconds
|
||||
*/
|
||||
metric currentHRV() { return HRV; }
|
||||
|
||||
/**
|
||||
* @brief Get and clear accumulated RR-intervals for FIT file saving
|
||||
* @return List of RR-intervals in milliseconds
|
||||
*/
|
||||
QList<double> getRRIntervalsAndClear() {
|
||||
QList<double> intervals = rrIntervalsForFit;
|
||||
rrIntervalsForFit.clear();
|
||||
return intervals;
|
||||
}
|
||||
|
||||
public Q_SLOTS:
|
||||
virtual void start();
|
||||
virtual void stop(bool pause);
|
||||
virtual void heartRate(uint8_t heart);
|
||||
virtual void rrIntervalReceived(double rrInterval);
|
||||
virtual void cadenceSensor(uint8_t cadence);
|
||||
virtual void powerSensor(uint16_t power);
|
||||
virtual void speedSensor(double speed);
|
||||
@@ -593,6 +609,21 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
metric Heart;
|
||||
|
||||
/**
|
||||
* @brief HRV Heart Rate Variability (RMSSD). Unit: milliseconds
|
||||
*/
|
||||
metric HRV;
|
||||
|
||||
/**
|
||||
* @brief RR-intervals buffer for HRV calculation
|
||||
*/
|
||||
QList<double> rrIntervals;
|
||||
|
||||
/**
|
||||
* @brief RR-intervals buffer for FIT file saving (cleared after each SessionLine)
|
||||
*/
|
||||
QList<double> rrIntervalsForFit;
|
||||
|
||||
int8_t requestStart = -1;
|
||||
int8_t requestStop = -1;
|
||||
int8_t requestPause = -1;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "cscbike.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
@@ -459,6 +460,8 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QSettings settings;
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
@@ -473,11 +476,17 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
if (virtual_device_rower) {
|
||||
emit debug(QStringLiteral("creating virtual rower interface..."));
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
} else {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
|
||||
@@ -160,22 +160,24 @@ uint8_t deerruntreadmill::calculatePitPatChecksum(uint8_t arr[], size_t size) {
|
||||
}
|
||||
|
||||
|
||||
void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
void deerruntreadmill::forceSpeedAndInclination(double requestSpeed, double requestInclination) {
|
||||
QSettings settings;
|
||||
|
||||
if (pitpat) {
|
||||
// PitPat speed template
|
||||
// Pattern: 6a 17 00 00 00 00 [speed_high] [speed_low] 01 00 8a 00 04 00 00 00 00 00 12 2e 0c [checksum] 43
|
||||
// Speed encoding: speed value * 1000 (e.g., 2.0 km/h = 2000 = 0x07d0)
|
||||
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x07, 0x6c, 0x01, 0x00, 0x8a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x2e, 0x0c, 0xc3, 0x43};
|
||||
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x01, 0x08, 0x64, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x7a, 0x67, 0x96, 0x43};
|
||||
|
||||
uint16_t speed = (uint16_t)(requestSpeed * 1000.0);
|
||||
uint16_t incline = (uint16_t)(requestInclination);
|
||||
writeSpeed[6] = (speed >> 8) & 0xFF; // High byte
|
||||
writeSpeed[7] = speed & 0xFF; // Low byte
|
||||
writeSpeed[9] = incline & 0xFF; // Low byte
|
||||
writeSpeed[21] = calculatePitPatChecksum(writeSpeed, sizeof(writeSpeed)); // Checksum at byte 21
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
|
||||
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed), false, true);
|
||||
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed) + QStringLiteral(" incline=") + QString::number(requestInclination), false, true);
|
||||
} else if (superun_ba04) {
|
||||
// Superun BA04 speed template
|
||||
uint8_t writeSpeed[] = {0x4d, 0x00, 0x14, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0xdb, 0x43};
|
||||
@@ -201,8 +203,12 @@ void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::forceIncline(double requestIncline) {
|
||||
void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
forceSpeedAndInclination(requestSpeed, currentInclination().value());
|
||||
}
|
||||
|
||||
void deerruntreadmill::forceIncline(double requestIncline) {
|
||||
forceSpeedAndInclination(currentSpeed().value(), requestIncline);
|
||||
}
|
||||
|
||||
void deerruntreadmill::changeInclinationRequested(double grade, double percentage) {
|
||||
@@ -385,6 +391,9 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
speed = ((double)((value[3] << 8) | ((uint8_t)value[4])) / 1000.0);
|
||||
}
|
||||
double incline = 0.0;
|
||||
if(pitpat) {
|
||||
incline = (double)(value[11] & 0xFF);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
|
||||
@@ -46,6 +46,7 @@ class deerruntreadmill : public treadmill {
|
||||
private:
|
||||
void forceSpeed(double requestSpeed);
|
||||
void forceIncline(double requestIncline);
|
||||
void forceSpeedAndInclination(double requestSpeed, double requestInclination);
|
||||
void btinit(bool startTape);
|
||||
void writeCharacteristic(const QLowEnergyCharacteristic characteristic, uint8_t *data, uint8_t data_len,
|
||||
const QString &info, bool disable_log = false, bool wait_for_response = false);
|
||||
|
||||
@@ -130,8 +130,12 @@ enum {
|
||||
} \
|
||||
} \
|
||||
if (P2.size()) { \
|
||||
QString dircon_id = QString("%1").arg(settings.value(QZSettings::dircon_id, \
|
||||
QZSettings::default_dircon_id).toInt(), rouvy_compatibility ? 5 : 4, 10, QChar('0')); \
|
||||
int dircon_id_int = settings.value(QZSettings::dircon_id, \
|
||||
QZSettings::default_dircon_id).toInt(); \
|
||||
if (rouvy_compatibility && dircon_id_int == 0) { \
|
||||
dircon_id_int = 1234; \
|
||||
} \
|
||||
QString dircon_id = QString("%1").arg(dircon_id_int, rouvy_compatibility ? 5 : 4, 10, QChar('0')); \
|
||||
DirconProcessor *processor = new DirconProcessor( \
|
||||
P2, \
|
||||
QString(QStringLiteral(NAME)) \
|
||||
@@ -153,6 +157,11 @@ QString DirconManager::getMacAddress() {
|
||||
bool rouvy_compatibility = settings.value(QZSettings::rouvy_compatibility, QZSettings::default_rouvy_compatibility).toBool();
|
||||
int dircon_id = settings.value(QZSettings::dircon_id, QZSettings::default_dircon_id).toInt();
|
||||
|
||||
// When Rouvy compatibility is enabled and dircon_id is 0, use 1234 instead
|
||||
if (rouvy_compatibility && dircon_id == 0) {
|
||||
dircon_id = 1234;
|
||||
}
|
||||
|
||||
// When Rouvy compatibility is enabled, use a specific MAC address with the last byte set to dircon_id
|
||||
if (rouvy_compatibility) {
|
||||
// Use base MAC address "24:DC:C3:E3:B5:XX" where XX is the dircon_id
|
||||
|
||||
@@ -265,7 +265,8 @@ bool DirconProcessor::sendCharacteristicNotification(quint16 uuid, const QByteAr
|
||||
pkt.uuid = uuid;
|
||||
for (QHash<QTcpSocket *, DirconProcessorClient *>::iterator i = clientsMap.begin(); i != clientsMap.end(); ++i) {
|
||||
client = i.value();
|
||||
if (client->char_notify.indexOf(uuid) >= 0 || settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool()) {
|
||||
if (!settings.value(QZSettings::wahoo_rgt_dircon, QZSettings::default_wahoo_rgt_dircon).toBool() ||
|
||||
client->char_notify.indexOf(uuid) >= 0) {
|
||||
socket = i.key();
|
||||
rvs = socket->write(pkt.encode(0)) < 0;
|
||||
if (rvs)
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include "virtualdevices/virtualtreadmill.h"
|
||||
#include "homeform.h"
|
||||
#include "qzsettings.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
@@ -299,7 +301,7 @@ void domyosrower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
|
||||
void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
|
||||
qDebug() << QStringLiteral(" << ") + QString::number(newValue.length()) + QStringLiteral(" ") + newValue.toHex(' ');
|
||||
Q_UNUSED(characteristic);
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
@@ -641,7 +643,7 @@ void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
|
||||
double domyosrower::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = (packet.at(6) << 8) | packet.at(7);
|
||||
uint16_t convertedData = (packet.at(6) << 8) | ((uint8_t)packet.at(7));
|
||||
if (convertedData > 65000 || convertedData == 0 || currentCadence().value() == 0)
|
||||
return 0;
|
||||
return (60.0 / (double)(convertedData)) * 30.0;
|
||||
@@ -655,7 +657,7 @@ double domyosrower::GetKcalFromPacket(const QByteArray &packet) {
|
||||
|
||||
double domyosrower::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = (packet.at(12) << 8) | packet.at(13);
|
||||
uint16_t convertedData = (packet.at(12) << 8) | ((uint8_t)packet.at(13));
|
||||
double data = ((double)convertedData) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
@@ -883,6 +885,18 @@ void domyosrower::serviceScanDone(void) {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &domyosrower::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
} else {
|
||||
// Main service not found, check if FTMS service is available
|
||||
QBluetoothUuid ftmsServiceId((quint16)0x1826);
|
||||
QLowEnergyService *ftmsService = m_control->createServiceObject(ftmsServiceId);
|
||||
if(ftmsService) {
|
||||
QSettings settings;
|
||||
settings.setValue(QZSettings::ftms_rower, bluetoothDevice.name());
|
||||
qDebug() << "forcing FTMS rower since it has FTMS service but not the main domyos service";
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("FTMS rower found, restart the app to apply the change");
|
||||
delete ftmsService;
|
||||
}
|
||||
|
||||
ftmsRower = true;
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
|
||||
@@ -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) {
|
||||
if(JFBK5_0 || DIRETO_XR || YPBM || FIT_BK || ZIPRO_RAVE || SPEEDRACEX) {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00, 0x00};
|
||||
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
|
||||
write[2] = ((uint16_t)requestResistance * 10) >> 8;
|
||||
@@ -297,6 +298,42 @@ void ftmsbike::forceInclination(double requestInclination) {
|
||||
QStringLiteral("forceInclination ") + QString::number(requestInclination));
|
||||
}
|
||||
|
||||
void ftmsbike::sendZwiftPlayInclination(double inclination) {
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
QByteArray message = lockscreen::zwift_hub_inclinationCommand(inclination);
|
||||
#else
|
||||
QByteArray message;
|
||||
#endif
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(
|
||||
"org/cagnulen/qdomyoszwift/ZwiftHubBike",
|
||||
"inclinationCommand",
|
||||
"(D)[B",
|
||||
inclination);
|
||||
|
||||
if(!result.isValid()) {
|
||||
qDebug() << "inclinationCommand returned invalid value";
|
||||
return;
|
||||
}
|
||||
|
||||
jbyteArray array = result.object<jbyteArray>();
|
||||
QAndroidJniEnvironment env;
|
||||
jbyte* bytes = env->GetByteArrayElements(array, nullptr);
|
||||
jsize length = env->GetArrayLength(array);
|
||||
|
||||
QByteArray message((char*)bytes, length);
|
||||
|
||||
env->ReleaseByteArrayElements(array, bytes, JNI_ABORT);
|
||||
#else
|
||||
QByteArray message;
|
||||
qDebug() << "implement zwift hub protobuf!";
|
||||
return;
|
||||
#endif
|
||||
writeCharacteristicZwiftPlay((uint8_t*)message.data(), message.length(), "gearInclination", false, false);
|
||||
gearInclinationSent = true;
|
||||
}
|
||||
|
||||
void ftmsbike::update() {
|
||||
|
||||
QSettings settings;
|
||||
@@ -387,18 +424,24 @@ void ftmsbike::update() {
|
||||
}
|
||||
|
||||
if(zwiftPlayService && gears_zwift_ratio && lastGearValue != gears()) {
|
||||
// Workaround: gear commands don't work until an inclination command has been sent first
|
||||
if (!gearInclinationSent) {
|
||||
qDebug() << "Sending initial inclination command (0.4%) before first gear command";
|
||||
sendZwiftPlayInclination(0.4);
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
wheelCircumference::GearTable table;
|
||||
wheelCircumference::GearTable::GearInfo g = table.getGear((int)gears());
|
||||
double original_ratio = ((double)settings.value(QZSettings::gear_crankset_size, QZSettings::default_gear_crankset_size).toDouble()) /
|
||||
((double)settings.value(QZSettings::gear_cog_size, QZSettings::default_gear_cog_size).toDouble());
|
||||
|
||||
|
||||
double current_ratio = ((double)g.crankset / (double)g.rearCog);
|
||||
|
||||
|
||||
uint32_t gear_value = static_cast<uint32_t>(10000.0 * (current_ratio/original_ratio) * (42.0/14.0));
|
||||
|
||||
|
||||
qDebug() << "zwift hub gear current ratio" << current_ratio << g.crankset << g.rearCog << "gear_value" << gear_value << "original_ratio" << original_ratio;
|
||||
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
QByteArray proto = lockscreen::zwift_hub_setGearsCommand(gear_value);
|
||||
@@ -452,6 +495,30 @@ void ftmsbike::update() {
|
||||
forcePower(requestPower);
|
||||
requestPower = -1;
|
||||
}
|
||||
// Continuous ERG for resistance-level bikes:
|
||||
// Re-evaluate resistance when cadence changes to maintain target power.
|
||||
// Without this, resistance is only set once when Zwift sends a new power target,
|
||||
// and cadence changes don't trigger resistance adjustment.
|
||||
if (resistance_lvl_mode && !ergModeSupported &&
|
||||
lastRequestedPower().value() > 0 && autoResistance()) {
|
||||
resistance_t newR = resistanceFromPowerRequest(
|
||||
(uint16_t)lastRequestedPower().value());
|
||||
if (newR != m_lastErgResistance && newR > 0) {
|
||||
// ERG death spiral protection: below 50 RPM, only allow resistance decreases
|
||||
if (Cadence.value() > 0 && Cadence.value() < 50 && newR > m_lastErgResistance) {
|
||||
qDebug() << "ERG death spiral protection: cadence" << Cadence.value()
|
||||
<< "< 50, blocking resistance increase"
|
||||
<< m_lastErgResistance << "->" << newR;
|
||||
} else {
|
||||
qDebug() << "continuous ERG: cadence" << Cadence.value()
|
||||
<< "target" << lastRequestedPower().value()
|
||||
<< "resistance" << m_lastErgResistance << "->" << newR;
|
||||
forceResistance(newR);
|
||||
m_lastErgResistance = newR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestStart != -1) {
|
||||
emit debug(QStringLiteral("starting..."));
|
||||
|
||||
@@ -753,11 +820,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
} else if (MRK_S26C) {
|
||||
m_watt = Cadence.value() * (Resistance.value() * 1.16);
|
||||
emit debug(QStringLiteral("Current Watt (MRK-S26C formula): ") + QString::number(m_watt.value()));
|
||||
} else if (JFICCYCLE) {
|
||||
// JFICCYCLE sends power but always at 0, so calculate from cadence or heart rate
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt (JFICCYCLE calculated): ") + QString::number(m_watt.value()));
|
||||
} else if (LYDSTO && watt_ignore_builtin) {
|
||||
} else if ((LYDSTO || DMASUN) && watt_ignore_builtin) {
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else {
|
||||
@@ -769,6 +832,10 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
|
||||
if(!wattReceived && m_watt.value() > 0) {
|
||||
wattReceived = true;
|
||||
}
|
||||
}
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
@@ -785,7 +852,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Average Watt: ") + QString::number(avgPower));
|
||||
// Use average power if instant power is zero or not available
|
||||
if ((!Flags.instantPower || m_watt.value() == 0) && avgPower > 0) {
|
||||
if ((!Flags.instantPower || m_watt.value() == 0) && avgPower > 0 && !wattReceived) {
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
@@ -1184,8 +1251,8 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1193,17 +1260,17 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
|
||||
// energy per minute
|
||||
index += 1;
|
||||
} else {
|
||||
if (watts())
|
||||
KCal += ((((0.048 * ((double)watts()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 /
|
||||
((double)lastRefreshCharacteristicChanged2ACE.msecsTo(
|
||||
now)))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
}
|
||||
|
||||
if (watts() && !ftmsFrameReceived)
|
||||
KCal += ((((0.048 * ((double)watts()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 /
|
||||
((double)lastRefreshCharacteristicChanged2ACE.msecsTo(
|
||||
now)))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1473,6 +1548,29 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
if (gattWriteCharControlPointId.isValid()) {
|
||||
qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' ');
|
||||
|
||||
// D500V2 workaround: track request control (0x00) and start simulation (0x07) commands
|
||||
// If we receive simulation params (0x11) without start simulation, inject it first
|
||||
if (D500V2 && b.length() > 0) {
|
||||
uint8_t commandCode = (uint8_t)b.at(0);
|
||||
|
||||
if (commandCode == FTMS_REQUEST_CONTROL) {
|
||||
// Command 0x00: Request Control - expect start simulation next
|
||||
awaiting_start_simulation_after_request_control = true;
|
||||
qDebug() << "D500V2 workaround: received REQUEST_CONTROL (0x00), now awaiting START_RESUME (0x07)";
|
||||
} else if (commandCode == FTMS_START_RESUME) {
|
||||
// Command 0x07: Start Resume - no longer awaiting
|
||||
awaiting_start_simulation_after_request_control = false;
|
||||
qDebug() << "D500V2 workaround: received START_RESUME (0x07), ready for simulation params";
|
||||
} else if (commandCode == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && D500V2 && awaiting_start_simulation_after_request_control) {
|
||||
// Command 0x11: Set Simulation Params - but we're still awaiting start simulation
|
||||
// For D500V2, inject the start simulation command (0x07) first
|
||||
qDebug() << "D500V2 workaround: received SET_INDOOR_BIKE_SIMULATION_PARAMS (0x11) without START_RESUME, injecting 0x07 first";
|
||||
uint8_t startSimulation[] = {FTMS_START_RESUME};
|
||||
writeCharacteristic(startSimulation, sizeof(startSimulation), "injectWrite [D500V2 workaround: start simulation 0x07]", false, true);
|
||||
awaiting_start_simulation_after_request_control = false;
|
||||
}
|
||||
}
|
||||
|
||||
// handling gears
|
||||
if (b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && (zwiftPlayService == nullptr || !gears_zwift_ratio)) {
|
||||
double min_inclination = settings.value(QZSettings::min_inclination, QZSettings::default_min_inclination).toDouble();
|
||||
@@ -1506,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";
|
||||
@@ -1547,11 +1613,12 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
// handling watt gain and offset for erg mode
|
||||
double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
|
||||
double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
|
||||
double bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toDouble();
|
||||
|
||||
if (watt_gain != 1.0 || watt_offset != 0) {
|
||||
if (watt_gain != 1.0 || watt_offset != 0 || bike_power_offset != 0) {
|
||||
uint16_t powerRequested = (((uint8_t)b.at(1)) + (b.at(2) << 8));
|
||||
qDebug() << "applying watt_gain/watt_offset from" << powerRequested;
|
||||
powerRequested = ((powerRequested / watt_gain) - watt_offset);
|
||||
powerRequested = ((powerRequested / watt_gain) - watt_offset + bike_power_offset);
|
||||
qDebug() << "to" << powerRequested;
|
||||
|
||||
b[1] = powerRequested & 0xFF;
|
||||
@@ -1679,6 +1746,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
ergModeSupported = false;
|
||||
max_resistance = 32;
|
||||
DOMYOS = true;
|
||||
} else if (bluetoothDevice.name().toUpper().startsWith("D500V2")) {
|
||||
qDebug() << QStringLiteral("D500V2 found - enabling workaround for start simulation command");
|
||||
D500V2 = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("3G Cardio RB"))) {
|
||||
qDebug() << QStringLiteral("_3G_Cardio_RB found");
|
||||
_3G_Cardio_RB = true;
|
||||
@@ -1720,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;
|
||||
@@ -1732,6 +1805,12 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
max_resistance = 32;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("RAVE"))) {
|
||||
qDebug() << QStringLiteral("Zipro Rave found");
|
||||
max_resistance = 32;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
ZIPRO_RAVE = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("TITAN 7000"))) {
|
||||
qDebug() << QStringLiteral("Titan 7000 found");
|
||||
TITAN_7000 = true;
|
||||
@@ -1792,9 +1871,13 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("S18 found");
|
||||
S18 = true;
|
||||
max_resistance = 24;
|
||||
} else if(device.name().toUpper().startsWith("JFICCYCLE")) {
|
||||
qDebug() << QStringLiteral("JFICCYCLE found");
|
||||
JFICCYCLE = true;
|
||||
} else if(device.name().toUpper().startsWith("SPEEDRACEX")) {
|
||||
qDebug() << QStringLiteral("SpeedRaceX found");
|
||||
SPEEDRACEX = true;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
max_resistance = 32;
|
||||
_ergTable.loadDefaultData(kSpeedRaceXDefaultErgData);
|
||||
}
|
||||
|
||||
|
||||
@@ -1866,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ enum FtmsControlPointCommand {
|
||||
FTMS_START_RESUME,
|
||||
FTMS_STOP_PAUSE,
|
||||
FTMS_SET_TARGETED_EXP_ENERGY,
|
||||
FTMS_SET_TARGETED_STEPS,
|
||||
FTMS_SET_TARGETED_STEPS,
|
||||
FTMS_SET_TARGETED_STRIDES,
|
||||
FTMS_SET_TARGETED_DISTANCE,
|
||||
FTMS_SET_TARGETED_TIME,
|
||||
@@ -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,14 +131,19 @@ class ftmsbike : public bike {
|
||||
bool noHeartService = false;
|
||||
|
||||
bool powerForced = false;
|
||||
resistance_t m_lastErgResistance = 0;
|
||||
|
||||
bool resistance_lvl_mode = false;
|
||||
bool resistance_received = false;
|
||||
inclinationResistanceTable _inclinationResistanceTable;
|
||||
|
||||
// D500V2 workaround: track if we're awaiting start simulation command after request control
|
||||
bool awaiting_start_simulation_after_request_control = false;
|
||||
|
||||
bool DU30_bike = false;
|
||||
bool ICSE = false;
|
||||
bool DOMYOS = false;
|
||||
bool D500V2 = false;
|
||||
bool _3G_Cardio_RB = false;
|
||||
bool SCH_190U = false;
|
||||
bool SCH_290R = false;
|
||||
@@ -150,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;
|
||||
@@ -168,7 +175,8 @@ class ftmsbike : public bike {
|
||||
bool SPORT01 = false;
|
||||
bool FS_YK = false;
|
||||
bool S18 = false;
|
||||
bool JFICCYCLE = false;
|
||||
bool ZIPRO_RAVE = false;
|
||||
bool SPEEDRACEX = false;
|
||||
|
||||
uint8_t secondsToResetTimer = 5;
|
||||
|
||||
@@ -176,6 +184,9 @@ class ftmsbike : public bike {
|
||||
|
||||
uint8_t battery_level = 0;
|
||||
|
||||
bool wattReceived = false;
|
||||
bool gearInclinationSent = false;
|
||||
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
QDateTime lastGoodCadence = QDateTime::currentDateTime();
|
||||
|
||||
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
|
||||
@@ -78,7 +78,7 @@ void ftmsrower::update() {
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
if(I_ROWER || ROWER || MRK_R06) {
|
||||
if(I_ROWER || SF_RW || ROWER || MRK_R06) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(write, sizeof(write), "start", false, true);
|
||||
|
||||
@@ -396,8 +396,8 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
|
||||
}
|
||||
|
||||
if (Flags.totDistance) {
|
||||
if (ICONSOLE_PLUS || FITSHOW) {
|
||||
// For ICONSOLE+, always calculate distance from speed instead of using characteristic data
|
||||
if (ICONSOLE_PLUS || FITSHOW || MRK_R11S) {
|
||||
// For ICONSOLE+/FITSHOW/MRK_R11S, always calculate distance from speed instead of using characteristic data
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
} else {
|
||||
@@ -451,7 +451,7 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
|
||||
if((DFIT_L_R && Cadence.value() > 0) || !DFIT_L_R)
|
||||
m_watt = watt;
|
||||
}
|
||||
} else {
|
||||
} else if(!PM5) {
|
||||
qDebug() << "rower doesn't send wattage, let's calculate it...";
|
||||
if(Speed.value() > 0)
|
||||
m_watt = rower::calculateWattsFromPace(instantPace);
|
||||
@@ -592,10 +592,10 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
connect(s, &QLowEnergyService::descriptorWritten, this, &ftmsrower::descriptorWritten);
|
||||
connect(s, &QLowEnergyService::descriptorRead, this, &ftmsrower::descriptorRead);
|
||||
|
||||
if (I_ROWER || ROWER || MRK_R06) {
|
||||
if (I_ROWER || SF_RW || ROWER || MRK_R06 || DOMYOS) {
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
if (s->serviceUuid() != ftmsService) {
|
||||
qDebug() << QStringLiteral("I-ROWER/ROWER/MRK-R06 wants to be subscribed only to FTMS service in order to send metrics")
|
||||
qDebug() << QStringLiteral("I-ROWER/SF-RW/ROWER/MRK-R06/DOMYOS wants to be subscribed only to FTMS service in order to send metrics")
|
||||
<< s->serviceUuid();
|
||||
continue;
|
||||
}
|
||||
@@ -774,20 +774,28 @@ void ftmsrower::serviceScanDone(void) {
|
||||
QBluetoothUuid concept2InfoService(QStringLiteral("ce060010-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2ControlService(QStringLiteral("ce060020-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2RowingService(QStringLiteral("ce060030-43e5-11e4-916c-0800200c9a66"));
|
||||
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
if (s == concept2InfoService || s == concept2ControlService || s == concept2RowingService) {
|
||||
hasConcept2Services = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hasConcept2Services) {
|
||||
emit debug(QStringLiteral("PM5 without FTMS service detected, using Concept2 protocol"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
// For DOMYOS, discover only FTMS service (0x1826)
|
||||
if (DOMYOS) {
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
if (s != ftmsService) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&ftmsrower::stateChanged);
|
||||
@@ -831,12 +839,18 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("I-ROWER"))) {
|
||||
I_ROWER = true;
|
||||
qDebug() << "I_ROWER found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("SF-RW"))) {
|
||||
SF_RW = true;
|
||||
qDebug() << "SF-RW found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("IROWER "))) {
|
||||
ROWER = true;
|
||||
qDebug() << "ROWER found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("MRK-R06-"))) {
|
||||
MRK_R06 = true;
|
||||
qDebug() << "MRK_R06 found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("MRK-R11S-"))) {
|
||||
MRK_R11S = true;
|
||||
qDebug() << "MRK_R11S found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) {
|
||||
PM5 = true;
|
||||
qDebug() << "PM5 found!";
|
||||
@@ -849,6 +863,9 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("FS-"))) {
|
||||
FITSHOW = true;
|
||||
qDebug() << "FITSHOW found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("DOMYOS-ROW-"))) {
|
||||
DOMYOS = true;
|
||||
qDebug() << "DOMYOS found!";
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
|
||||
@@ -73,12 +73,15 @@ class ftmsrower : public rower {
|
||||
bool NORDLYS = false;
|
||||
bool ICONSOLE_PLUS = false;
|
||||
bool FITSHOW = false;
|
||||
bool DOMYOS = false;
|
||||
|
||||
bool WATER_ROWER = false;
|
||||
bool DFIT_L_R = false;
|
||||
bool I_ROWER = false;
|
||||
bool SF_RW = false;
|
||||
bool ROWER = false;
|
||||
bool MRK_R06 = false;
|
||||
bool MRK_R11S = false;
|
||||
QDateTime lastStroke = QDateTime::currentDateTime();
|
||||
double lastStrokesCount = 0;
|
||||
|
||||
|
||||
@@ -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) /
|
||||
|
||||
@@ -921,6 +921,8 @@ void horizontreadmill::update() {
|
||||
settings.value(QZSettings::horizon_treadmill_7_8, QZSettings::default_horizon_treadmill_7_8).toBool();
|
||||
bool horizon_paragon_x =
|
||||
settings.value(QZSettings::horizon_paragon_x, QZSettings::default_horizon_paragon_x).toBool();
|
||||
bool treadmill_direct_distance =
|
||||
settings.value(QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance).toBool();
|
||||
update_metrics(!powerReceivedFromPowerSensor, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
if (firstDistanceCalculated) {
|
||||
@@ -932,9 +934,11 @@ void horizontreadmill::update() {
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
|
||||
now)))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
if (!treadmill_direct_distance) {
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged = now;
|
||||
}
|
||||
@@ -1256,11 +1260,11 @@ void horizontreadmill::forceSpeed(double requestSpeed) {
|
||||
uint8_t writeS[] = {FTMS_SET_TARGET_SPEED, 0x00, 0x00};
|
||||
if(BOWFLEX_T9) {
|
||||
requestSpeed *= miles_conversion; // this treadmill wants the speed in miles, at least seems so!!
|
||||
}
|
||||
if(TM4800 || TM6500) {
|
||||
}
|
||||
if(TM4800 || TM6500 || T3G_ELITE || WT_TREADMILL) {
|
||||
bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
if(miles) {
|
||||
requestSpeed *= miles_conversion; // this treadmill wants the speed in miles when miles_unit is enabled
|
||||
requestSpeed *= miles_conversion; // these treadmills want the speed in miles when miles_unit is enabled
|
||||
}
|
||||
}
|
||||
uint16_t speed_int = round(requestSpeed * 100);
|
||||
@@ -1533,6 +1537,8 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
bool treadmill_direct_distance =
|
||||
settings.value(QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance).toBool();
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
@@ -1589,7 +1595,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
|
||||
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
@@ -1615,7 +1621,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
|
||||
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
@@ -1639,7 +1645,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
|
||||
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
@@ -1733,19 +1739,19 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
if (Flags.totDistance) {
|
||||
|
||||
/*
|
||||
* the distance sent from the most trainers is a total distance, so it's useless for QZ
|
||||
*
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;*/
|
||||
if (treadmill_direct_distance) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
}
|
||||
index += 3;
|
||||
}
|
||||
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
if (!treadmill_direct_distance) {
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
|
||||
@@ -1865,9 +1871,15 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
double speed = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index)))) /
|
||||
100.0;
|
||||
if(BOWFLEX_T9) {
|
||||
bool fitshow_treadmill_miles = settings.value(QZSettings::fitshow_treadmill_miles, QZSettings::default_fitshow_treadmill_miles).toBool();
|
||||
if(BOWFLEX_T9 && fitshow_treadmill_miles) {
|
||||
// this treadmill sends the speed in miles!
|
||||
speed *= miles_conversion;
|
||||
} else if(T3G_ELITE) {
|
||||
if(miles) {
|
||||
// this treadmill sends the speed in miles when miles_unit is enabled!
|
||||
speed /= miles_conversion;
|
||||
}
|
||||
} else if(horizon_treadmill_7_8 && miles) {
|
||||
// this treadmill sends the speed in miles!
|
||||
speed /= miles_conversion;
|
||||
@@ -1888,14 +1900,16 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
if (Flags.totalDistance) {
|
||||
// ignoring the distance, because it's a total life odometer
|
||||
// Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
// (uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index)))) / 1000.0;
|
||||
if (treadmill_direct_distance) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
}
|
||||
index += 3;
|
||||
}
|
||||
// else
|
||||
{
|
||||
if (firstDistanceCalculated && !isPaused())
|
||||
if (firstDistanceCalculated && !isPaused() && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
distanceEval = true;
|
||||
@@ -1904,7 +1918,10 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
|
||||
if (Flags.inclination) {
|
||||
if(!tunturi_t60_treadmill && !ICONCEPT_FTMS_treadmill)
|
||||
if(domyos_treadmill_ts100) {
|
||||
// Domyos TS100 has a fixed 15° inclination
|
||||
Inclination = 15;
|
||||
} else if(!tunturi_t60_treadmill && !ICONCEPT_FTMS_treadmill && !T01)
|
||||
parseInclination(treadmillInclinationOverride((double)(
|
||||
(int16_t)(
|
||||
((int16_t)(int8_t)newValue.at(index + 1) << 8) |
|
||||
@@ -1912,7 +1929,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
)
|
||||
) /
|
||||
10.0));
|
||||
else if(ICONCEPT_FTMS_treadmill) {
|
||||
else if(ICONCEPT_FTMS_treadmill || T01) {
|
||||
uint8_t val1 = (uint8_t)newValue.at(index);
|
||||
uint8_t val2 = (uint8_t)newValue.at(index + 1);
|
||||
if(val1 == 0x3C && val2 == 0x00) {
|
||||
@@ -1951,6 +1968,10 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
index += 4; // the ramo value is useless
|
||||
emit debug(QStringLiteral("Current Inclination: ") + QString::number(Inclination.value()));
|
||||
} else if(domyos_treadmill_ts100) {
|
||||
// Domyos TS100 has a fixed 15° inclination (no inclination flag in 2ACD)
|
||||
Inclination = 15;
|
||||
emit debug(QStringLiteral("Current Inclination (TS100 fixed): ") + QString::number(Inclination.value()));
|
||||
}
|
||||
|
||||
if (Flags.elevation) {
|
||||
@@ -2094,13 +2115,20 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
if (Flags.totDistance && newValue.length() > index + 2) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
if (treadmill_direct_distance) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
} else {
|
||||
if (firstDistanceCalculated)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
}
|
||||
index += 3;
|
||||
distanceEval = true;
|
||||
} else {
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
distanceEval = true;
|
||||
@@ -2635,6 +2663,10 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((device.name().toUpper().startsWith("DOMYOS"))) {
|
||||
qDebug() << QStringLiteral("DOMYOS found");
|
||||
DOMYOS = true;
|
||||
domyos_treadmill_ts100 = settings.value(QZSettings::domyos_treadmill_ts100, QZSettings::default_domyos_treadmill_ts100).toBool();
|
||||
if(domyos_treadmill_ts100) {
|
||||
qDebug() << QStringLiteral("Domyos TS100 mode ON - Fixed 15° inclination");
|
||||
}
|
||||
} else if ((device.name().toUpper().startsWith(QStringLiteral("BFX_T9_")))) {
|
||||
qDebug() << QStringLiteral("BOWFLEX T9 found");
|
||||
BOWFLEX_T9 = true;
|
||||
@@ -2666,6 +2698,9 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("TM6500 treadmill found");
|
||||
TM6500 = true;
|
||||
minInclination = -3.0;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("WT")) && device.name().length() == 5) {
|
||||
qDebug() << QStringLiteral("WT treadmill found");
|
||||
WT_TREADMILL = true;
|
||||
}
|
||||
|
||||
if (device.name().toUpper().startsWith(QStringLiteral("TRX3500"))) {
|
||||
|
||||
@@ -106,6 +106,7 @@ class horizontreadmill : public treadmill {
|
||||
bool ICONCEPT_FTMS_treadmill = false;
|
||||
bool iconcept_ftms_treadmill_inclination_table = false;
|
||||
bool DOMYOS = false;
|
||||
bool domyos_treadmill_ts100 = false;
|
||||
bool SW_TREADMILL = false;
|
||||
bool BOWFLEX_T9 = false;
|
||||
bool YPOO_MINI_PRO = false;
|
||||
@@ -118,6 +119,7 @@ class horizontreadmill : public treadmill {
|
||||
bool T01 = false;
|
||||
bool TM4800 = false;
|
||||
bool TM6500 = false;
|
||||
bool WT_TREADMILL = false;
|
||||
|
||||
void testProfileCRC();
|
||||
void updateProfileCRC();
|
||||
|
||||
@@ -412,6 +412,38 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic
|
||||
}
|
||||
if (lastRunState != runState) {
|
||||
lastRunState = runState;
|
||||
|
||||
// Only handle hardware buttons if setting is enabled
|
||||
QSettings settingsForHW;
|
||||
if (settingsForHW.value(QZSettings::kingsmith_r2_enable_hw_buttons,
|
||||
QZSettings::default_kingsmith_r2_enable_hw_buttons).toBool()) {
|
||||
|
||||
// Connection packet check: runState=0 + controlMode=1
|
||||
bool isConnectionPacket = (runState == STOP) && (controlMode == MANUAL) && !initDone;
|
||||
|
||||
if (runState == START) {
|
||||
emit debug(QStringLiteral("start button pressed on treadmill!"));
|
||||
emit buttonHWStart();
|
||||
} else if (runState == STOP && !isConnectionPacket) {
|
||||
emit debug(QStringLiteral("pause button pressed on treadmill!"));
|
||||
emit buttonHWPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for real stop: paused (from bluetoothdevice) + metrics reset + has distance
|
||||
// Only if setting is enabled
|
||||
QSettings settingsForStopCheck;
|
||||
if (settingsForStopCheck.value(QZSettings::kingsmith_r2_enable_hw_buttons,
|
||||
QZSettings::default_kingsmith_r2_enable_hw_buttons).toBool() &&
|
||||
paused) {
|
||||
|
||||
if (props.value("RunningTotalTime", -1) == 0 &&
|
||||
props.value("RunningSteps", -1) == 0 &&
|
||||
Distance.value() > 0) {
|
||||
emit debug(QStringLiteral("stop button pressed on treadmill!"));
|
||||
emit buttonHWStop();
|
||||
}
|
||||
}
|
||||
firstCharacteristicChanged = false;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <chrono>
|
||||
#include <math.h>
|
||||
#include <qmath.h>
|
||||
#include "homeform.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
@@ -608,6 +609,89 @@ void nordictrackelliptical::forceResistance(resistance_t requestResistance) {
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackelliptical::se7i_send_next_frame() {
|
||||
if (!nordictrack_se7i) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("se7i_send_next_frame: state = ") + QString::number(se7i_init_state));
|
||||
|
||||
switch (se7i_init_state) {
|
||||
case 0: {
|
||||
// Frame 1: se7i_initData11, se7i_initData12, se7i_initData13 (ends with 0xff)
|
||||
uint8_t se7i_initData11[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x06, 0x28, 0x90, 0x04, 0x00, 0x0d, 0x68, 0xc9, 0x28, 0x95, 0xf0, 0x69, 0xc0, 0x3d};
|
||||
uint8_t se7i_initData12[] = {0x01, 0x12, 0xa8, 0x19, 0x88, 0xf5, 0x60, 0xf9, 0x70, 0xcd, 0x48, 0xc9, 0x48, 0xf5, 0x70, 0xe9, 0x60, 0x1d, 0x88, 0x39};
|
||||
uint8_t se7i_initData13[] = {0xff, 0x08, 0xa8, 0x55, 0xc0, 0x80, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_initData11, sizeof(se7i_initData11), QStringLiteral("se7i_frame1_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_initData12, sizeof(se7i_initData12), QStringLiteral("se7i_frame1_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_initData13, sizeof(se7i_initData13), QStringLiteral("se7i_frame1_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 1;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 1 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// Frame 2: se7i_initData14, se7i_initData15, se7i_initData16 (ends with 0xff)
|
||||
uint8_t se7i_initData14[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t se7i_initData15[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x06, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData16[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_initData14, sizeof(se7i_initData14), QStringLiteral("se7i_frame2_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_initData15, sizeof(se7i_initData15), QStringLiteral("se7i_frame2_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_initData16, sizeof(se7i_initData16), QStringLiteral("se7i_frame2_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 2;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 2 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// Frame 3: se7i_init_020, se7i_init_021, se7i_init_022 (ends with 0xff)
|
||||
uint8_t se7i_init_020[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t se7i_init_021[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x06, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_init_022[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_init_020, sizeof(se7i_init_020), QStringLiteral("se7i_frame3_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_init_021, sizeof(se7i_init_021), QStringLiteral("se7i_frame3_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_init_022, sizeof(se7i_init_022), QStringLiteral("se7i_frame3_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 3;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 3 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// Frame 4: se7i_init_023, se7i_init_024, se7i_init_025 (ends with 0xff)
|
||||
uint8_t se7i_init_023[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t se7i_init_024[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x06, 0x13, 0x02, 0x00, 0x0d, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x4c, 0x00, 0x00, 0xe0};
|
||||
uint8_t se7i_init_025[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_init_023, sizeof(se7i_init_023), QStringLiteral("se7i_frame4_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_init_024, sizeof(se7i_init_024), QStringLiteral("se7i_frame4_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_init_025, sizeof(se7i_init_025), QStringLiteral("se7i_frame4_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 4;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 4 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
// Initialization complete!
|
||||
emit debug(QStringLiteral("se7i: Initialization completed successfully!"));
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("SE7i init completed!");
|
||||
initDone = true;
|
||||
se7i_waiting_for_response = false;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
emit debug(QStringLiteral("se7i_send_next_frame: invalid state ") + QString::number(se7i_init_state));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackelliptical::update() {
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
@@ -1016,6 +1100,21 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
|
||||
|
||||
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
|
||||
|
||||
// SE7i frame-based protocol: check for 0xFF marker indicating end of response frame
|
||||
if (nordictrack_se7i && se7i_waiting_for_response && newValue.length() > 0) {
|
||||
if ((uint8_t)newValue.at(0) == 0xFF) {
|
||||
emit debug(QStringLiteral("SE7i: Received 0xFF marker - end of response frame detected"));
|
||||
se7i_waiting_for_response = false;
|
||||
// Schedule next frame send in the next event loop iteration to avoid reentrancy issues
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
se7i_send_next_frame();
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
emit debug(QStringLiteral("SE7i: Received packet (waiting for 0xFF): ") + QString::number((uint8_t)newValue.at(0), 16));
|
||||
}
|
||||
}
|
||||
|
||||
lastPacket = newValue;
|
||||
|
||||
// SE7i Speed and Cadence parsing (Type 0x01 packets with byte[4]=0x46)
|
||||
@@ -1298,66 +1397,19 @@ void nordictrackelliptical::btinit() {
|
||||
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
if (nordictrack_se7i) {
|
||||
// NordicTrack Elliptical SE7i initialization (19 packets: pkt944 to pkt1020)
|
||||
max_resistance = 22;
|
||||
max_inclination = 20;
|
||||
|
||||
uint8_t se7i_initData1[] = {0xfe, 0x02, 0x08, 0x02};
|
||||
uint8_t se7i_initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x06, 0x04, 0x80, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x06, 0x04, 0x88, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData5[] = {0xfe, 0x02, 0x0b, 0x02}; // pkt972
|
||||
uint8_t se7i_initData6[] = {0xff, 0x0b, 0x02, 0x04, 0x02, 0x07, 0x02, 0x07, 0x82, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt975
|
||||
uint8_t se7i_initData7[] = {0xfe, 0x02, 0x0a, 0x02}; // pkt982
|
||||
uint8_t se7i_initData8[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt985
|
||||
uint8_t se7i_initData9[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt994
|
||||
uint8_t se7i_initData10[] = {0xfe, 0x02, 0x2c, 0x04}; // pkt1000
|
||||
uint8_t se7i_initData11[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x06, 0x28, 0x90, 0x04, 0x00, 0x0d, 0x68, 0xc9, 0x28, 0x95, 0xf0, 0x69, 0xc0, 0x3d}; // pkt1003
|
||||
uint8_t se7i_initData12[] = {0x01, 0x12, 0xa8, 0x19, 0x88, 0xf5, 0x60, 0xf9, 0x70, 0xcd, 0x48, 0xc9, 0x48, 0xf5, 0x70, 0xe9, 0x60, 0x1d, 0x88, 0x39}; // pkt1006
|
||||
uint8_t se7i_initData13[] = {0xff, 0x08, 0xa8, 0x55, 0xc0, 0x80, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt1009
|
||||
uint8_t se7i_initData14[] = {0xfe, 0x02, 0x19, 0x03}; // pkt1014
|
||||
uint8_t se7i_initData15[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x06, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt1017
|
||||
uint8_t se7i_initData16[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt1020
|
||||
// Initialize frame-based communication state machine
|
||||
se7i_init_state = 0;
|
||||
se7i_waiting_for_response = false;
|
||||
|
||||
int sleepms = 400;
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData2, sizeof(se7i_initData2), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData3, sizeof(se7i_initData3), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData4, sizeof(se7i_initData4), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData5, sizeof(se7i_initData5), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData6, sizeof(se7i_initData6), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData7, sizeof(se7i_initData7), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData8, sizeof(se7i_initData8), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData9, sizeof(se7i_initData9), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData10, sizeof(se7i_initData10), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData11, sizeof(se7i_initData11), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData12, sizeof(se7i_initData12), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData13, sizeof(se7i_initData13), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData14, sizeof(se7i_initData14), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData15, sizeof(se7i_initData15), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData16, sizeof(se7i_initData16), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
// Start frame-based initialization sequence
|
||||
emit debug(QStringLiteral("SE7i: Starting frame-based initialization (no sleep mode)"));
|
||||
se7i_send_next_frame();
|
||||
|
||||
// Do NOT set initDone here - it will be set when all frames complete
|
||||
return;
|
||||
} else if (nordictrack_elliptical_c7_5) {
|
||||
max_resistance = 22;
|
||||
max_inclination = 20;
|
||||
|
||||
@@ -86,6 +86,11 @@ class nordictrackelliptical : public elliptical {
|
||||
bool nordictrack_elliptical_c7_5 = false;
|
||||
bool nordictrack_se7i = false;
|
||||
|
||||
// SE7i frame-based initialization state management
|
||||
int se7i_init_state = 0;
|
||||
bool se7i_waiting_for_response = false;
|
||||
void se7i_send_next_frame();
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
@@ -428,6 +428,111 @@ void proformbike::forceResistance(resistance_t requestResistance) {
|
||||
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
|
||||
break;
|
||||
}
|
||||
} else if (nordictrack_gx_4_5_pro) {
|
||||
// Nordic Track GX 4.5 Pro - 25 resistance levels
|
||||
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x8f, 0x01, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x1f, 0x03, 0x00, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xaf, 0x04, 0x00, 0xca, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x3f, 0x06, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xcf, 0x07, 0x00, 0xed, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x5f, 0x09, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xef, 0x0a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x7f, 0x0c, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x0f, 0x0e, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x9f, 0x0f, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x2f, 0x11, 0x00, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xbf, 0x12, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x4f, 0x14, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xdf, 0x15, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x6f, 0x17, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xff, 0x18, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x8f, 0x1a, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x1f, 0x1c, 0x00, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xaf, 0x1d, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x3f, 0x1f, 0x00, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xcf, 0x20, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x5f, 0x22, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0xef, 0x23, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x7f, 0x25, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res25[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x0f, 0x27, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
switch (requestResistance) {
|
||||
case 1:
|
||||
writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true);
|
||||
break;
|
||||
case 2:
|
||||
writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true);
|
||||
break;
|
||||
case 3:
|
||||
writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true);
|
||||
break;
|
||||
case 4:
|
||||
writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true);
|
||||
break;
|
||||
case 5:
|
||||
writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true);
|
||||
break;
|
||||
case 6:
|
||||
writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true);
|
||||
break;
|
||||
case 7:
|
||||
writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true);
|
||||
break;
|
||||
case 8:
|
||||
writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true);
|
||||
break;
|
||||
case 9:
|
||||
writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true);
|
||||
break;
|
||||
case 10:
|
||||
writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true);
|
||||
break;
|
||||
case 11:
|
||||
writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true);
|
||||
break;
|
||||
case 12:
|
||||
writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true);
|
||||
break;
|
||||
case 13:
|
||||
writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true);
|
||||
break;
|
||||
case 14:
|
||||
writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true);
|
||||
break;
|
||||
case 15:
|
||||
writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true);
|
||||
break;
|
||||
case 16:
|
||||
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
|
||||
break;
|
||||
case 17:
|
||||
writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true);
|
||||
break;
|
||||
case 18:
|
||||
writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true);
|
||||
break;
|
||||
case 19:
|
||||
writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true);
|
||||
break;
|
||||
case 20:
|
||||
writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true);
|
||||
break;
|
||||
case 21:
|
||||
writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true);
|
||||
break;
|
||||
case 22:
|
||||
writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true);
|
||||
break;
|
||||
case 23:
|
||||
writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true);
|
||||
break;
|
||||
case 24:
|
||||
writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true);
|
||||
break;
|
||||
case 25:
|
||||
writeCharacteristic((uint8_t *)res25, sizeof(res25), QStringLiteral("resistance25"), false, true);
|
||||
break;
|
||||
}
|
||||
} else if (nordictrack_gx_2_7 || proform_bike_225_csx || proform_225_csx_PFEX32925_INT_0) {
|
||||
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0xc2, 0x01, 0x00, 0xda, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
@@ -849,9 +954,9 @@ bool proformbike::innerWriteResistance() {
|
||||
if (requestResistance != currentResistance().value()) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
forceResistance(requestResistance);
|
||||
return true;
|
||||
}
|
||||
requestResistance = -1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -996,9 +1101,20 @@ void proformbike::update() {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
|
||||
// nordictrack gx 4.5 pro
|
||||
uint8_t noOpData1_nordictrack_gx_4_5_pro[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t noOpData2_nordictrack_gx_4_5_pro[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, 0x0d, 0x3c, 0x9c, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80};
|
||||
uint8_t noOpData3_nordictrack_gx_4_5_pro[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData4_nordictrack_gx_4_5_pro[] = {0xfe, 0x02, 0x0d, 0x02};
|
||||
uint8_t noOpData5_nordictrack_gx_4_5_pro[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t noOpData6_nordictrack_gx_4_5_pro[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x00, 0x0f, 0xbc, 0x90, 0x70, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00};
|
||||
uint8_t noOpData7_nordictrack_gx_4_5_pro[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x08, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
switch (counterPoll) {
|
||||
case 0:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData1_nordictrack_gx_4_5_pro, sizeof(noOpData1_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData1_proform_csx210, sizeof(noOpData1_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_225_csx || proform_bike_325_csx || proform_xbike || proform_225_csx_PFEX32925_INT_0) {
|
||||
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
|
||||
@@ -1009,7 +1125,9 @@ void proformbike::update() {
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData2_nordictrack_gx_4_5_pro, sizeof(noOpData2_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData2_proform_csx210, sizeof(noOpData2_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData2_proform_xbike, sizeof(noOpData2_proform_xbike), QStringLiteral("noOp"));
|
||||
@@ -1045,7 +1163,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 2:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData3_nordictrack_gx_4_5_pro, sizeof(noOpData3_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData3_proform_csx210, sizeof(noOpData3_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData3_proform_xbike, sizeof(noOpData3_proform_xbike), QStringLiteral("noOp"));
|
||||
@@ -1081,7 +1201,10 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 3:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData4_nordictrack_gx_4_5_pro, sizeof(noOpData4_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
innerWriteResistance();
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData4_proform_csx210, sizeof(noOpData4_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
innerWriteResistance();
|
||||
@@ -1106,13 +1229,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 4:
|
||||
if (proform_csx210) {
|
||||
writeCharacteristic(noOpData5_proform_csx210, sizeof(noOpData5_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData5_proform_xbike, sizeof(noOpData5_proform_xbike), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
writeCharacteristic(noOpData5_proform_studio, sizeof(noOpData5_proform_studio), QStringLiteral("noOp"));
|
||||
else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData5_nordictrack_gx_4_5_pro, sizeof(noOpData5_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci) {
|
||||
writeCharacteristic(noOpData5_nordictrack_gx_2_7, sizeof(noOpData5_nordictrack_gx_2_7),
|
||||
QStringLiteral("noOp"));
|
||||
} else if (proform_hybrid_trainer_PFEL03815) {
|
||||
@@ -1136,7 +1255,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 5:
|
||||
if (proform_csx210) {
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData6_nordictrack_gx_4_5_pro, sizeof(noOpData6_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else if (proform_csx210) {
|
||||
writeCharacteristic(noOpData6_proform_csx210, sizeof(noOpData6_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
writeCharacteristic(noOpData6_proform_studio, sizeof(noOpData6_proform_studio), QStringLiteral("noOp"));
|
||||
@@ -1171,13 +1292,18 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 6:
|
||||
if (proform_studio || proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
}
|
||||
writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp"));
|
||||
if (!proform_studio && !proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
if (nordictrack_gx_4_5_pro) {
|
||||
writeCharacteristic(noOpData7_nordictrack_gx_4_5_pro, sizeof(noOpData7_nordictrack_gx_4_5_pro), QStringLiteral("noOp"));
|
||||
} else {
|
||||
if (proform_studio || proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
}
|
||||
writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp"));
|
||||
if (!proform_studio && !proform_tdf_10) {
|
||||
innerWriteResistance();
|
||||
}
|
||||
}
|
||||
break;
|
||||
if (requestInclination != -100 && (proform_studio || proform_tdf_10)) {
|
||||
// only 0.5 steps ara available
|
||||
double inc = qRound(requestInclination * 2.0) / 2.0;
|
||||
@@ -1194,7 +1320,7 @@ void proformbike::update() {
|
||||
counterPoll++;
|
||||
if (counterPoll > 6) {
|
||||
counterPoll = 0;
|
||||
} else if(counterPoll == 6 && (proform_bike_225_csx || proform_225_csx_PFEX32925_INT_0 || proform_bike_PFEVEX71316_0)) {
|
||||
} else if(counterPoll == 6 && (nordictrack_gx_4_5_pro || proform_bike_225_csx || proform_225_csx_PFEX32925_INT_0 || proform_bike_PFEVEX71316_0)) {
|
||||
counterPoll = 0;
|
||||
} else if (counterPoll == 6 &&
|
||||
(proform_tour_de_france_clc || proform_cycle_trainer_400 || proform_bike_PFEVEX71316_1) &&
|
||||
@@ -1817,6 +1943,119 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
Resistance = 1;
|
||||
m_pelotonResistance = 10;
|
||||
}
|
||||
} else if(nordictrack_gx_4_5_pro) {
|
||||
switch ((uint8_t)newValue.at(11)) {
|
||||
case 0x00:
|
||||
case 0x01:
|
||||
Resistance = 1;
|
||||
m_pelotonResistance = 10;
|
||||
break;
|
||||
case 0x02:
|
||||
case 0x03:
|
||||
Resistance = 2;
|
||||
m_pelotonResistance = 20;
|
||||
break;
|
||||
case 0x04:
|
||||
case 0x05:
|
||||
Resistance = 3;
|
||||
m_pelotonResistance = 25;
|
||||
break;
|
||||
case 0x06:
|
||||
Resistance = 4;
|
||||
m_pelotonResistance = 30;
|
||||
break;
|
||||
case 0x07:
|
||||
case 0x08:
|
||||
Resistance = 5;
|
||||
m_pelotonResistance = 33;
|
||||
break;
|
||||
case 0x09:
|
||||
Resistance = 6;
|
||||
m_pelotonResistance = 35;
|
||||
break;
|
||||
case 0x0A:
|
||||
case 0x0b:
|
||||
Resistance = 7;
|
||||
m_pelotonResistance = 38;
|
||||
break;
|
||||
case 0x0c:
|
||||
case 0x0d:
|
||||
Resistance = 8;
|
||||
m_pelotonResistance = 40;
|
||||
break;
|
||||
case 0x0e:
|
||||
Resistance = 9;
|
||||
m_pelotonResistance = 45;
|
||||
break;
|
||||
case 0x0f:
|
||||
case 0x10:
|
||||
Resistance = 10;
|
||||
m_pelotonResistance = 50;
|
||||
break;
|
||||
case 0x11:
|
||||
Resistance = 11;
|
||||
m_pelotonResistance = 55;
|
||||
break;
|
||||
case 0x12:
|
||||
case 0x13:
|
||||
Resistance = 12;
|
||||
m_pelotonResistance = 60;
|
||||
break;
|
||||
case 0x14:
|
||||
Resistance = 13;
|
||||
m_pelotonResistance = 63;
|
||||
break;
|
||||
case 0x15:
|
||||
case 0x16:
|
||||
Resistance = 14;
|
||||
m_pelotonResistance = 65;
|
||||
break;
|
||||
case 0x17:
|
||||
Resistance = 15;
|
||||
m_pelotonResistance = 68;
|
||||
case 0x18:
|
||||
case 0x19:
|
||||
Resistance = 16;
|
||||
m_pelotonResistance = 70;
|
||||
break;
|
||||
case 0x1a:
|
||||
case 0x1b:
|
||||
Resistance = 17;
|
||||
m_pelotonResistance = 75;
|
||||
break;
|
||||
case 0x1c:
|
||||
Resistance = 18;
|
||||
m_pelotonResistance = 80;
|
||||
break;
|
||||
case 0x1d:
|
||||
Resistance = 19;
|
||||
m_pelotonResistance = 85;
|
||||
break;
|
||||
case 0x1f:
|
||||
Resistance = 20;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x20:
|
||||
Resistance = 21;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x22:
|
||||
Resistance = 22;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x23:
|
||||
Resistance = 23;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x25:
|
||||
Resistance = 24;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
case 0x27:
|
||||
Resistance = 25;
|
||||
m_pelotonResistance = 100;
|
||||
break;
|
||||
}
|
||||
} else if (!nordictrack_gx_2_7) {
|
||||
switch ((uint8_t)newValue.at(11)) {
|
||||
case 0x00:
|
||||
@@ -2080,9 +2319,10 @@ void proformbike::btinit() {
|
||||
proform_xbike = settings.value(QZSettings::proform_xbike, QZSettings::default_proform_xbike).toBool();
|
||||
proform_225_csx_PFEX32925_INT_0 = settings.value(QZSettings::proform_225_csx_PFEX32925_INT_0, QZSettings::default_proform_225_csx_PFEX32925_INT_0).toBool();
|
||||
proform_csx210 = settings.value(QZSettings::proform_csx210, QZSettings::default_proform_csx210).toBool();
|
||||
nordictrack_gx_4_5_pro = settings.value(QZSettings::nordictrack_gx_4_5_pro, QZSettings::default_nordictrack_gx_4_5_pro).toBool();
|
||||
|
||||
|
||||
if(nordictrack_GX4_5_bike)
|
||||
if(nordictrack_GX4_5_bike || nordictrack_gx_4_5_pro)
|
||||
max_resistance = 25;
|
||||
if(proform_csx210)
|
||||
max_resistance = 16;
|
||||
@@ -3028,6 +3268,30 @@ void proformbike::btinit() {
|
||||
|
||||
writeCharacteristic(noOpData22, sizeof(noOpData22), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
} else if (nordictrack_gx_4_5_pro) {
|
||||
max_resistance = 25;
|
||||
|
||||
uint8_t initData14[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07, 0x01, 0x99, 0x78, 0x65, 0x40, 0x29, 0x10, 0x0d, 0xf8, 0xe9};
|
||||
uint8_t initData15[] = {0x01, 0x12, 0xd8, 0xc5, 0xb0, 0xb9, 0xa0, 0xbd, 0xb8, 0xb9, 0xb8, 0xa5, 0xa0, 0xa9, 0xd0, 0xcd, 0xf8, 0xe9, 0x18, 0x05};
|
||||
uint8_t initData16[] = {0xff, 0x08, 0x30, 0x59, 0x40, 0x98, 0x02, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData17[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t initData18[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData19[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// Execution
|
||||
writeCharacteristic(initData14, sizeof(initData14), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData15, sizeof(initData15), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData16, sizeof(initData16), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData18, sizeof(initData18), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData19, sizeof(initData19), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
|
||||
} else if (proform_bike_225_csx) {
|
||||
max_resistance = 20;
|
||||
uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07, 0x01, 0xd2, 0x74, 0x14, 0xb2, 0x5e, 0x08, 0xa0, 0x5e, 0x0a};
|
||||
|
||||
@@ -85,6 +85,7 @@ class proformbike : public bike {
|
||||
bool proform_hybrid_trainer_PFEL03815 = false;
|
||||
bool proform_bike_sb = false;
|
||||
bool proform_cycle_trainer_300_ci =false;
|
||||
bool nordictrack_gx_4_5_pro = false;
|
||||
bool proform_bike_225_csx = false;
|
||||
bool proform_bike_325_csx = false;
|
||||
bool proform_tour_de_france_clc = false;
|
||||
|
||||
@@ -488,7 +488,7 @@ void proformwifibike::characteristicChanged(const QString &newValue) {
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = m_rawWatt.value();
|
||||
m_watt.setValue(m_rawWatt.value(), false);
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
|
||||
} else if (!values[QStringLiteral("Watt attuali")].isUndefined()) {
|
||||
double watt = values[QStringLiteral("Watt attuali")].toString().toDouble();
|
||||
|
||||
457
src/devices/sportstechrower/sportstechrower.cpp
Normal file
457
src/devices/sportstechrower/sportstechrower.cpp
Normal file
@@ -0,0 +1,457 @@
|
||||
#include "sportstechrower.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
sportstechrower::sportstechrower(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
this->bikeResistanceGain = bikeResistanceGain;
|
||||
this->bikeResistanceOffset = bikeResistanceOffset;
|
||||
initDone = false;
|
||||
connect(refresh, &QTimer::timeout, this, &sportstechrower::update);
|
||||
refresh->start(200ms);
|
||||
}
|
||||
|
||||
void sportstechrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
if (wait_for_response) {
|
||||
connect(this, &sportstechrower::packetReceived, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + " // " + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
void sportstechrower::forceResistance(resistance_t requestResistance) {
|
||||
Q_UNUSED(requestResistance);
|
||||
// Resistance control disabled for this rower
|
||||
}
|
||||
|
||||
void sportstechrower::update() {
|
||||
// qDebug() << bike.isValid() << m_control->state() << gattCommunicationChannelService <<
|
||||
// gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
|
||||
|
||||
if (!m_control) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
btinit(false);
|
||||
} else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
|
||||
gattCommunicationChannelService && gattWriteCharacteristic.isValid() &&
|
||||
gattNotify1Characteristic.isValid() && initDone) {
|
||||
update_metrics(false, 0);
|
||||
|
||||
// updating the bike console every second
|
||||
if (sec1update++ == (1000 / refresh->interval())) {
|
||||
sec1update = 0;
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
uint8_t noOpData[] = {0xf2, 0xc3, 0x07, 0x04, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbe};
|
||||
// Always send resistance = 0 (no resistance control for rower)
|
||||
writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true);
|
||||
}
|
||||
}
|
||||
|
||||
void sportstechrower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void sportstechrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
// qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
|
||||
Q_UNUSED(characteristic);
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
emit packetReceived();
|
||||
|
||||
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
|
||||
|
||||
lastPacket = newValue;
|
||||
if (newValue.length() != 20) {
|
||||
return;
|
||||
}
|
||||
|
||||
double speed = GetSpeedFromPacket(newValue);
|
||||
double strokeRate = GetStrokeRateFromPacket(newValue);
|
||||
double resistance = GetResistanceFromPacket(newValue);
|
||||
double kcal = GetKcalFromPacket(newValue);
|
||||
double watt = GetWattFromPacket(newValue);
|
||||
bool disable_hr_frommachinery =
|
||||
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
Heart = (uint8_t)KeepAwakeHelper::heart();
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
|
||||
|
||||
uint8_t heart = ((uint8_t)newValue.at(11));
|
||||
if (heart == 0 || disable_hr_frommachinery) {
|
||||
update_hr_from_external();
|
||||
} else {
|
||||
Heart = heart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstCharChanged) {
|
||||
Distance += ((speed / 3600.0) / (1000.0 / (lastTimeCharChanged.msecsTo(QTime::currentTime()))));
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("Current speed: ") + QString::number(speed));
|
||||
emit debug(QStringLiteral("Current stroke rate: ") + QString::number(strokeRate));
|
||||
emit debug(QStringLiteral("Current resistance: ") + QString::number(resistance));
|
||||
emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value()));
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(kcal));
|
||||
emit debug(QStringLiteral("Current watt: ") + QString::number(watt));
|
||||
emit debug(QStringLiteral("Current Elapsed from the bike (not used): ") +
|
||||
QString::number(GetElapsedFromPacket(newValue)));
|
||||
emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(Distance.value()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
}
|
||||
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = speed;
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), 0);
|
||||
}
|
||||
Resistance = resistance;
|
||||
emit resistanceRead(Resistance.value());
|
||||
KCal = kcal;
|
||||
|
||||
// For rowers, cadence = stroke rate (strokes per minute)
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = strokeRate;
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = watt;
|
||||
|
||||
lastTimeCharChanged = QTime::currentTime();
|
||||
firstCharChanged = false;
|
||||
}
|
||||
|
||||
uint16_t sportstechrower::GetElapsedFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedDataSec = (packet.at(4));
|
||||
uint16_t convertedDataMin = (packet.at(3));
|
||||
uint16_t convertedData = convertedDataMin * 60.f + convertedDataSec;
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
double sportstechrower::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(12) << 8) | ((uint8_t)packet.at(13));
|
||||
double data = (double)(convertedData) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
|
||||
double sportstechrower::GetKcalFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(7) << 8) | ((uint8_t)packet.at(8));
|
||||
return (double)(convertedData);
|
||||
}
|
||||
|
||||
double sportstechrower::GetWattFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(9) << 8) | ((uint8_t)packet.at(10));
|
||||
double data = ((double)(convertedData));
|
||||
return data;
|
||||
}
|
||||
|
||||
double sportstechrower::GetStrokeRateFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = packet.at(17);
|
||||
double data = (convertedData);
|
||||
if (data < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
double sportstechrower::GetResistanceFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = packet.at(15);
|
||||
double data = (convertedData);
|
||||
if (data < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void sportstechrower::btinit(bool startTape) {
|
||||
Q_UNUSED(startTape);
|
||||
QSettings settings;
|
||||
|
||||
const uint8_t initData1[] = {0xf2, 0xc0, 0x00, 0xb2};
|
||||
const uint8_t initData2[] = {0xf2, 0xc1, 0x05, 0x01, 0xff, 0xff, 0xff, 0xff, 0xb5};
|
||||
const uint8_t initData3[] = {0xf2, 0xc4, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0};
|
||||
const uint8_t initData4[] = {0xf2, 0xc3, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbb};
|
||||
|
||||
writeCharacteristic((uint8_t *)initData1, sizeof(initData1), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData2, sizeof(initData2), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData4, sizeof(initData4), QStringLiteral("init"), false, true);
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void sportstechrower::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
auto characteristics_list = gattCommunicationChannelService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
emit debug(QStringLiteral("characteristic ") + c.uuid().toString());
|
||||
}
|
||||
|
||||
// QString uuidWrite = "0000fff2-0000-1000-8000-00805f9b34fb";
|
||||
// QString uuidNotify1 = "0000fff1-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0000fff2-0000-1000-8000-00805f9b34fb"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0000fff1-0000-1000-8000-00805f9b34fb"));
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&sportstechrower::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&sportstechrower::characteristicWritten);
|
||||
connect(gattCommunicationChannelService,
|
||||
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &sportstechrower::errorService);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&sportstechrower::descriptorWritten);
|
||||
|
||||
// ******************************************* virtual device init *************************************
|
||||
if (!firstVirtualBike && !this->hasVirtualDevice()) {
|
||||
QSettings settings;
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_rower) {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &sportstechrower::changeInclination);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
emit debug(QStringLiteral("creating virtual rower interface..."));
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstVirtualBike = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
void sportstechrower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' '));
|
||||
|
||||
initRequest = true;
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void sportstechrower::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void sportstechrower::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
// QString uuid = "0000fff0-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
if (gattCommunicationChannelService == nullptr) {
|
||||
qDebug() << QStringLiteral("invalid service") << _gattCommunicationChannelServiceId.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &sportstechrower::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void sportstechrower::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("sportstechrower::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sportstechrower::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("sportstechrower::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sportstechrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &sportstechrower::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &sportstechrower::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &sportstechrower::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &sportstechrower::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("LowEnergy controller disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t sportstechrower::watts() {
|
||||
if (currentCadence().value() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
bool sportstechrower::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void sportstechrower::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t sportstechrower::wattsFromResistance(double resistance) {
|
||||
// Coefficients from the polynomial regression
|
||||
double intercept = 14.4968;
|
||||
double b1 = -4.1878;
|
||||
double b2 = -0.5051;
|
||||
double b3 = 0.00387;
|
||||
double b4 = 0.2392;
|
||||
double b5 = 0.01108;
|
||||
double cadence = Cadence.value();
|
||||
|
||||
// Calculate power using the polynomial equation
|
||||
double power = intercept +
|
||||
(b1 * resistance) +
|
||||
(b2 * cadence) +
|
||||
(b3 * resistance * resistance) +
|
||||
(b4 * resistance * cadence) +
|
||||
(b5 * cadence * cadence);
|
||||
|
||||
return power;
|
||||
}
|
||||
|
||||
resistance_t sportstechrower::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
}
|
||||
100
src/devices/sportstechrower/sportstechrower.h
Normal file
100
src/devices/sportstechrower/sportstechrower.h
Normal file
@@ -0,0 +1,100 @@
|
||||
#ifndef SPORTSTECHROWER_H
|
||||
#define SPORTSTECHROWER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
|
||||
#include "devices/rower.h"
|
||||
|
||||
class sportstechrower : public rower {
|
||||
Q_OBJECT
|
||||
public:
|
||||
sportstechrower(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain);
|
||||
bool connected() override;
|
||||
resistance_t maxResistance() override { return 24; }
|
||||
resistance_t resistanceFromPowerRequest(uint16_t power) override;
|
||||
|
||||
private:
|
||||
double GetSpeedFromPacket(const QByteArray &packet);
|
||||
double GetResistanceFromPacket(const QByteArray &packet);
|
||||
double GetKcalFromPacket(const QByteArray &packet);
|
||||
double GetDistanceFromPacket(QByteArray packet);
|
||||
uint16_t GetElapsedFromPacket(const QByteArray &packet);
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void updateDisplay(uint16_t elapsed);
|
||||
void btinit(bool startTape);
|
||||
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response);
|
||||
void startDiscover();
|
||||
uint16_t watts() override;
|
||||
double GetWattFromPacket(const QByteArray &packet);
|
||||
double GetStrokeRateFromPacket(const QByteArray &packet);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
double bikeResistanceGain = 1.0;
|
||||
|
||||
uint8_t firstVirtualBike = 0;
|
||||
bool firstCharChanged = true;
|
||||
QTime lastTimeCharChanged;
|
||||
uint8_t sec1update = 0;
|
||||
QByteArray lastPacket;
|
||||
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotify1Characteristic;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
bool readyToStart = false;
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void packetReceived();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // SPORTSTECHROWER_H
|
||||
@@ -654,7 +654,16 @@ void strydrunpowersensor::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
bool isZwiftPod = bluetoothDevice.name().contains(QStringLiteral("Zwift RunPod"), Qt::CaseInsensitive);
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
// For Zwift RunPod, skip both fff0 and ffc0 services that cause discovery issues
|
||||
if (isZwiftPod && (s.toString() == QStringLiteral("{0000fff0-0000-1000-8000-00805f9b34fb}") ||
|
||||
s.toString() == QStringLiteral("{f000ffc0-0451-4000-b000-000000000000}"))) {
|
||||
qDebug() << QStringLiteral("Skipping problematic services for Zwift RunPod:") << s.toString();
|
||||
continue;
|
||||
}
|
||||
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&strydrunpowersensor::stateChanged);
|
||||
|
||||
389
src/devices/sunnyfitstepper/sunnyfitstepper.cpp
Normal file
389
src/devices/sunnyfitstepper/sunnyfitstepper.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
#include "sunnyfitstepper.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualtreadmill.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
sunnyfitstepper::sunnyfitstepper(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
this->pollDeviceTime = pollDeviceTime;
|
||||
|
||||
refresh = new QTimer(this);
|
||||
initDone = false;
|
||||
frameBuffer.clear();
|
||||
expectingSecondPart = false;
|
||||
|
||||
connect(refresh, &QTimer::timeout, this, &sunnyfitstepper::update);
|
||||
refresh->start(pollDeviceTime);
|
||||
}
|
||||
|
||||
bool sunnyfitstepper::connected() {
|
||||
if (!m_control)
|
||||
return false;
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
if (wait_for_response) {
|
||||
connect(this, &sunnyfitstepper::packetReceived, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit debug(QStringLiteral("writeCharacteristic error because the connection is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::sendPoll() {
|
||||
// Alternate between two poll commands
|
||||
|
||||
counterPoll++;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::changeInclinationRequested(double grade, double percentage) {
|
||||
if (percentage < 0)
|
||||
percentage = 0;
|
||||
changeInclination(grade, percentage);
|
||||
}
|
||||
|
||||
void sunnyfitstepper::processDataFrame(const QByteArray &completeFrame) {
|
||||
if (completeFrame.length() != 32) {
|
||||
qDebug() << "ERROR: Frame length is not 32 bytes:" << completeFrame.length();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint8_t)completeFrame.at(0) != 0x5a) {
|
||||
qDebug() << "ERROR: Frame doesn't start with 0x5a";
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint8_t)completeFrame.at(1) != 0x05) {
|
||||
qDebug() << "WARNING: Expected 0x05 at byte 1, got:" << QString::number((uint8_t)completeFrame.at(1), 16);
|
||||
}
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QSettings settings;
|
||||
|
||||
// Extract cadence (bytes 6-7, little-endian)
|
||||
uint16_t rawCadence = ((uint8_t)completeFrame.at(7) << 8) | (uint8_t)completeFrame.at(6);
|
||||
Cadence = (double)rawCadence;
|
||||
|
||||
// Extract step count (bytes 10-12, little-endian)
|
||||
uint32_t steps = ((uint32_t)(uint8_t)completeFrame.at(12) << 16) |
|
||||
((uint32_t)(uint8_t)completeFrame.at(11) << 8) |
|
||||
(uint32_t)(uint8_t)completeFrame.at(10);
|
||||
StepCount = steps;
|
||||
|
||||
// Calculate elevation manually (0.2 meters per step)
|
||||
elevationAcc = (double)steps * 0.20;
|
||||
|
||||
// Calculate speed from cadence (stairclimber convention)
|
||||
Speed = Cadence.value() / 3.2;
|
||||
|
||||
qDebug() << QStringLiteral("Current Cadence (SPM): ") + QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current StepCount: ") + QString::number(StepCount.value());
|
||||
qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value());
|
||||
qDebug() << QStringLiteral("Current Elevation: ") + QString::number(elevationAcc.value());
|
||||
|
||||
// Calculate metrics
|
||||
if (!firstCharacteristicChanged) {
|
||||
if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) {
|
||||
KCal += ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) +
|
||||
1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
Distance += ((Speed.value() / 3600.0) / (1000.0 / (lastTimeCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Current Distance: ") + QString::number(Distance.value());
|
||||
qDebug() << QStringLiteral("Current KCal: ") + QString::number(KCal.value());
|
||||
qDebug() << QStringLiteral("Current Watt: ") +
|
||||
QString::number(watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError)
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
|
||||
lastTimeCharacteristicChanged = now;
|
||||
firstCharacteristicChanged = false;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::update() {
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
btinit();
|
||||
} else if (m_control->state() == QLowEnergyController::DiscoveredState && gattCommunicationChannelService &&
|
||||
gattWriteCharacteristic.isValid() && gattNotify1Characteristic.isValid() &&
|
||||
gattNotify4Characteristic.isValid() && initDone) {
|
||||
QSettings settings;
|
||||
|
||||
// *********** virtual treadmill init *************************************
|
||||
if (!this->hasVirtualDevice()) {
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_force_bike =
|
||||
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike)
|
||||
.toBool();
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_force_bike) {
|
||||
debug("creating virtual treadmill interface...");
|
||||
auto virtualTreadMill = new virtualtreadmill(this, noHeartService);
|
||||
connect(virtualTreadMill, &virtualtreadmill::debug, this, &sunnyfitstepper::debug);
|
||||
connect(virtualTreadMill, &virtualtreadmill::changeInclination, this,
|
||||
&sunnyfitstepper::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
debug("creating virtual bike interface...");
|
||||
auto virtualBike = new virtualbike(this);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this,
|
||||
&sunnyfitstepper::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ************************************************************
|
||||
|
||||
update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
// Send poll every 2 seconds
|
||||
if (sec1Update++ >= (2000 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
//sendPoll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
|
||||
|
||||
// Handle command responses (Notify 1)
|
||||
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("fd710003-e950-458e-8a4d-a1cbc5aa4cce"))) {
|
||||
qDebug() << "Command response:" << newValue.toHex(' ');
|
||||
emit packetReceived();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle main data stream (Notify 4) - SPLIT FRAME LOGIC
|
||||
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("fd710006-e950-458e-8a4d-a1cbc5aa4cce"))) {
|
||||
// First part: 20 bytes starting with 0x5a
|
||||
if (newValue.length() == 20 && (uint8_t)newValue.at(0) == 0x5a) {
|
||||
frameBuffer.clear();
|
||||
frameBuffer.append(newValue);
|
||||
expectingSecondPart = true;
|
||||
qDebug() << "First part of frame received (20 bytes)";
|
||||
return;
|
||||
}
|
||||
|
||||
// Second part: 12 bytes
|
||||
if (newValue.length() == 12 && expectingSecondPart) {
|
||||
frameBuffer.append(newValue);
|
||||
expectingSecondPart = false;
|
||||
|
||||
if (frameBuffer.length() == 32) {
|
||||
emit debug(QStringLiteral(" << COMPLETE FRAME >> ") + frameBuffer.toHex(' '));
|
||||
processDataFrame(frameBuffer);
|
||||
frameBuffer.clear();
|
||||
} else {
|
||||
qDebug() << "ERROR: Complete frame size mismatch:" << frameBuffer.length();
|
||||
frameBuffer.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Unexpected frame structure
|
||||
qDebug() << "Unexpected frame - length:" << newValue.length() << "expecting second part:" << expectingSecondPart;
|
||||
frameBuffer.clear();
|
||||
expectingSecondPart = false;
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::btinit() {
|
||||
uint8_t init1[] = {0x5a, 0x02, 0x00, 0x08, 0x07, 0xa0, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0xe6, 0xa5};
|
||||
uint8_t init2[] = {0x5a, 0x02, 0x00, 0x03, 0x02, 0xa3, 0x00, 0xaa, 0xa5};
|
||||
uint8_t init3[] = {0x5a, 0x02, 0x00, 0x03, 0x02, 0xb4, 0x00, 0xbb, 0xa5};
|
||||
uint8_t init4[] = {0x5a, 0x04, 0x00, 0x03, 0x02, 0xf1, 0x00, 0xfa, 0xa5};
|
||||
|
||||
writeCharacteristic(init1, sizeof(init1), QStringLiteral("init1"), false, true);
|
||||
writeCharacteristic(init2, sizeof(init2), QStringLiteral("init2"), false, true);
|
||||
writeCharacteristic(init3, sizeof(init3), QStringLiteral("init3"), false, false);
|
||||
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, false);
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void sunnyfitstepper::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("fd710002-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("fd710003-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
QBluetoothUuid _gattNotify4CharacteristicId(QStringLiteral("fd710006-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
gattNotify4Characteristic = gattCommunicationChannelService->characteristic(_gattNotify4CharacteristicId);
|
||||
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
Q_ASSERT(gattNotify4Characteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&sunnyfitstepper::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&sunnyfitstepper::characteristicWritten);
|
||||
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)), this,
|
||||
SLOT(errorService(QLowEnergyService::ServiceError)));
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&sunnyfitstepper::descriptorWritten);
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify4Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
|
||||
initRequest = true;
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void sunnyfitstepper::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void sunnyfitstepper::serviceScanDone(void) {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
qDebug() << s << "service found!";
|
||||
}
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("fd710001-e950-458e-8a4d-a1cbc5aa4cce"));
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
if (gattCommunicationChannelService == nullptr) {
|
||||
qDebug() << "invalid service";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &sunnyfitstepper::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void sunnyfitstepper::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("sunnyfitstepper::errorService ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("sunnyfitstepper::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void sunnyfitstepper::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("sunnyfitstepper::controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &sunnyfitstepper::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &sunnyfitstepper::serviceScanDone);
|
||||
connect(m_control, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(error(QLowEnergyController::Error)));
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &sunnyfitstepper::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("QLowEnergyController disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void sunnyfitstepper::startDiscover() {
|
||||
m_control->discoverServices();
|
||||
}
|
||||
99
src/devices/sunnyfitstepper/sunnyfitstepper.h
Normal file
99
src/devices/sunnyfitstepper/sunnyfitstepper.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef SUNNYFITSTEPPER_H
|
||||
#define SUNNYFITSTEPPER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "stairclimber.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class sunnyfitstepper : public stairclimber {
|
||||
Q_OBJECT
|
||||
public:
|
||||
sunnyfitstepper(uint32_t pollDeviceTime = 200, bool noConsole = false, bool noHeartService = false,
|
||||
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
void btinit();
|
||||
void sendPoll();
|
||||
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
|
||||
bool wait_for_response = false);
|
||||
void processDataFrame(const QByteArray &completeFrame);
|
||||
void startDiscover();
|
||||
|
||||
// Bluetooth
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotify1Characteristic;
|
||||
QLowEnergyCharacteristic gattNotify4Characteristic;
|
||||
|
||||
// Split-frame handling (CRITICAL)
|
||||
QByteArray frameBuffer;
|
||||
bool expectingSecondPart = false;
|
||||
|
||||
// State
|
||||
QTimer *refresh;
|
||||
uint8_t sec1Update = 0;
|
||||
uint8_t counterPoll = 0;
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
bool noConsole = false;
|
||||
bool noHeartService = false;
|
||||
uint32_t pollDeviceTime = 200;
|
||||
QDateTime lastTimeCharacteristicChanged;
|
||||
bool firstCharacteristicChanged = true;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void speedChanged(double speed);
|
||||
void packetReceived();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
void changeInclinationRequested(double grade, double percentage);
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // SUNNYFITSTEPPER_H
|
||||
225
src/devices/thinkridercontroller/thinkridercontroller.cpp
Normal file
225
src/devices/thinkridercontroller/thinkridercontroller.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "thinkridercontroller.h"
|
||||
#include "homeform.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Thinkrider VS200 UUIDs
|
||||
const QBluetoothUuid thinkridercontroller::SERVICE_UUID =
|
||||
QBluetoothUuid(QStringLiteral("0000fea0-0000-1000-8000-00805f9b34fb"));
|
||||
const QBluetoothUuid thinkridercontroller::CHARACTERISTIC_UUID =
|
||||
QBluetoothUuid(QStringLiteral("0000fea1-0000-1000-8000-00805f9b34fb"));
|
||||
|
||||
// Button patterns (from swiftcontrol implementation)
|
||||
const QByteArray thinkridercontroller::SHIFT_UP_PATTERN = QByteArray::fromHex("f3050301fc");
|
||||
const QByteArray thinkridercontroller::SHIFT_DOWN_PATTERN = QByteArray::fromHex("f3050300fb");
|
||||
|
||||
thinkridercontroller::thinkridercontroller(bluetoothdevice *parentDevice) {
|
||||
this->parentDevice = parentDevice;
|
||||
}
|
||||
|
||||
void thinkridercontroller::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::disconnectBluetooth() {
|
||||
qDebug() << QStringLiteral("thinkridercontroller::disconnect") << m_control;
|
||||
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
|
||||
const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
|
||||
qDebug() << QStringLiteral("thinkridercontroller << ") << newValue.toHex(' ');
|
||||
|
||||
// Check for shift up pattern
|
||||
if (newValue == SHIFT_UP_PATTERN) {
|
||||
qDebug() << QStringLiteral("Thinkrider: Shift UP detected");
|
||||
emit plus();
|
||||
}
|
||||
// Check for shift down pattern
|
||||
else if (newValue == SHIFT_DOWN_PATTERN) {
|
||||
qDebug() << QStringLiteral("Thinkrider: Shift DOWN detected");
|
||||
emit minus();
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state();
|
||||
if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) {
|
||||
qDebug() << QStringLiteral("not all services discovered");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (state != QLowEnergyService::ServiceState::ServiceDiscovered) {
|
||||
qDebug() << QStringLiteral("ignoring this state");
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("all services discovered!");
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
if (s->state() == QLowEnergyService::ServiceDiscovered) {
|
||||
// establish hook into notifications
|
||||
connect(s, &QLowEnergyService::characteristicChanged, this, &thinkridercontroller::characteristicChanged);
|
||||
connect(s, &QLowEnergyService::characteristicRead, this, &thinkridercontroller::characteristicChanged);
|
||||
connect(
|
||||
s, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &thinkridercontroller::errorService);
|
||||
connect(s, &QLowEnergyService::descriptorWritten, this, &thinkridercontroller::descriptorWritten);
|
||||
|
||||
qDebug() << s->serviceUuid() << QStringLiteral("connected!");
|
||||
|
||||
auto characteristics_list = s->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle();
|
||||
auto descriptors_list = c.descriptors();
|
||||
for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) {
|
||||
qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle();
|
||||
}
|
||||
|
||||
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
|
||||
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle()
|
||||
<< QStringLiteral(" is not valid");
|
||||
}
|
||||
|
||||
qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("notification subscribed!");
|
||||
} else if ((c.properties() & QLowEnergyCharacteristic::Indicate) ==
|
||||
QLowEnergyCharacteristic::Indicate) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x02);
|
||||
descriptor.append((char)0x00);
|
||||
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
|
||||
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid()
|
||||
<< c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle()
|
||||
<< QStringLiteral(" is not valid");
|
||||
}
|
||||
|
||||
qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("indication subscribed!");
|
||||
}
|
||||
|
||||
if (c.uuid() == CHARACTERISTIC_UUID) {
|
||||
qDebug() << QStringLiteral("Thinkrider characteristic found");
|
||||
gattNotifyCharacteristic = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void thinkridercontroller::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void thinkridercontroller::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
if (gattCommunicationChannelService.constLast()) {
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&thinkridercontroller::stateChanged);
|
||||
gattCommunicationChannelService.constLast()->discoverDetails();
|
||||
} else {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void thinkridercontroller::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("thinkridercontroller::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("thinkridercontroller::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void thinkridercontroller::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &thinkridercontroller::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &thinkridercontroller::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &thinkridercontroller::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &thinkridercontroller::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("LowEnergy controller disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool thinkridercontroller::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
void thinkridercontroller::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
|
||||
if (m_control)
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
73
src/devices/thinkridercontroller/thinkridercontroller.h
Normal file
73
src/devices/thinkridercontroller/thinkridercontroller.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef THINKRIDERCONTROLLER_H
|
||||
#define THINKRIDERCONTROLLER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
|
||||
#include "devices/bluetoothdevice.h"
|
||||
|
||||
class thinkridercontroller : public bluetoothdevice {
|
||||
Q_OBJECT
|
||||
public:
|
||||
thinkridercontroller(bluetoothdevice *parentDevice);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
// Thinkrider VS200 UUIDs
|
||||
static const QBluetoothUuid SERVICE_UUID;
|
||||
static const QBluetoothUuid CHARACTERISTIC_UUID;
|
||||
|
||||
// Button patterns
|
||||
static const QByteArray SHIFT_UP_PATTERN;
|
||||
static const QByteArray SHIFT_DOWN_PATTERN;
|
||||
|
||||
QList<QLowEnergyService *> gattCommunicationChannelService;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
|
||||
bluetoothdevice *parentDevice = nullptr;
|
||||
|
||||
bool initDone = false;
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void plus();
|
||||
void minus();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
void disconnectBluetooth();
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
private slots:
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // THINKRIDERCONTROLLER_H
|
||||
@@ -539,8 +539,19 @@ double treadmill::treadmillInclinationOverride(double Inclination) {
|
||||
}
|
||||
|
||||
void treadmill::evaluateStepCount() {
|
||||
// Auto-detect cadence format: if < 120, assume it's per-leg and needs doubling for step count
|
||||
double effectiveCadence = (Cadence.value() < 120 && Cadence.value() > 0) ? Cadence.value() * 2 : Cadence.value();
|
||||
// Auto-detect cadence format: if per-leg, needs doubling for step count
|
||||
// Running (>6 km/h): double if cadence < 120
|
||||
// Walking (<6 km/h): double if cadence < 60
|
||||
double effectiveCadence = Cadence.value();
|
||||
|
||||
if (Speed.value() > 6.0 && Cadence.value() < 120 && Cadence.value() > 0) {
|
||||
// Running: likely per-leg cadence, double it
|
||||
effectiveCadence = Cadence.value() * 2;
|
||||
} else if (Speed.value() > 0 && Speed.value() <= 6.0 && Cadence.value() < 60 && Cadence.value() > 0) {
|
||||
// Walking: likely per-leg cadence, double it
|
||||
effectiveCadence = Cadence.value() * 2;
|
||||
}
|
||||
|
||||
StepCount += (Cadence.lastChanged().msecsTo(QDateTime::currentDateTime())) * (effectiveCadence / 60000);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ class treadmill : public bluetoothdevice {
|
||||
|
||||
signals:
|
||||
void tapeStarted();
|
||||
void buttonHWStart(); // Physical start button pressed on hardware
|
||||
void buttonHWPause(); // Physical pause button pressed on hardware
|
||||
void buttonHWStop(); // Physical stop button pressed on hardware
|
||||
|
||||
protected:
|
||||
volatile double requestSpeed = -1;
|
||||
|
||||
@@ -84,6 +84,29 @@ void trxappgateusbelliptical::update() {
|
||||
QSettings settings;
|
||||
update_metrics(true, watts());
|
||||
|
||||
// Restore resistance after reconnection and init
|
||||
if (needsResistanceRestore && lastResistanceBeforeDisconnection > 0) {
|
||||
qDebug() << QStringLiteral("Restoring resistance after reconnection:") << lastResistanceBeforeDisconnection;
|
||||
forceResistance(lastResistanceBeforeDisconnection);
|
||||
needsResistanceRestore = false;
|
||||
lastResistanceBeforeDisconnection = -1;
|
||||
}
|
||||
|
||||
// Calculate time since last valid packet
|
||||
qint64 msSinceLastValidPacket = lastValidPacketTime.msecsTo(QDateTime::currentDateTime());
|
||||
|
||||
// If we haven't received a valid packet for more than 5 seconds, reinitialize
|
||||
if (msSinceLastValidPacket > 5000) {
|
||||
qDebug() << QStringLiteral("NO VALID PACKETS for") << (msSinceLastValidPacket / 1000.0)
|
||||
<< QStringLiteral("seconds. Reinitializing connection...");
|
||||
|
||||
// Reset timer
|
||||
lastValidPacketTime = QDateTime::currentDateTime();
|
||||
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance < 1)
|
||||
@@ -191,10 +214,24 @@ void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacterist
|
||||
|
||||
lastPacket = newValue;
|
||||
|
||||
if(newValue.length() != 21) {
|
||||
lastValidPacketTime = QDateTime::currentDateTime();
|
||||
|
||||
// Check for invalid packet length first
|
||||
bool isValidPacket = (newValue.length() == 21);
|
||||
|
||||
if (!isValidPacket) {
|
||||
// Invalid packet length - log and return
|
||||
qDebug() << QStringLiteral("Invalid packet length:") << newValue.length();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log controller errors but don't block processing of valid packets
|
||||
bool hasError = (m_control->error() != QLowEnergyController::NoError);
|
||||
if (hasError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
// Continue processing - the packet is still valid
|
||||
}
|
||||
|
||||
Resistance = newValue.at(18) - 1;
|
||||
Speed = GetSpeedFromPacket(newValue);
|
||||
Cadence = (GetCadenceFromPacket(newValue) * cadence_gain) + cadence_offset;
|
||||
@@ -227,10 +264,6 @@ void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacterist
|
||||
emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value()));
|
||||
// debug("Current Distance: " + QString::number(distance));
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
void trxappgateusbelliptical::btinit() {
|
||||
@@ -497,9 +530,26 @@ bool trxappgateusbelliptical::connected() {
|
||||
void trxappgateusbelliptical::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
qDebug() << QStringLiteral("trying to connect back again in 3 seconds...");
|
||||
|
||||
// Save current resistance before disconnection
|
||||
if (Resistance.value() > 0) {
|
||||
lastResistanceBeforeDisconnection = Resistance.value();
|
||||
needsResistanceRestore = true;
|
||||
qDebug() << QStringLiteral("Saved resistance before disconnection:") << lastResistanceBeforeDisconnection;
|
||||
}
|
||||
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
|
||||
// Schedule reconnection after 3 seconds
|
||||
QTimer::singleShot(3000, this, [this]() {
|
||||
if (m_control && m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
qDebug() << QStringLiteral("Reconnection timer fired, attempting to reconnect...");
|
||||
// Reset the last valid packet timer
|
||||
lastValidPacketTime = QDateTime::currentDateTime();
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ class trxappgateusbelliptical : public elliptical {
|
||||
|
||||
uint8_t sec1Update = 0;
|
||||
QByteArray lastPacket;
|
||||
QDateTime lastValidPacketTime = QDateTime::currentDateTime();
|
||||
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
uint8_t firstStateChanged = 0;
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
@@ -69,6 +70,9 @@ class trxappgateusbelliptical : public elliptical {
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
resistance_t lastResistanceBeforeDisconnection = -1;
|
||||
bool needsResistanceRestore = false;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
|
||||
|
||||
@@ -693,6 +693,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
if (newvalue.length() == 15) {
|
||||
Speed = (double)((((uint8_t)newvalue.at(10)) << 8) | ((uint8_t)newvalue.at(9))) / 100.0;
|
||||
Cadence = newvalue.at(6);
|
||||
m_watt = elliptical::watts();
|
||||
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
|
||||
@@ -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
|
||||
@@ -128,8 +128,10 @@ void FitDatabaseProcessor::processDirectory(const QString& dirPath) {
|
||||
|
||||
void FitDatabaseProcessor::processFile(const QString& filePath) {
|
||||
if (!db.isOpen()) {
|
||||
emit error("Failed to initialize database for single file processing");
|
||||
return;
|
||||
if (!initializeDatabase()) {
|
||||
emit error("Failed to initialize database for single file processing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!processFitFile(filePath)) {
|
||||
|
||||
@@ -391,6 +391,9 @@ bool GarminConnect::fetchCsrfToken()
|
||||
bool GarminConnect::performLogin(const QString &email, const QString &password, bool suppressMfaSignal)
|
||||
{
|
||||
qDebug() << "GarminConnect: Performing login...";
|
||||
qDebug() << "GarminConnect: Using domain:" << m_domain;
|
||||
qDebug() << "GarminConnect: SSO URL:" << ssoUrl();
|
||||
qDebug() << "GarminConnect: Connect API URL:" << connectApiUrl();
|
||||
|
||||
QString ssoEmbedUrl = ssoUrl() + SSO_EMBED_PATH;
|
||||
|
||||
@@ -452,15 +455,54 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
qDebug() << "GarminConnect: Login response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(300);
|
||||
|
||||
// Check for success title (like Python garth library)
|
||||
// Check page title (like Python garth library)
|
||||
// garth checks ONLY the title for MFA detection, not the body
|
||||
// This is important because some servers (like garmin.cn) may have "MFA" text
|
||||
// in their Success page HTML body, which would cause false positives
|
||||
QString pageTitle;
|
||||
QRegularExpression titleRegex("<title>(.+?)</title>");
|
||||
QRegularExpressionMatch titleMatch = titleRegex.match(response);
|
||||
if (titleMatch.hasMatch()) {
|
||||
QString title = titleMatch.captured(1);
|
||||
qDebug() << "GarminConnect: Page title:" << title;
|
||||
if (title == "Success") {
|
||||
qDebug() << "GarminConnect: Login successful (Success page detected)";
|
||||
pageTitle = titleMatch.captured(1);
|
||||
qDebug() << "GarminConnect: Page title:" << pageTitle;
|
||||
}
|
||||
|
||||
// Check if MFA is required by looking at the TITLE (garth approach)
|
||||
// This is more reliable than checking the body which may contain "MFA" in scripts/URLs
|
||||
if (pageTitle.contains("MFA", Qt::CaseInsensitive)) {
|
||||
m_lastError = "MFA Required";
|
||||
qDebug() << "GarminConnect: MFA detected in page title";
|
||||
|
||||
// Extract new CSRF token from MFA page - try multiple patterns
|
||||
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
|
||||
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
|
||||
|
||||
QRegularExpressionMatch match = csrfRegex1.match(response);
|
||||
if (!match.hasMatch()) {
|
||||
match = csrfRegex2.match(response);
|
||||
}
|
||||
if (match.hasMatch()) {
|
||||
m_csrfToken = match.captured(1);
|
||||
qDebug() << "GarminConnect: CSRF token from MFA page:" << m_csrfToken.left(20) << "...";
|
||||
}
|
||||
|
||||
// Update cookies
|
||||
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
|
||||
|
||||
if (!suppressMfaSignal) {
|
||||
qDebug() << "GarminConnect: Emitting mfaRequired signal";
|
||||
emit mfaRequired();
|
||||
} else {
|
||||
qDebug() << "GarminConnect: MFA required but signal suppressed (retrying with MFA code)";
|
||||
}
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if login was successful (title is "Success")
|
||||
if (pageTitle == "Success") {
|
||||
qDebug() << "GarminConnect: Login successful (Success page detected)";
|
||||
// Continue to extract ticket below
|
||||
}
|
||||
|
||||
// Check for error messages in response
|
||||
@@ -549,39 +591,17 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if MFA is required (legacy check for non-redirect MFA)
|
||||
if (response.contains("MFA", Qt::CaseInsensitive) ||
|
||||
response.contains("Enter MFA Code", Qt::CaseInsensitive)) {
|
||||
m_lastError = "MFA Required";
|
||||
qDebug() << "GarminConnect: MFA content detected in response";
|
||||
|
||||
// Extract new CSRF token from MFA page - try multiple patterns
|
||||
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
|
||||
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
|
||||
|
||||
QRegularExpressionMatch match = csrfRegex1.match(response);
|
||||
if (!match.hasMatch()) {
|
||||
match = csrfRegex2.match(response);
|
||||
}
|
||||
if (match.hasMatch()) {
|
||||
m_csrfToken = match.captured(1);
|
||||
}
|
||||
|
||||
// Update cookies
|
||||
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
|
||||
|
||||
if (!suppressMfaSignal) {
|
||||
emit mfaRequired();
|
||||
}
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract ticket from response URL (already declared above)
|
||||
if (responseUrl.isEmpty()) {
|
||||
responseUrl = reply->url();
|
||||
}
|
||||
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response URL:" << responseUrl.toString();
|
||||
qDebug() << "GarminConnect: Response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Full response body:" << response;
|
||||
}
|
||||
|
||||
QUrlQuery responseQuery(responseUrl);
|
||||
QString ticket = responseQuery.queryItemValue("ticket");
|
||||
|
||||
@@ -599,6 +619,8 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
if (match.hasMatch()) {
|
||||
ticket = match.captured(1);
|
||||
qDebug() << "GarminConnect: Found ticket with fallback pattern:" << ticket.left(20) << "...";
|
||||
} else if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: No ticket patterns matched in response body";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -608,6 +630,9 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
|
||||
if (ticket.isEmpty()) {
|
||||
m_lastError = "Failed to extract ticket from login response";
|
||||
qDebug() << "GarminConnect:" << m_lastError;
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -708,8 +733,12 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
qDebug() << "GarminConnect: MFA response status code:" << statusCode;
|
||||
qDebug() << "GarminConnect: MFA response redirect URL:" << responseUrl.toString();
|
||||
|
||||
// If no redirect, log response body to understand what happened
|
||||
if (responseUrl.isEmpty()) {
|
||||
// Log detailed response information
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: MFA response length:" << response.length();
|
||||
qDebug() << "GarminConnect: Full MFA response body:" << response;
|
||||
} else if (responseUrl.isEmpty()) {
|
||||
// If no redirect, log response body to understand what happened (non-verbose)
|
||||
qDebug() << "GarminConnect: MFA response body (first 500 chars):" << response.left(500);
|
||||
}
|
||||
|
||||
@@ -748,6 +777,9 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
|
||||
// If not found in redirect URL, try response body
|
||||
if (ticket.isEmpty() && !response.isEmpty()) {
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Attempting to extract ticket from MFA response body";
|
||||
}
|
||||
// Try multiple patterns for ticket extraction
|
||||
QRegularExpression ticketRegex1("embed\\?ticket=([^\"]+)\"");
|
||||
QRegularExpression ticketRegex2("ticket=([^&\"']+)");
|
||||
@@ -761,6 +793,16 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
if (match.hasMatch()) {
|
||||
ticket = match.captured(1);
|
||||
qDebug() << "GarminConnect: Found ticket in response body (pattern 2):" << ticket.left(20) << "...";
|
||||
} else if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: No MFA ticket patterns matched. Checking for other patterns...";
|
||||
// Check for JSON format
|
||||
if (response.contains("ticket")) {
|
||||
qDebug() << "GarminConnect: Response contains 'ticket' keyword, may be JSON or different format";
|
||||
}
|
||||
// Check for common response patterns
|
||||
if (response.contains("\"")) {
|
||||
qDebug() << "GarminConnect: Response contains quoted strings (may be JSON)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,6 +812,9 @@ void GarminConnect::handleMfaReplyFinished()
|
||||
if (ticket.isEmpty()) {
|
||||
m_lastError = "Failed to extract ticket after MFA";
|
||||
qDebug() << "GarminConnect:" << m_lastError;
|
||||
if (DEBUG_GARMIN_VERBOSE) {
|
||||
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
|
||||
}
|
||||
emit authenticationFailed(m_lastError);
|
||||
return;
|
||||
}
|
||||
@@ -1401,6 +1446,7 @@ void GarminConnect::loadTokensFromSettings()
|
||||
m_oauth1Token.oauth_token = settings.value(QZSettings::garmin_oauth1_token, QZSettings::default_garmin_oauth1_token).toString();
|
||||
m_oauth1Token.oauth_token_secret = settings.value(QZSettings::garmin_oauth1_token_secret, QZSettings::default_garmin_oauth1_token_secret).toString();
|
||||
m_domain = settings.value(QZSettings::garmin_domain, QZSettings::default_garmin_domain).toString();
|
||||
qDebug() << "GarminConnect: Loaded Garmin domain from settings:" << m_domain;
|
||||
|
||||
if (!m_oauth2Token.access_token.isEmpty()) {
|
||||
qDebug() << "GarminConnect: Loaded tokens from settings (OAuth1 + OAuth2)";
|
||||
|
||||
@@ -176,6 +176,7 @@ private:
|
||||
static constexpr const char* SSO_URL_PATH = "/sso/signin";
|
||||
static constexpr const char* SSO_EMBED_PATH = "/sso/embed";
|
||||
static constexpr const char* OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json";
|
||||
static constexpr bool DEBUG_GARMIN_VERBOSE = false; // Set to true for detailed response logging (may contain sensitive data)
|
||||
|
||||
// Private methods
|
||||
QString ssoUrl() const { return QString("https://sso.%1").arg(m_domain); }
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
|
||||
gpx::gpx(QObject *parent) : QObject(parent) {}
|
||||
|
||||
QList<gpx_altitude_point_for_treadmill> gpx::open(const QString &gpx, BLUETOOTH_TYPE device_type) {
|
||||
QList<gpx_altitude_point_for_treadmill> gpx::open(const QString &gpx, BLUETOOTH_TYPE device_type, bool forceNoLoop) {
|
||||
QSettings settings;
|
||||
const double meter_limit_for_auto_loop = 300;
|
||||
bool treadmill_force_speed =
|
||||
settings.value(QZSettings::treadmill_force_speed, QZSettings::default_treadmill_force_speed).toBool();
|
||||
bool gpx_loop = settings.value(QZSettings::gpx_loop, QZSettings::default_gpx_loop).toBool();
|
||||
bool gpx_loop = forceNoLoop ? false : settings.value(QZSettings::gpx_loop, QZSettings::default_gpx_loop).toBool();
|
||||
|
||||
if(device_type == BIKE)
|
||||
treadmill_force_speed = false;
|
||||
@@ -71,6 +71,7 @@ QList<gpx_altitude_point_for_treadmill> gpx::open(const QString &gpx, BLUETOOTH_
|
||||
gpx_point pP = this->points.constFirst();
|
||||
|
||||
if (treadmill_force_speed) {
|
||||
double totDistance = 0;
|
||||
|
||||
// starting point
|
||||
gpx_altitude_point_for_treadmill g;
|
||||
@@ -97,6 +98,7 @@ QList<gpx_altitude_point_for_treadmill> gpx::open(const QString &gpx, BLUETOOTH_
|
||||
gpx_altitude_point_for_treadmill g;
|
||||
g.seconds = this->points.constFirst().time.secsTo(pP.time);
|
||||
g.distance = distance / 1000.0;
|
||||
totDistance += g.distance;
|
||||
g.speed = (distance / 1000.0) * (3600 / dT);
|
||||
g.inclination = (elevation / distance) * 100;
|
||||
g.elevation = this->points.at(i).p.altitude();
|
||||
@@ -104,6 +106,7 @@ QList<gpx_altitude_point_for_treadmill> gpx::open(const QString &gpx, BLUETOOTH_
|
||||
g.longitude = pP.p.longitude();
|
||||
inclinationList.append(g);
|
||||
}
|
||||
this->totalDistance = totDistance;
|
||||
}
|
||||
if (inclinationList.empty()) {
|
||||
gpx_point pP = this->points.constFirst();
|
||||
@@ -151,6 +154,7 @@ QList<gpx_altitude_point_for_treadmill> gpx::open(const QString &gpx, BLUETOOTH_
|
||||
<< g.longitude << totDistance << pP.time;*/
|
||||
inclinationList.append(g);
|
||||
}
|
||||
this->totalDistance = totDistance;
|
||||
}
|
||||
|
||||
return inclinationList;
|
||||
|
||||
@@ -29,13 +29,15 @@ class gpx : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit gpx(QObject *parent = nullptr);
|
||||
QList<gpx_altitude_point_for_treadmill> open(const QString &gpx, BLUETOOTH_TYPE device_type);
|
||||
QList<gpx_altitude_point_for_treadmill> open(const QString &gpx, BLUETOOTH_TYPE device_type, bool forceNoLoop = false);
|
||||
static void save(const QString &filename, QList<SessionLine> session, BLUETOOTH_TYPE type);
|
||||
QString getVideoURL() {return videoUrl;}
|
||||
double getTotalDistance() const {return totalDistance;}
|
||||
|
||||
private:
|
||||
QList<gpx_point> points;
|
||||
QString videoUrl = "";
|
||||
double totalDistance = 0.0;
|
||||
|
||||
signals:
|
||||
};
|
||||
|
||||
325
src/homeform.cpp
325
src/homeform.cpp
@@ -312,6 +312,8 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
QStringLiteral("0"), true, QStringLiteral("autoVirtualShiftingSprint"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Sprint", QStringLiteral("red"));
|
||||
powerAvg = new DataObject(QStringLiteral("Power Avg"), QStringLiteral("icons/icons/watt.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("powerAvg"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Off", QStringLiteral("grey"));
|
||||
hrv = new DataObject(QStringLiteral("HRV (ms)"), QStringLiteral("icons/icons/heart_red.png"),
|
||||
QStringLiteral("0"), false, QStringLiteral("hrv"), 48, labelFontSize);
|
||||
pidHR = new DataObject(QStringLiteral("PID Heart"), QStringLiteral("icons/icons/heart_red.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("pid_hr"), 48, labelFontSize);
|
||||
extIncline = new DataObject(QStringLiteral("Ext.Inclin.(%)"), QStringLiteral("icons/icons/inclination.png"),
|
||||
@@ -558,6 +560,10 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
&homeform::pelotonOffset_Minus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Plus, this, &homeform::gearUp);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Minus, this, &homeform::gearDown);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::speed_Plus, this, &homeform::speedPlus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::speed_Minus, this, &homeform::speedMinus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::inclination_Plus, this, &homeform::inclinationPlus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::inclination_Minus, this, &homeform::inclinationMinus);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonOffset, this, &homeform::pelotonOffset);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonAskStart, this, &homeform::pelotonAskStart);
|
||||
connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::peloton_start_workout, this,
|
||||
@@ -1471,6 +1477,12 @@ void homeform::trainProgramSignals() {
|
||||
((elliptical *)bluetoothManager->device()), &elliptical::changeRequestedPelotonResistance);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::tapeStarted, trainProgram,
|
||||
&trainprogram::onTapeStarted);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStart, this,
|
||||
&homeform::StartFromDevice);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWPause, this,
|
||||
&homeform::PauseFromDevice);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStop, this,
|
||||
&homeform::StopFromDevice);
|
||||
disconnect(((bike *)bluetoothManager->device()), &bike::bikeStarted, trainProgram,
|
||||
&trainprogram::onTapeStarted);
|
||||
disconnect(trainProgram, &trainprogram::changeGeoPosition, bluetoothManager->device(),
|
||||
@@ -1497,6 +1509,12 @@ void homeform::trainProgramSignals() {
|
||||
&treadmill::changeSpeedAndInclination);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::tapeStarted, trainProgram,
|
||||
&trainprogram::onTapeStarted);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStart, this,
|
||||
&homeform::StartFromDevice);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWPause, this,
|
||||
&homeform::PauseFromDevice);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStop, this,
|
||||
&homeform::StopFromDevice);
|
||||
connect(trainProgram, &trainprogram::changePower, ((treadmill *)bluetoothManager->device()), &treadmill::changePower);
|
||||
} else if (bluetoothManager->device()->deviceType() == BIKE) {
|
||||
connect(trainProgram, &trainprogram::changeCadence, ((bike *)bluetoothManager->device()),
|
||||
@@ -1598,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);
|
||||
@@ -1726,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);
|
||||
@@ -2115,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);
|
||||
@@ -2504,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);
|
||||
@@ -2978,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);
|
||||
@@ -3344,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);
|
||||
@@ -3707,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);
|
||||
@@ -5118,6 +5188,21 @@ void homeform::Start_inner(bool send_event_to_device) {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::StartFromDevice() {
|
||||
qDebug() << QStringLiteral("Physical start button pressed on device");
|
||||
Start_inner(false); // false = don't send command back to device (it already started)
|
||||
}
|
||||
|
||||
void homeform::PauseFromDevice() {
|
||||
qDebug() << QStringLiteral("Physical pause button pressed on device");
|
||||
Start_inner(false); // false = don't send command back to device
|
||||
}
|
||||
|
||||
void homeform::StopFromDevice() {
|
||||
qDebug() << QStringLiteral("Physical stop button pressed on device - stopping app");
|
||||
Stop();
|
||||
}
|
||||
|
||||
void homeform::StartRequested() {
|
||||
Start();
|
||||
m_stopRequested = false;
|
||||
@@ -5158,6 +5243,17 @@ void homeform::Stop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothManager->device()) {
|
||||
if (bluetoothManager->device()->deviceType() == TREADMILL) {
|
||||
QTime zero(0, 0, 0, 0);
|
||||
if (bluetoothManager->device()->currentSpeed().value() == 0.0 &&
|
||||
zero.secsTo(bluetoothManager->device()->elapsedTime()) == 0) {
|
||||
qDebug() << QStringLiteral("Stop pressed - nothing to do. Elapsed time is 0 and current speed is 0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
// due to #857
|
||||
if (!settings.value(QZSettings::peloton_companion_workout_ocr, QZSettings::default_companion_peloton_workout_ocr)
|
||||
@@ -5169,16 +5265,6 @@ void homeform::Stop() {
|
||||
m_speech.say("Stop pressed");
|
||||
|
||||
if (bluetoothManager->device()) {
|
||||
|
||||
if (bluetoothManager->device()->deviceType() == TREADMILL) {
|
||||
QTime zero(0, 0, 0, 0);
|
||||
if (bluetoothManager->device()->currentSpeed().value() == 0.0 &&
|
||||
zero.secsTo(bluetoothManager->device()->elapsedTime()) == 0) {
|
||||
qDebug() << QStringLiteral("Stop pressed - nothing to do. Elapsed time is 0 and current speed is 0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bluetoothManager->device()->stop(false);
|
||||
}
|
||||
|
||||
@@ -5368,6 +5454,7 @@ void homeform::update() {
|
||||
double stepCount = 0;
|
||||
|
||||
bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
bool weight_kg_unit = settings.value(QZSettings::weight_kg_unit, QZSettings::default_weight_kg_unit).toBool();
|
||||
double ftpSetting = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble();
|
||||
double unit_conversion = 1.0;
|
||||
double meter_feet_conversion = 1.0;
|
||||
@@ -5481,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));
|
||||
@@ -5651,7 +5750,7 @@ void homeform::update() {
|
||||
datetime->setValue(formattedTime);
|
||||
watts = bluetoothManager->device()->wattsMetricforUI();
|
||||
watt->setValue(QString::number(watts, 'f', 0));
|
||||
weightLoss->setValue(QString::number(miles ? bluetoothManager->device()->weightLoss() * 35.274
|
||||
weightLoss->setValue(QString::number((miles && !weight_kg_unit) ? bluetoothManager->device()->weightLoss() * 35.274
|
||||
: bluetoothManager->device()->weightLoss(),
|
||||
'f', 2));
|
||||
|
||||
@@ -6724,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)
|
||||
@@ -7053,34 +7164,74 @@ void homeform::update() {
|
||||
}
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == BIKE) {
|
||||
double step = 1;
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableBySoftware();
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableByHardware();
|
||||
bool inclinationAvailable = ((bike*)bluetoothManager->device())->inclinationAvailableBySoftware();
|
||||
|
||||
if(ergMode) {
|
||||
step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
}
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
if(ergMode)
|
||||
// Use power control for bikes with erg mode support
|
||||
double step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt - step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone) && ((maxResistance >= currentResistance + step && !ergMode) || ergMode)) {
|
||||
if(ergMode)
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
}
|
||||
}
|
||||
} else if(inclinationAvailable) {
|
||||
// Use inclination control for bikes without erg mode but with inclination support (e.g., ftmsbike)
|
||||
double step = 0.5;
|
||||
double currentInclination = ((bike *)bluetoothManager->device())->currentInclination().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination - step, currentInclination - step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination + step, currentInclination + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination + step, currentInclination + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to resistance control for bikes without erg mode or inclination
|
||||
double step = 1;
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableBySoftware();
|
||||
if(ergMode) {
|
||||
step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
}
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
if(ergMode)
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt - step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone) && ((maxResistance >= currentResistance + step && !ergMode) || ergMode)) {
|
||||
if(ergMode)
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
if(ergMode)
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == ROWING) {
|
||||
@@ -7201,24 +7352,63 @@ void homeform::update() {
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == BIKE) {
|
||||
|
||||
const int step = 1;
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - currentResistance:") << currentResistance
|
||||
<< QStringLiteral("maxResistance:") << maxResistance;
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableByHardware();
|
||||
bool inclinationAvailable = ((bike*)bluetoothManager->device())->inclinationAvailableBySoftware();
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << (currentResistance - step);
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s() &&
|
||||
currentResistance < maxResistance) {
|
||||
resistance_t newResistance = std::min(static_cast<resistance_t>(currentResistance + step), static_cast<resistance_t>(maxResistance));
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << newResistance;
|
||||
((bike *)bluetoothManager->device())->changeResistance(newResistance);
|
||||
if (ergMode) {
|
||||
// Use power control for bikes with erg mode support
|
||||
const int step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - ergMode enabled, currentPower:") << current_target_watt;
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING power from")
|
||||
<< current_target_watt << QStringLiteral("to") << (current_target_watt - step);
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING power from")
|
||||
<< current_target_watt << QStringLiteral("to") << (current_target_watt + step);
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
}
|
||||
} else if (inclinationAvailable) {
|
||||
// Use inclination control for bikes without erg mode but with inclination support (e.g., ftmsbike)
|
||||
const double step = 0.5;
|
||||
double currentInclination = ((bike *)bluetoothManager->device())->currentInclination().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - Using inclination control, currentInclination:") << currentInclination;
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING inclination from")
|
||||
<< currentInclination << QStringLiteral("to") << (currentInclination - step);
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination - step, currentInclination - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING inclination from")
|
||||
<< currentInclination << QStringLiteral("to") << (currentInclination + step);
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination + step, currentInclination + step);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
}
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
const int step = 1;
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - currentResistance:") << currentResistance
|
||||
<< QStringLiteral("maxResistance:") << maxResistance;
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << (currentResistance - step);
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s() &&
|
||||
currentResistance < maxResistance) {
|
||||
resistance_t newResistance = std::min(static_cast<resistance_t>(currentResistance + step), static_cast<resistance_t>(maxResistance));
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << newResistance;
|
||||
((bike *)bluetoothManager->device())->changeResistance(newResistance);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
}
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == ROWING) {
|
||||
|
||||
@@ -7536,10 +7726,17 @@ void homeform::update() {
|
||||
}
|
||||
}
|
||||
|
||||
if(bluetoothManager->device()->currentSpeed().value() > 0 && !isinf(bluetoothManager->device()->currentSpeed().value()))
|
||||
bluetoothManager->device()->addCurrentDistance1s((bluetoothManager->device()->currentSpeed().value() / 3600.0));
|
||||
|
||||
qDebug() << "Current Distance 1s:" << bluetoothManager->device()->currentDistance1s().value() << bluetoothManager->device()->currentSpeed().value() << watts;
|
||||
bool treadmill_direct_distance = settings.value(QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance).toBool();
|
||||
double distance1s = 0;
|
||||
if (treadmill_direct_distance) {
|
||||
distance1s = bluetoothManager->device()->odometer();
|
||||
} else {
|
||||
if(bluetoothManager->device()->currentSpeed().value() > 0 && !isinf(bluetoothManager->device()->currentSpeed().value()))
|
||||
bluetoothManager->device()->addCurrentDistance1s((bluetoothManager->device()->currentSpeed().value() / 3600.0));
|
||||
distance1s = bluetoothManager->device()->currentDistance1s().value();
|
||||
}
|
||||
|
||||
qDebug() << "Current Distance 1s:" << distance1s << bluetoothManager->device()->currentSpeed().value() << watts;
|
||||
|
||||
// Calculate current elapsed time in seconds
|
||||
uint32_t currentElapsedSeconds = bluetoothManager->device()->elapsedTime().second() +
|
||||
@@ -7561,7 +7758,7 @@ void homeform::update() {
|
||||
uint32_t lastRecordedTime = Session.last().elapsedTime;
|
||||
for (int i = 1; i <= missedSeconds; i++) {
|
||||
SessionLine gapFill(
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, distance1s,
|
||||
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
|
||||
pace, cadence, bluetoothManager->device()->calories().value(),
|
||||
bluetoothManager->device()->elevationGain().value(),
|
||||
@@ -7571,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);
|
||||
@@ -7596,7 +7794,7 @@ void homeform::update() {
|
||||
}
|
||||
|
||||
SessionLine s(
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, distance1s,
|
||||
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
|
||||
pace, cadence, bluetoothManager->device()->calories().value(),
|
||||
bluetoothManager->device()->elevationGain().value(),
|
||||
@@ -7607,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);
|
||||
|
||||
@@ -8145,11 +8345,14 @@ void homeform::gpxpreview_open_clicked(const QUrl &fileName) {
|
||||
|
||||
if (!file.fileName().isEmpty()) {
|
||||
gpx g;
|
||||
auto g_list = g.open(file.fileName(), bluetoothManager->device() ? bluetoothManager->device()->deviceType() : BIKE);
|
||||
// Force no loop for preview to show actual GPX distance
|
||||
auto g_list = g.open(file.fileName(), bluetoothManager->device() ? bluetoothManager->device()->deviceType() : BIKE, true);
|
||||
gpx_preview.clearPath();
|
||||
for (const auto &p : g_list) {
|
||||
gpx_preview.addCoordinate(QGeoCoordinate(p.latitude, p.longitude, p.elevation));
|
||||
}
|
||||
// Set distance BEFORE setGeoPath to ensure QML onGeopathChanged has correct value
|
||||
pathController.setDistance(g.getTotalDistance());
|
||||
pathController.setGeoPath(gpx_preview);
|
||||
pathController.setCenter(gpx_preview.center());
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ class homeform : public QObject {
|
||||
Q_PROPERTY(QString previewWorkoutDescription READ previewWorkoutDescription NOTIFY previewWorkoutDescriptionChanged)
|
||||
Q_PROPERTY(QString previewWorkoutTags READ previewWorkoutTags NOTIFY previewWorkoutTagsChanged)
|
||||
Q_PROPERTY(bool miles_unit READ miles_unit)
|
||||
Q_PROPERTY(bool iPadMultiWindowMode READ iPadMultiWindowMode)
|
||||
|
||||
Q_PROPERTY(bool currentCoordinateValid READ currentCoordinateValid)
|
||||
Q_PROPERTY(bool trainProgramLoadedWithVideo READ trainProgramLoadedWithVideo)
|
||||
@@ -705,6 +706,18 @@ class homeform : public QObject {
|
||||
return settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
}
|
||||
|
||||
bool iPadMultiWindowMode() {
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
return lockscreen::isInMultiWindowMode();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool currentCoordinateValid() {
|
||||
if (bluetoothManager && bluetoothManager->device()) {
|
||||
return bluetoothManager->device()->currentCordinate().isValid();
|
||||
@@ -808,6 +821,7 @@ class homeform : public QObject {
|
||||
DataObject *autoVirtualShiftingClimb;
|
||||
DataObject *autoVirtualShiftingSprint;
|
||||
DataObject *powerAvg;
|
||||
DataObject *hrv;
|
||||
|
||||
private:
|
||||
static homeform *m_singleton;
|
||||
@@ -1043,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();
|
||||
@@ -1053,6 +1071,9 @@ class homeform : public QObject {
|
||||
void strava_upload_file_prepare();
|
||||
void garmin_upload_file_prepare();
|
||||
void handleRestoreDefaultWheelDiameter();
|
||||
void StartFromDevice(); // Called when physical start button pressed on hardware
|
||||
void PauseFromDevice(); // Called when physical pause button pressed on hardware
|
||||
void StopFromDevice(); // Called when physical stop button pressed on hardware
|
||||
|
||||
#if defined(Q_OS_WIN) || (defined(Q_OS_MAC) && !defined(Q_OS_IOS)) || (defined(Q_OS_ANDROID) && defined(LICENSE))
|
||||
void licenseReply(QNetworkReply *reply);
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.treadmill-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -73,20 +77,24 @@
|
||||
<tr class="speed" sort-order="0">
|
||||
<td class="icon">🏃</td>
|
||||
<td style="text-align: left">SPEED</td>
|
||||
<td class="speed-avg-title"><small>AVG</small></td>
|
||||
<td class="speed-avg">0.0</td>
|
||||
<td class="speed-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedMinus()">-</button></td>
|
||||
<td class="speed-avg"><span class="speed-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="speed-value values"><b>0.0</b></td>
|
||||
<td class="speed-max-title"><small>MAX</small></td>
|
||||
<td class="speed-max">0.0</td>
|
||||
<td class="speed-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedPlus()">+</button></td>
|
||||
<td class="speed-max"><span class="speed-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="inclination" sort-order="1">
|
||||
<td class="icon">📐</td>
|
||||
<td style="text-align: left">INCLINE</td>
|
||||
<td><small>AVG</small></td>
|
||||
<td class="inclination-avg">0.0</td>
|
||||
<td class="inclination-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationMinus()">-</button></td>
|
||||
<td class="inclination-avg"><span class="inclination-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="inclination-value values"><b>0.0</b></td>
|
||||
<td><small>MAX</small></td>
|
||||
<td class="inclination-max">0.0</td>
|
||||
<td class="inclination-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationPlus()">+</button></td>
|
||||
<td class="inclination-max"><span class="inclination-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="pace" sort-order="2">
|
||||
<td class="icon">🏃</td>
|
||||
@@ -306,6 +314,62 @@
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function Lap() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'lap',
|
||||
@@ -611,6 +675,7 @@
|
||||
'speed_color', 'pace_color', 'power_zone_color', 'target_power_zone_color', 'cadence_color', 'heart_color', 'watts_color',
|
||||
'peloton_resistance_color', 'target_resistance', 'target_peloton_resistance',
|
||||
'target_cadence', 'target_power', 'peloton_offset', 'peloton_ask_start', 'target_speed', 'target_pace_h', 'target_pace_m', 'target_pace_s',
|
||||
'deviceType', 'TREADMILL_TYPE',
|
||||
'inclination', 'inclination_lapavg',
|
||||
'inclination_lapmax', 'target_inclination', 'power_zone', 'power_zone_lapavg', 'power_zone_lapmax', 'target_power_zone', 'jouls',
|
||||
'row_remaining_time_s', 'row_remaining_time_m', 'row_remaining_time_h' , 'autoresistance', 'gears', 'elevation', 'pace_s' , 'pace_m',
|
||||
@@ -673,6 +738,8 @@
|
||||
var peloton_offset = 0;
|
||||
var gears = 0;
|
||||
var nextrow = "";
|
||||
var deviceType = -1;
|
||||
var TREADMILL_TYPE = -1;
|
||||
|
||||
for (let key of keys_arr) {
|
||||
if (msg.content[key] === undefined || msg.content[key] === null)
|
||||
@@ -789,6 +856,10 @@
|
||||
peloton_offset = msg.content[key];
|
||||
} else if (key === 'gears') {
|
||||
gears = msg.content[key];
|
||||
} else if (key === 'deviceType') {
|
||||
deviceType = msg.content[key];
|
||||
} else if (key === 'TREADMILL_TYPE') {
|
||||
TREADMILL_TYPE = msg.content[key];
|
||||
} else if (key === 'peloton_resistance_color') {
|
||||
$('.pelotonresistance-value').css('color', msg.content[key]);
|
||||
} else if (key === 'heart_color') {
|
||||
@@ -837,14 +908,24 @@
|
||||
|
||||
$('.speed-value').html("<b>" + speed.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.speed-avg').html(speed_lapavg.toFixed(1));
|
||||
$('.speed-max').html(speed_lapmax.toFixed(1));
|
||||
$('.speed-avg-value').html(speed_lapavg.toFixed(1));
|
||||
$('.speed-max-value').html(speed_lapmax.toFixed(1));
|
||||
if (tile_target_inclination_enabled && target_inclination > 0)
|
||||
$('.inclination-value').html("<b>" + inclination.toFixed(1) + "/" + target_inclination.toFixed(1) + "</b>");
|
||||
else
|
||||
$('.inclination-value').html("<b>" + inclination.toFixed(1) + "</b>");
|
||||
$('.inclination-avg').html(inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max').html(inclination_lapmax.toFixed(1));
|
||||
$('.inclination-avg-value').html(inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max-value').html(inclination_lapmax.toFixed(1));
|
||||
|
||||
// Show/hide treadmill-only controls based on device type
|
||||
if (deviceType === TREADMILL_TYPE && TREADMILL_TYPE !== -1) {
|
||||
$('.treadmill-only').show();
|
||||
$('.non-treadmill').hide();
|
||||
} else {
|
||||
$('.treadmill-only').hide();
|
||||
$('.non-treadmill').show();
|
||||
}
|
||||
|
||||
$('.elevation-value').html("<b>" + elevation.toFixed(1) + "</b>");
|
||||
if (tile_target_cadence_enabled && target_cadence > 0)
|
||||
$('.cadence-value').html("<b>" + cadence.toFixed(0) + "/" + target_cadence.toFixed(0) + "</b>");
|
||||
|
||||
@@ -197,6 +197,10 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.treadmill-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Metric selector panel */
|
||||
.metric-selector-panel {
|
||||
display: none;
|
||||
@@ -247,20 +251,24 @@
|
||||
<tr class="speed" sort-order="0">
|
||||
<td class="icon">🏃</td>
|
||||
<td style="text-align: left">SPEED</td>
|
||||
<td class="speed-avg-title"><small>AVG</small></td>
|
||||
<td class="speed-avg">0.0</td>
|
||||
<td class="speed-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedMinus()">-</button></td>
|
||||
<td class="speed-avg"><span class="speed-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="speed-value values"><b>0.0</b></td>
|
||||
<td class="speed-max-title"><small>MAX</small></td>
|
||||
<td class="speed-max">0.0</td>
|
||||
<td class="speed-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="SpeedPlus()">+</button></td>
|
||||
<td class="speed-max"><span class="speed-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="inclination" sort-order="1">
|
||||
<td class="icon">📐</td>
|
||||
<td style="text-align: left">INCLINE</td>
|
||||
<td><small>AVG</small></td>
|
||||
<td class="inclination-avg">0.0</td>
|
||||
<td class="inclination-avg-title"><small class="non-treadmill">AVG</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationMinus()">-</button></td>
|
||||
<td class="inclination-avg"><span class="inclination-avg-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">AVG</small></span></td>
|
||||
<td class="inclination-value values"><b>0.0</b></td>
|
||||
<td><small>MAX</small></td>
|
||||
<td class="inclination-max">0.0</td>
|
||||
<td class="inclination-max-title"><small class="non-treadmill">MAX</small><button class="treadmill-only" style="width: 30px; font-size: 18px; color: white; background-color:#4C70BF; border: none; cursor: pointer;"
|
||||
onclick="InclinationPlus()">+</button></td>
|
||||
<td class="inclination-max"><span class="inclination-max-value">0.0</span><span class="treadmill-only"><br><small style="font-size: 10px; color: #888;">MAX</small></span></td>
|
||||
</tr>
|
||||
<tr class="pace" sort-order="2">
|
||||
<td class="icon">🏃</td>
|
||||
@@ -826,6 +834,62 @@
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function SpeedMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'speed_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_speed_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationPlus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_plus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_plus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function InclinationMinus() {
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'inclination_minus',
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_inclination_minus') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to clear/lap
|
||||
function Lap() {
|
||||
let el = new MainWSQueueElement({
|
||||
@@ -1072,6 +1136,7 @@
|
||||
'speed_color', 'pace_color', 'power_zone_color', 'target_power_zone_color', 'cadence_color', 'heart_color', 'watts_color',
|
||||
'peloton_resistance_color', 'target_resistance', 'target_peloton_resistance',
|
||||
'target_cadence', 'target_power', 'peloton_offset', 'peloton_ask_start', 'target_speed', 'target_pace_h', 'target_pace_m', 'target_pace_s',
|
||||
'deviceType', 'TREADMILL_TYPE',
|
||||
'inclination', 'inclination_lapavg',
|
||||
'inclination_lapmax', 'target_inclination', 'power_zone', 'power_zone_lapavg', 'power_zone_lapmax', 'target_power_zone', 'jouls',
|
||||
'row_remaining_time_s', 'row_remaining_time_m', 'row_remaining_time_h' , 'autoresistance', 'gears', 'elevation', 'pace_s' , 'pace_m',
|
||||
@@ -1137,6 +1202,8 @@
|
||||
var peloton_offset = 0;
|
||||
var gears = 0;
|
||||
var nextrow = "";
|
||||
var deviceType = -1;
|
||||
var TREADMILL_TYPE = -1;
|
||||
|
||||
// Get values from message
|
||||
for (let key of keys_arr) {
|
||||
@@ -1255,6 +1322,10 @@
|
||||
peloton_offset = msg.content[key];
|
||||
} else if (key === 'gears') {
|
||||
gears = msg.content[key];
|
||||
} else if (key === 'deviceType') {
|
||||
deviceType = msg.content[key];
|
||||
} else if (key === 'TREADMILL_TYPE') {
|
||||
TREADMILL_TYPE = msg.content[key];
|
||||
} else if (key === 'peloton_resistance_color') {
|
||||
$('.pelotonresistance-value').css('color', msg.content[key]);
|
||||
} else if (key === 'heart_color') {
|
||||
@@ -1336,7 +1407,9 @@
|
||||
target_power: target_power,
|
||||
peloton_offset: peloton_offset,
|
||||
gears: gears,
|
||||
nextrow: nextrow
|
||||
nextrow: nextrow,
|
||||
deviceType: deviceType,
|
||||
TREADMILL_TYPE: TREADMILL_TYPE
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@@ -1356,8 +1429,8 @@
|
||||
} else {
|
||||
$('.speed-value').html("<b>" + data.speed.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.speed-avg').html(data.speed_lapavg.toFixed(1));
|
||||
$('.speed-max').html(data.speed_lapmax.toFixed(1));
|
||||
$('.speed-avg-value').html(data.speed_lapavg.toFixed(1));
|
||||
$('.speed-max-value').html(data.speed_lapmax.toFixed(1));
|
||||
|
||||
// Inclination
|
||||
if (tile_target_inclination_enabled && data.target_inclination > 0) {
|
||||
@@ -1365,8 +1438,17 @@
|
||||
} else {
|
||||
$('.inclination-value').html("<b>" + data.inclination.toFixed(1) + "</b>");
|
||||
}
|
||||
$('.inclination-avg').html(data.inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max').html(data.inclination_lapmax.toFixed(1));
|
||||
$('.inclination-avg-value').html(data.inclination_lapavg.toFixed(1));
|
||||
$('.inclination-max-value').html(data.inclination_lapmax.toFixed(1));
|
||||
|
||||
// Show/hide treadmill-only controls based on device type
|
||||
if (data.deviceType === data.TREADMILL_TYPE && data.TREADMILL_TYPE !== undefined) {
|
||||
$('.treadmill-only').show();
|
||||
$('.non-treadmill').hide();
|
||||
} else {
|
||||
$('.treadmill-only').hide();
|
||||
$('.non-treadmill').show();
|
||||
}
|
||||
|
||||
// Elevation
|
||||
$('.elevation-value').html("<b>" + data.elevation.toFixed(1) + "</b>");
|
||||
|
||||
@@ -71,8 +71,13 @@ viewer.trackedEntity = bike;
|
||||
</body>
|
||||
<body>
|
||||
<div id="cesiumContainer" class="cesiumContainer"></div>
|
||||
<div><p class="metrics" style="color: #FFFFFF; position: absolute; bottom: 0px; right: 0px; margin-bottom: 0px; border-radius: 25px; border: 2px solid #73AD21; background: #73AD21; padding: 2px;">🏃Speed: 0.00<br>🚴Cadence:0<br>💓Heart:0<br>🔥Calories:0.0<br>📏Odometer:0.00<br>⚡Watt:0<br>⏲️Elapsed:0:00:00<br>📐Inclination:0.0<br>🧲Resistance:0<br>✈️Altitude:0.0<br>⛰️Elevation:0.0</p></div>
|
||||
<div style="border: 0px solid #aaa; border-radius: 10px; overflow: hidden; position:absolute; bottom: 0px; right: 150px; width=150px; height=75px"><canvas id="canvasChart" style="width=150px; height=75px; border-right: 0pt solid #ffff00;"></canvas></div>
|
||||
<div id="metricsContainer" style="position: absolute; bottom: 0px; right: 0px; width: 200px; height: 250px; touch-action: none; user-select: none; z-index: 1000;">
|
||||
<div class="metrics" style="color: #FFFFFF; width: 100%; height: 100%; margin: 0; border-radius: 25px; border: 2px solid #73AD21; background: #73AD21; padding: 8px; box-sizing: border-box; overflow: hidden; position: relative; touch-action: none;">
|
||||
<div id="metricsText" style="font-size: 12px; line-height: 1.4;">🏃Speed: 0.00<br>🚴Cadence:0<br>💓Heart:0<br>🔥Calories:0.0<br>📏Odometer:0.00<br>⚡Watt:0<br>⏲️Elapsed:0:00:00<br>📐Inclination:0.0<br>🧲Resistance:0<br>✈️Altitude:0.0<br>⛰️Elevation:0.0</div>
|
||||
<div id="resizeHandle" style="position: absolute; bottom: 0; right: 0; width: 50px; height: 50px; background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.8) 50%); cursor: nwse-resize; border-bottom-right-radius: 23px; display: flex; align-items: flex-end; justify-content: flex-end; font-size: 24px; color: rgba(0,0,0,0.6); padding-bottom: 2px; padding-right: 4px;">⤡</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chartContainer" style="border: 0px solid #aaa; border-radius: 10px; overflow: hidden; position:absolute; bottom: 0px; right: 150px; width=150px; height=75px; z-index: 999; touch-action: none;"><canvas id="canvasChart" style="width=150px; height=75px; border-right: 0pt solid #ffff00;"></canvas></div>
|
||||
<script type="text/javascript">
|
||||
let cameraComplete = true
|
||||
let lastAzimuth = 0
|
||||
@@ -147,7 +152,7 @@ viewer.trackedEntity = bike;
|
||||
}
|
||||
}
|
||||
|
||||
$('.metrics').html("🏃Speed: " + speed.toFixed(2) + "<br>🚴Cadence:" + cadence.toFixed(0) + "<br>💓Heart:"+ hr.toFixed(0) + "<br>🔥Calories:"+ calories.toFixed(1) + "<br>📏Odometer:"+ odometer.toFixed(2) + "<br>⚡Watt:"+ watt.toFixed(0) + "<br>⏲️Elapsed:"+ elapsed_h.toString().padStart(2, "0") + ":" + elapsed_m.toString().padStart(2, "0") + ":"+ elapsed_s.toString().padStart(2, "0") + "<br>📐Inclination:"+ inclination.toFixed(1) + "<br>🧲Resistance:"+ resistance.toFixed(0) + "<br>✈️Altitude:"+ altitude.toFixed(1) + "<br>⛰️Elevation:"+ elevation.toFixed(2));
|
||||
$('#metricsText').html("🏃Speed: " + speed.toFixed(2) + "<br>🚴Cadence:" + cadence.toFixed(0) + "<br>💓Heart:"+ hr.toFixed(0) + "<br>🔥Calories:"+ calories.toFixed(1) + "<br>📏Odometer:"+ odometer.toFixed(2) + "<br>⚡Watt:"+ watt.toFixed(0) + "<br>⏲️Elapsed:"+ elapsed_h.toString().padStart(2, "0") + ":" + elapsed_m.toString().padStart(2, "0") + ":"+ elapsed_s.toString().padStart(2, "0") + "<br>📐Inclination:"+ inclination.toFixed(1) + "<br>🧲Resistance:"+ resistance.toFixed(0) + "<br>✈️Altitude:"+ altitude.toFixed(1) + "<br>⛰️Elevation:"+ elevation.toFixed(2));
|
||||
}
|
||||
return null;
|
||||
}, 15000, 3);
|
||||
@@ -314,6 +319,301 @@ el.enqueue().then(process_gpxbase64).catch(function(err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
setTimeout(a,0);
|
||||
|
||||
// Metrics container drag and resize functionality
|
||||
(function() {
|
||||
const container = document.getElementById('metricsContainer');
|
||||
const resizeHandle = document.getElementById('resizeHandle');
|
||||
const metricsText = document.getElementById('metricsText');
|
||||
const chartContainer = document.getElementById('chartContainer');
|
||||
|
||||
let isDragging = false;
|
||||
let isResizing = false;
|
||||
let startX, startY, startLeft, startTop, startWidth, startHeight;
|
||||
let resizeTimeout = null;
|
||||
|
||||
// Update chart position to follow metrics container
|
||||
function updateChartPosition() {
|
||||
// Position chart to the left of metrics container, aligned at bottom
|
||||
const metricsLeft = container.offsetLeft;
|
||||
const metricsTop = container.offsetTop;
|
||||
const metricsHeight = container.offsetHeight;
|
||||
const chartWidth = chartContainer.offsetWidth;
|
||||
const chartHeight = chartContainer.offsetHeight;
|
||||
|
||||
// Position chart: 10px to the left of metrics, aligned at bottom
|
||||
chartContainer.style.left = (metricsLeft - chartWidth - 10) + 'px';
|
||||
chartContainer.style.top = (metricsTop + metricsHeight - chartHeight) + 'px';
|
||||
chartContainer.style.right = 'auto';
|
||||
chartContainer.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
// Load saved position and size
|
||||
function loadState() {
|
||||
const saved = localStorage.getItem('metricsContainerState');
|
||||
if (saved) {
|
||||
try {
|
||||
const state = JSON.parse(saved);
|
||||
if (state.left !== undefined) container.style.left = state.left + 'px';
|
||||
if (state.top !== undefined) container.style.top = state.top + 'px';
|
||||
if (state.right !== undefined) container.style.right = state.right + 'px';
|
||||
if (state.bottom !== undefined) container.style.bottom = state.bottom + 'px';
|
||||
if (state.width) container.style.width = state.width + 'px';
|
||||
if (state.height) container.style.height = state.height + 'px';
|
||||
updateChartPosition();
|
||||
updateFontSize();
|
||||
} catch (e) {
|
||||
console.error('Error loading metrics state:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save position and size
|
||||
function saveState() {
|
||||
const state = {
|
||||
left: container.offsetLeft,
|
||||
top: container.offsetTop,
|
||||
width: container.offsetWidth,
|
||||
height: container.offsetHeight
|
||||
};
|
||||
localStorage.setItem('metricsContainerState', JSON.stringify(state));
|
||||
}
|
||||
|
||||
// Update font size using binary search to find maximum size that fits
|
||||
function updateFontSize() {
|
||||
const minFontSize = 6;
|
||||
const maxFontSize = 48;
|
||||
let low = minFontSize;
|
||||
let high = maxFontSize;
|
||||
let bestFit = minFontSize;
|
||||
|
||||
// Count number of lines in the content
|
||||
function countLines() {
|
||||
const html = metricsText.innerHTML;
|
||||
const brCount = (html.match(/<br>/gi) || []).length;
|
||||
return brCount + 1; // +1 for the first line
|
||||
}
|
||||
|
||||
// Check if content fits with current font size
|
||||
function contentFits() {
|
||||
// Force reflow
|
||||
void metricsText.offsetHeight;
|
||||
|
||||
// Get computed line height
|
||||
const computedStyle = window.getComputedStyle(metricsText);
|
||||
const lineHeight = parseFloat(computedStyle.lineHeight);
|
||||
|
||||
// Calculate total height needed
|
||||
const numLines = countLines();
|
||||
const totalHeightNeeded = numLines * lineHeight;
|
||||
|
||||
// Get available height (parent's clientHeight minus padding)
|
||||
const metricsDiv = metricsText.parentElement;
|
||||
const parentStyle = window.getComputedStyle(metricsDiv);
|
||||
const paddingTop = parseFloat(parentStyle.paddingTop);
|
||||
const paddingBottom = parseFloat(parentStyle.paddingBottom);
|
||||
const availableHeight = metricsDiv.clientHeight - paddingTop - paddingBottom;
|
||||
|
||||
// Check if it fits
|
||||
const fitsVertically = totalHeightNeeded <= availableHeight;
|
||||
const fitsHorizontally = metricsText.scrollWidth <= metricsText.clientWidth;
|
||||
|
||||
return fitsVertically && fitsHorizontally;
|
||||
}
|
||||
|
||||
// Binary search for the maximum font size that fits
|
||||
while (low <= high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
metricsText.style.fontSize = mid + 'px';
|
||||
|
||||
if (contentFits()) {
|
||||
bestFit = mid;
|
||||
low = mid + 1; // Try larger
|
||||
} else {
|
||||
high = mid - 1; // Try smaller
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the best fit size
|
||||
metricsText.style.fontSize = bestFit + 'px';
|
||||
}
|
||||
|
||||
// Debounced version of updateFontSize (only during resize)
|
||||
function debouncedUpdateFontSize() {
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout);
|
||||
}
|
||||
resizeTimeout = setTimeout(function() {
|
||||
updateFontSize();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Get touch or mouse coordinates
|
||||
function getCoordinates(e) {
|
||||
if (e.touches && e.touches.length > 0) {
|
||||
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
}
|
||||
return { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
|
||||
// Check if touch/click is in resize handle area (bottom-right 50x50px)
|
||||
function isInResizeHandle(x, y) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const handleSize = 50;
|
||||
return (
|
||||
x >= rect.right - handleSize &&
|
||||
x <= rect.right &&
|
||||
y >= rect.bottom - handleSize &&
|
||||
y <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Start dragging
|
||||
function startDrag(e) {
|
||||
if (isResizing) return;
|
||||
isDragging = true;
|
||||
const coords = getCoordinates(e);
|
||||
startX = coords.x;
|
||||
startY = coords.y;
|
||||
startLeft = container.offsetLeft;
|
||||
startTop = container.offsetTop;
|
||||
container.style.right = 'auto';
|
||||
container.style.bottom = 'auto';
|
||||
|
||||
// Add global listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
document.addEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Start resizing
|
||||
function startResize(e) {
|
||||
isResizing = true;
|
||||
const coords = getCoordinates(e);
|
||||
startX = coords.x;
|
||||
startY = coords.y;
|
||||
startWidth = container.offsetWidth;
|
||||
startHeight = container.offsetHeight;
|
||||
|
||||
// Add global listeners
|
||||
document.addEventListener('mousemove', handleMove);
|
||||
document.addEventListener('touchmove', handleMove, { passive: false });
|
||||
document.addEventListener('mouseup', endDragOrResize);
|
||||
document.addEventListener('touchend', endDragOrResize);
|
||||
document.addEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Handle move
|
||||
function handleMove(e) {
|
||||
// Only handle if we're actually dragging or resizing
|
||||
if (!isDragging && !isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = getCoordinates(e);
|
||||
|
||||
if (isDragging) {
|
||||
const deltaX = coords.x - startX;
|
||||
const deltaY = coords.y - startY;
|
||||
container.style.left = (startLeft + deltaX) + 'px';
|
||||
container.style.top = (startTop + deltaY) + 'px';
|
||||
updateChartPosition();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else if (isResizing) {
|
||||
const deltaX = coords.x - startX;
|
||||
const deltaY = coords.y - startY;
|
||||
const newWidth = Math.max(100, startWidth + deltaX);
|
||||
const newHeight = Math.max(100, startHeight + deltaY);
|
||||
container.style.width = newWidth + 'px';
|
||||
container.style.height = newHeight + 'px';
|
||||
updateChartPosition();
|
||||
debouncedUpdateFontSize();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// End drag or resize
|
||||
function endDragOrResize(e) {
|
||||
const wasDragging = isDragging;
|
||||
const wasResizing = isResizing;
|
||||
|
||||
// Always reset state first
|
||||
isDragging = false;
|
||||
isResizing = false;
|
||||
|
||||
// Always remove listeners to prevent leaks
|
||||
document.removeEventListener('mousemove', handleMove);
|
||||
document.removeEventListener('touchmove', handleMove);
|
||||
document.removeEventListener('mouseup', endDragOrResize);
|
||||
document.removeEventListener('touchend', endDragOrResize);
|
||||
document.removeEventListener('touchcancel', endDragOrResize);
|
||||
|
||||
// Only save state if we were actually dragging/resizing
|
||||
if (wasDragging || wasResizing) {
|
||||
saveState();
|
||||
// If we were resizing, clear pending timeout and update immediately
|
||||
if (wasResizing) {
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = null;
|
||||
}
|
||||
updateFontSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if touch/click is inside container
|
||||
function isInsideContainer(x, y) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
return (
|
||||
x >= rect.left &&
|
||||
x <= rect.right &&
|
||||
y >= rect.top &&
|
||||
y <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Add event listeners for dragging/resizing on container
|
||||
container.addEventListener('mousedown', function(e) {
|
||||
const coords = getCoordinates(e);
|
||||
// Double check we're actually inside the container
|
||||
if (!isInsideContainer(coords.x, coords.y)) {
|
||||
return;
|
||||
}
|
||||
if (isInResizeHandle(coords.x, coords.y)) {
|
||||
startResize(e);
|
||||
} else {
|
||||
startDrag(e);
|
||||
}
|
||||
});
|
||||
container.addEventListener('touchstart', function(e) {
|
||||
const coords = getCoordinates(e);
|
||||
// Double check we're actually inside the container
|
||||
if (!isInsideContainer(coords.x, coords.y)) {
|
||||
return;
|
||||
}
|
||||
if (isInResizeHandle(coords.x, coords.y)) {
|
||||
startResize(e);
|
||||
} else {
|
||||
startDrag(e);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Load saved state and set initial font size
|
||||
loadState();
|
||||
updateFontSize();
|
||||
updateChartPosition();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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,
|
||||
@@ -111,6 +113,8 @@ class lockscreen {
|
||||
static void set_action_profile(const char* profile);
|
||||
static const char* get_action_profile();
|
||||
|
||||
// multi-window detection for iPadOS
|
||||
static bool isInMultiWindowMode();
|
||||
};
|
||||
|
||||
#endif // LOCKSCREEN_H
|
||||
|
||||
@@ -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)
|
||||
@@ -616,13 +627,43 @@ void lockscreen::zwiftClickRemote(const char* Name, const char* UUID, void* devi
|
||||
|
||||
void lockscreen::zwiftClickRemote_WriteCharacteristic(unsigned char* qdata, unsigned char length, void* deviceClass) {
|
||||
if (ios_zwiftClickRemotes == nil) return;
|
||||
|
||||
|
||||
// Get the specific remote for this device
|
||||
NSValue *key = [NSValue valueWithPointer:deviceClass];
|
||||
ios_zwiftclickremote *remote = [ios_zwiftClickRemotes objectForKey:key];
|
||||
|
||||
|
||||
if(remote) {
|
||||
[remote writeCharacteristic:qdata length:length];
|
||||
}
|
||||
}
|
||||
|
||||
bool lockscreen::isInMultiWindowMode() {
|
||||
// Check if we're on iPad and in multi-window mode (Stage Manager, Split View, Slide Over)
|
||||
if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
// Get the foreground active scene
|
||||
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive &&
|
||||
[scene isKindOfClass:[UIWindowScene class]]) {
|
||||
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
||||
|
||||
// Get the window bounds and screen bounds
|
||||
CGRect windowBounds = windowScene.coordinateSpace.bounds;
|
||||
CGRect screenBounds = windowScene.screen.bounds;
|
||||
|
||||
// If window is smaller than screen in either dimension, we're in multi-window mode
|
||||
// Add a small tolerance for floating point comparison
|
||||
if (windowBounds.size.width < screenBounds.size.width - 1 ||
|
||||
windowBounds.size.height < screenBounds.size.height - 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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,
|
||||
|
||||
22
src/main.qml
22
src/main.qml
@@ -31,20 +31,30 @@ ApplicationWindow {
|
||||
|
||||
// Helper functions for cleaner padding calculations
|
||||
function getTopPadding() {
|
||||
// Add padding for iPadOS multi-window mode (Stage Manager, Split View, Slide Over)
|
||||
// to avoid overlap with window control buttons (red/yellow/green)
|
||||
// Check both the native detection and window size comparison for reactivity
|
||||
if (Qt.platform.os === "ios") {
|
||||
var isMultiWindow = (typeof rootItem !== "undefined" && rootItem && rootItem.iPadMultiWindowMode) ||
|
||||
(window.width < Screen.width - 10); // Window smaller than screen = multi-window
|
||||
if (isMultiWindow) {
|
||||
return 15; // Space for window control buttons
|
||||
}
|
||||
}
|
||||
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.height : AndroidStatusBar.leftInset;
|
||||
}
|
||||
|
||||
|
||||
function getBottomPadding() {
|
||||
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.navigationBarHeight : AndroidStatusBar.rightInset;
|
||||
}
|
||||
|
||||
|
||||
function getLeftPadding() {
|
||||
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
|
||||
return (Screen.orientation === Qt.LandscapeOrientation || Screen.orientation === Qt.InvertedLandscapeOrientation) ?
|
||||
return (Screen.orientation === Qt.LandscapeOrientation || Screen.orientation === Qt.InvertedLandscapeOrientation) ?
|
||||
AndroidStatusBar.leftInset : 0;
|
||||
}
|
||||
|
||||
@@ -925,7 +935,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
text: "version 2.20.21"
|
||||
text: "version 2.20.26"
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
|
||||
@@ -186,8 +186,9 @@ void MainWindow::update() {
|
||||
verticalOscillation, stepCount,
|
||||
target_cadence, target_watt, target_resistance, target_inclination, target_speed,
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(),
|
||||
bluetoothManager->device()->HeatStrainIndex.value() // TODO add lap
|
||||
);
|
||||
bluetoothManager->device()->HeatStrainIndex.value(),
|
||||
bluetoothManager->device()->currentHRV().value(),
|
||||
bluetoothManager->device()->getRRIntervalsAndClear());
|
||||
|
||||
Session.append(s);
|
||||
|
||||
|
||||
@@ -101,16 +101,20 @@ SOURCES += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.cpp \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.cpp \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.cpp \
|
||||
$$PWD/devices/sportstechrower/sportstechrower.cpp \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.cpp \
|
||||
$$PWD/devices/thinkridercontroller/thinkridercontroller.cpp \
|
||||
$$PWD/devices/stairclimber.cpp \
|
||||
$$PWD/devices/echelonstairclimber/echelonstairclimber.cpp \
|
||||
$$PWD/devices/sunnyfitstepper/sunnyfitstepper.cpp \
|
||||
$$PWD/devices/technogymbike/technogymbike.cpp \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.cpp \
|
||||
$$PWD/fitdatabaseprocessor.cpp \
|
||||
$$PWD/devices/trxappgateusbrower/trxappgateusbrower.cpp \
|
||||
$$PWD/logwriter.cpp \
|
||||
$$PWD/fitbackupwriter.cpp \
|
||||
$$PWD/filesearcher.cpp \
|
||||
$$PWD/mqtt/qmqttauthenticationproperties.cpp \
|
||||
$$PWD/mqtt/qmqttclient.cpp \
|
||||
$$PWD/mqtt/qmqttconnection.cpp \
|
||||
@@ -366,6 +370,7 @@ HEADERS += \
|
||||
$$PWD/devices/cycleopsphantombike/cycleopsphantombike.h \
|
||||
$$PWD/devices/deeruntreadmill/deerruntreadmill.h \
|
||||
$$PWD/devices/echelonstairclimber/echelonstairclimber.h \
|
||||
$$PWD/devices/sunnyfitstepper/sunnyfitstepper.h \
|
||||
$$PWD/devices/elitesquarecontroller/elitesquarecontroller.h \
|
||||
$$PWD/devices/focustreadmill/focustreadmill.h \
|
||||
$$PWD/devices/jumprope.h \
|
||||
@@ -378,8 +383,10 @@ HEADERS += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.h \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.h \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.h \
|
||||
$$PWD/devices/sportstechrower/sportstechrower.h \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.h \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.h \
|
||||
$$PWD/devices/thinkridercontroller/thinkridercontroller.h \
|
||||
$$PWD/devices/stairclimber.h \
|
||||
$$PWD/devices/technogymbike/technogymbike.h \
|
||||
$$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.h \
|
||||
@@ -389,6 +396,7 @@ HEADERS += \
|
||||
$$PWD/inclinationresistancetable.h \
|
||||
$$PWD/logwriter.h \
|
||||
$$PWD/fitbackupwriter.h \
|
||||
$$PWD/filesearcher.h \
|
||||
$$PWD/osc.h \
|
||||
$$PWD/oscpp/client.hpp \
|
||||
$$PWD/oscpp/detail/endian.hpp \
|
||||
@@ -770,6 +778,7 @@ fit-sdk/fit_zones_target_mesg.hpp \
|
||||
fit-sdk/fit_zones_target_mesg_listener.hpp \
|
||||
devices/flywheelbike/flywheelbike.h \
|
||||
devices/ftmsbike/ftmsbike.h \
|
||||
devices/ftmsbike/speedracex_defaults.h \
|
||||
devices/heartratebelt/heartratebelt.h \
|
||||
homeform.h \
|
||||
garminconnect.h \
|
||||
@@ -985,6 +994,9 @@ ios {
|
||||
|
||||
TARGET = qdomyoszwift
|
||||
QMAKE_TARGET_BUNDLE_PREFIX = org.cagnulein
|
||||
|
||||
# iOS Code Signing Configuration - handled manually in Xcode project
|
||||
|
||||
DEFINES+=_Nullable_result=_Nullable NS_FORMAT_ARGUMENT\\(A\\)=
|
||||
}
|
||||
|
||||
@@ -1004,4 +1016,4 @@ INCLUDEPATH += purchasing/inapp
|
||||
|
||||
WINRT_MANIFEST = AppxManifest.xml
|
||||
|
||||
VERSION = 2.20.21
|
||||
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
|
||||
159
src/qfit.cpp
159
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"
|
||||
@@ -75,20 +76,28 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
int fit_file_garmin_device_training_effect_device = settings.value(QZSettings::fit_file_garmin_device_training_effect_device, QZSettings::default_fit_file_garmin_device_training_effect_device).toInt();
|
||||
uint32_t garmin_device_serial = settings.value(QZSettings::garmin_device_serial, QZSettings::default_garmin_device_serial).toUInt();
|
||||
bool is_zwift_device = (fit_file_garmin_device_training_effect_device == 99999);
|
||||
bool is_tacx_device = (fit_file_garmin_device_training_effect_device == 88888);
|
||||
fit::FileIdMesg fileIdMesg; // Every FIT file requires a File ID message
|
||||
fileIdMesg.SetType(FIT_FILE_ACTIVITY);
|
||||
if(bluetooth_device_name.toUpper().startsWith("DOMYOS") && !is_zwift_device && !fit_file_garmin_device_training_effect)
|
||||
if(bluetooth_device_name.toUpper().startsWith("DOMYOS") && !is_zwift_device && !is_tacx_device && !fit_file_garmin_device_training_effect)
|
||||
fileIdMesg.SetManufacturer(FIT_MANUFACTURER_DECATHLON);
|
||||
else {
|
||||
if(is_zwift_device)
|
||||
fileIdMesg.SetManufacturer(FIT_MANUFACTURER_ZWIFT);
|
||||
else if(is_tacx_device)
|
||||
fileIdMesg.SetManufacturer(FIT_MANUFACTURER_TACX);
|
||||
else if(fit_file_garmin_device_training_effect)
|
||||
fileIdMesg.SetManufacturer(FIT_MANUFACTURER_GARMIN);
|
||||
else
|
||||
fileIdMesg.SetManufacturer(FIT_MANUFACTURER_DEVELOPMENT);
|
||||
}
|
||||
if(fit_file_garmin_device_training_effect || is_zwift_device) {
|
||||
fileIdMesg.SetProduct(is_zwift_device ? 3288 : fit_file_garmin_device_training_effect_device);
|
||||
if(fit_file_garmin_device_training_effect || is_zwift_device || is_tacx_device) {
|
||||
if(is_zwift_device)
|
||||
fileIdMesg.SetProduct(3288);
|
||||
else if(is_tacx_device)
|
||||
fileIdMesg.SetProduct(20533);
|
||||
else
|
||||
fileIdMesg.SetProduct(fit_file_garmin_device_training_effect_device);
|
||||
fileIdMesg.SetSerialNumber(garmin_device_serial);
|
||||
} else {
|
||||
fileIdMesg.SetProduct(1);
|
||||
@@ -119,6 +128,11 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
deviceInfoMesg.SetSerialNumber(garmin_device_serial);
|
||||
deviceInfoMesg.SetProduct(3288);
|
||||
deviceInfoMesg.SetSoftwareVersion(21.19);
|
||||
} else if(is_tacx_device) {
|
||||
deviceInfoMesg.SetManufacturer(FIT_MANUFACTURER_TACX);
|
||||
deviceInfoMesg.SetSerialNumber(garmin_device_serial);
|
||||
deviceInfoMesg.SetProduct(20533);
|
||||
deviceInfoMesg.SetSoftwareVersion(1.30);
|
||||
} else if(fit_file_garmin_device_training_effect) {
|
||||
deviceInfoMesg.SetManufacturer(FIT_MANUFACTURER_GARMIN);
|
||||
deviceInfoMesg.SetSerialNumber(garmin_device_serial);
|
||||
@@ -149,6 +163,11 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
double watt_sum = 0;
|
||||
int watt_count = 0;
|
||||
|
||||
// Variables for jump rope cadence
|
||||
double cadence_sum = 0;
|
||||
int cadence_count = 0;
|
||||
uint8_t max_cadence = 0;
|
||||
|
||||
for (int i = firstRealIndex; i < session.length(); i++) {
|
||||
if (session.at(i).coordinate.isValid()) {
|
||||
gps_data = true;
|
||||
@@ -191,6 +210,15 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
watt_sum += session.at(i).watt;
|
||||
watt_count++;
|
||||
}
|
||||
|
||||
// Collect cadence data for jump rope
|
||||
if (type == JUMPROPE && session.at(i).cadence > 0) {
|
||||
cadence_sum += session.at(i).cadence;
|
||||
cadence_count++;
|
||||
if (session.at(i).cadence > max_cadence) {
|
||||
max_cadence = session.at(i).cadence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (speed_count > 0) {
|
||||
@@ -214,7 +242,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
// where IF (Intensity Factor) = average_power / FTP
|
||||
double intensity_factor = avg_watt / ftp;
|
||||
tss = (duration_seconds * avg_watt * intensity_factor) / (ftp * 36.0);
|
||||
training_load = tss; // Use TSS as training load
|
||||
training_load = tss; // Use TSS as training load in the worst scenario
|
||||
has_tss = true;
|
||||
|
||||
qDebug() << "Training Load (TSS) calculated:" << tss
|
||||
@@ -226,7 +254,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
}
|
||||
|
||||
// Always calculate TRIMP if we have HR data (fallback or additional metric)
|
||||
if (hr_count > 0 && training_load == 0) {
|
||||
if (hr_count > 0) {
|
||||
double avg_hr = hr_sum / hr_count;
|
||||
uint32_t duration_minutes = duration_seconds / 60;
|
||||
|
||||
@@ -285,7 +313,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
activityTitle.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
activityTitle.SetFieldName(0, L"Activity Title");
|
||||
activityTitle.SetUnits(0, L"Title");
|
||||
activityTitle.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
activityTitle.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg targetCadenceMesg;
|
||||
targetCadenceMesg.SetDeveloperDataIndex(0);
|
||||
@@ -317,7 +345,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
ftpSessionMesg.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT64);
|
||||
ftpSessionMesg.SetFieldName(0, L"FTP");
|
||||
ftpSessionMesg.SetUnits(0, L"FTP");
|
||||
ftpSessionMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
ftpSessionMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
// Peloton and workout source fields
|
||||
fit::FieldDescriptionMesg workoutSourceMesg;
|
||||
@@ -326,7 +354,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
workoutSourceMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
workoutSourceMesg.SetFieldName(0, L"Workout Source");
|
||||
workoutSourceMesg.SetUnits(0, L"source");
|
||||
workoutSourceMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
workoutSourceMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg pelotonWorkoutIdMesg;
|
||||
pelotonWorkoutIdMesg.SetDeveloperDataIndex(0);
|
||||
@@ -334,7 +362,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
pelotonWorkoutIdMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
pelotonWorkoutIdMesg.SetFieldName(0, L"Peloton Workout ID");
|
||||
pelotonWorkoutIdMesg.SetUnits(0, L"id");
|
||||
pelotonWorkoutIdMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
pelotonWorkoutIdMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg pelotonUrlMesg;
|
||||
pelotonUrlMesg.SetDeveloperDataIndex(0);
|
||||
@@ -342,7 +370,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
pelotonUrlMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
pelotonUrlMesg.SetFieldName(0, L"Peloton URL");
|
||||
pelotonUrlMesg.SetUnits(0, L"url");
|
||||
pelotonUrlMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
pelotonUrlMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg trainingProgramFileMesg;
|
||||
trainingProgramFileMesg.SetDeveloperDataIndex(0);
|
||||
@@ -350,7 +378,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
trainingProgramFileMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
trainingProgramFileMesg.SetFieldName(0, L"Training Program File");
|
||||
trainingProgramFileMesg.SetUnits(0, L"filename");
|
||||
trainingProgramFileMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
trainingProgramFileMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::SessionMesg sessionMesg;
|
||||
sessionMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
@@ -362,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);
|
||||
@@ -372,15 +403,18 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
|
||||
// Set training load in FIT file
|
||||
// Always set training_load_peak (Garmin uses this for acute training load)
|
||||
// COMMENTED OUT: Garmin Connect doesn't properly reflect these values
|
||||
// Moving to developer data message instead
|
||||
if (training_load > 0) {
|
||||
sessionMesg.SetTrainingLoadPeak(training_load);
|
||||
qDebug() << "Setting training_load_peak in FIT file:" << training_load;
|
||||
//sessionMesg.SetTrainingLoadPeak(training_load);
|
||||
qDebug() << "Training load will be stored in developer data:" << training_load;
|
||||
}
|
||||
|
||||
// For cycling with power, also set training_stress_score (TSS)
|
||||
if (has_tss) {
|
||||
sessionMesg.SetTrainingStressScore(tss);
|
||||
qDebug() << "Setting training_stress_score (TSS) in FIT file:" << tss;
|
||||
}
|
||||
// For cycling with power, also set training_stress_score (TSS)
|
||||
// COMMENTED OUT: Moving to developer data message
|
||||
if (has_tss) {
|
||||
//sessionMesg.SetTrainingStressScore(tss);
|
||||
qDebug() << "TSS will be stored in developer data:" << tss;
|
||||
}
|
||||
|
||||
// First, set sport and subsport based on device type
|
||||
@@ -424,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) {
|
||||
|
||||
@@ -432,6 +466,15 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
sessionMesg.SetSubSport(FIT_SUB_SPORT_GENERIC);
|
||||
if (session.last().stepCount)
|
||||
sessionMesg.SetJumpCount(session.last().stepCount);
|
||||
// Total cycles
|
||||
if (session.last().stepCount)
|
||||
sessionMesg.SetTotalCycles(session.last().stepCount);
|
||||
// Avg cadence (jump rate)
|
||||
if (cadence_count > 0)
|
||||
sessionMesg.SetAvgCadence((uint8_t)(cadence_sum / cadence_count));
|
||||
// Max cadence (max jump rate)
|
||||
if (max_cadence > 0)
|
||||
sessionMesg.SetMaxCadence(max_cadence);
|
||||
} else {
|
||||
|
||||
sessionMesg.SetSport(FIT_SPORT_CYCLING);
|
||||
@@ -527,18 +570,8 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
trainingProgramFileField.SetSTRINGValue(trainingProgramFile.toStdWString());
|
||||
}
|
||||
|
||||
sessionMesg.AddDeveloperField(activityTitleField);
|
||||
sessionMesg.AddDeveloperField(ftpSessionField);
|
||||
sessionMesg.AddDeveloperField(workoutSourceField);
|
||||
if (!pelotonWorkoutId.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(pelotonWorkoutIdField);
|
||||
}
|
||||
if (!pelotonUrl.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(pelotonUrlField);
|
||||
}
|
||||
if (!trainingProgramFile.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(trainingProgramFileField);
|
||||
}
|
||||
// Developer fields are now added to custom message instead of session
|
||||
// This improves Garmin Connect compatibility
|
||||
|
||||
fit::ActivityMesg activityMesg;
|
||||
activityMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
@@ -595,6 +628,8 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
|
||||
encode.Write(timestampCorrelationMesg);
|
||||
|
||||
// Write workout message with developer metadata fields when workout name exists
|
||||
// This keeps workout-related metadata separate from session/activity for better compatibility
|
||||
if (workoutName.length() > 0) {
|
||||
fit::TrainingFileMesg trainingFile;
|
||||
trainingFile.SetTimestamp(sessionMesg.GetTimestamp());
|
||||
@@ -609,6 +644,21 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
workout.SetWktName(workoutName.toStdWString());
|
||||
#endif
|
||||
workout.SetNumValidSteps(1);
|
||||
|
||||
// Add developer fields to workout message
|
||||
workout.AddDeveloperField(activityTitleField);
|
||||
workout.AddDeveloperField(ftpSessionField);
|
||||
workout.AddDeveloperField(workoutSourceField);
|
||||
if (!pelotonWorkoutId.isEmpty()) {
|
||||
workout.AddDeveloperField(pelotonWorkoutIdField);
|
||||
}
|
||||
if (!pelotonUrl.isEmpty()) {
|
||||
workout.AddDeveloperField(pelotonUrlField);
|
||||
}
|
||||
if (!trainingProgramFile.isEmpty()) {
|
||||
workout.AddDeveloperField(trainingProgramFileField);
|
||||
}
|
||||
|
||||
encode.Write(workout);
|
||||
|
||||
fit::WorkoutStepMesg workoutStep;
|
||||
@@ -653,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 {
|
||||
|
||||
@@ -758,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
|
||||
@@ -768,7 +831,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
lapMesg.SetMessageIndex(lap_index++);
|
||||
lapMesg.SetLapTrigger(FIT_LAP_TRIGGER_DISTANCE);
|
||||
if (type == JUMPROPE)
|
||||
lapMesg.SetRepetitionNum(session.at(i - 1).inclination);
|
||||
lapMesg.SetRepetitionNum(lap_index);
|
||||
lastLapTimer = sl.elapsedTime;
|
||||
lastLapOdometer = sl.distance;
|
||||
|
||||
@@ -857,6 +920,36 @@ class Listener : public fit::FileIdMesgListener,
|
||||
// std::wcout << L" New Mesg: " << mesg.GetName().c_str() << L". It has " << mesg.GetNumFields() << L"
|
||||
// field(s) and " << mesg.GetNumDevFields() << " developer field(s).\n";
|
||||
|
||||
// Check if this is a Workout message with developer fields (new format)
|
||||
if (mesg.GetNum() == FIT_MESG_NUM_WORKOUT) {
|
||||
printf("Found Workout message with developer fields\n");
|
||||
// Read developer fields from workout message (new format)
|
||||
for (auto devField : mesg.GetDeveloperFields()) {
|
||||
std::string fieldName = devField.GetName();
|
||||
if (fieldName == "Activity Title" && workoutName != nullptr) {
|
||||
std::wstring wWorkoutName = devField.GetSTRINGValue(0);
|
||||
*workoutName = QString::fromStdWString(wWorkoutName);
|
||||
printf(" Found Activity Title in workout: %s\n", workoutName->toStdString().c_str());
|
||||
} else if (fieldName == "Workout Source" && workoutSource != nullptr) {
|
||||
std::wstring wWorkoutSource = devField.GetSTRINGValue(0);
|
||||
*workoutSource = QString::fromStdWString(wWorkoutSource);
|
||||
printf(" Found Workout Source in workout: %s\n", workoutSource->toStdString().c_str());
|
||||
} else if (fieldName == "Peloton Workout ID" && pelotonWorkoutId != nullptr) {
|
||||
std::wstring wPelotonWorkoutId = devField.GetSTRINGValue(0);
|
||||
*pelotonWorkoutId = QString::fromStdWString(wPelotonWorkoutId);
|
||||
printf(" Found Peloton Workout ID in workout: %s\n", pelotonWorkoutId->toStdString().c_str());
|
||||
} else if (fieldName == "Peloton URL" && pelotonUrl != nullptr) {
|
||||
std::wstring wPelotonUrl = devField.GetSTRINGValue(0);
|
||||
*pelotonUrl = QString::fromStdWString(wPelotonUrl);
|
||||
printf(" Found Peloton URL in workout: %s\n", pelotonUrl->toStdString().c_str());
|
||||
} else if (fieldName == "Training Program File" && trainingProgramFile != nullptr) {
|
||||
std::wstring wTrainingProgramFile = devField.GetSTRINGValue(0);
|
||||
*trainingProgramFile = QString::fromStdWString(wTrainingProgramFile);
|
||||
printf(" Found Training Program File in workout: %s\n", trainingProgramFile->toStdString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (FIT_UINT16 i = 0; i < (FIT_UINT16)mesg.GetNumFields(); i++) {
|
||||
fit::Field *field = mesg.GetFieldByIndex(i);
|
||||
// std::wcout << L" Field" << i << " (" << field->GetName().c_str() << ") has " << field->GetNumValues()
|
||||
|
||||
@@ -92,6 +92,7 @@ const QString QZSettings::default_user_email = QLatin1String("");
|
||||
const QString QZSettings::user_nickname = QStringLiteral("user_nickname");
|
||||
const QString QZSettings::default_user_nickname = QStringLiteral("");
|
||||
const QString QZSettings::miles_unit = QStringLiteral("miles_unit");
|
||||
const QString QZSettings::weight_kg_unit = QStringLiteral("weight_kg_unit");
|
||||
const QString QZSettings::pause_on_start = QStringLiteral("pause_on_start");
|
||||
const QString QZSettings::treadmill_force_speed = QStringLiteral("treadmill_force_speed");
|
||||
const QString QZSettings::pause_on_start_treadmill = QStringLiteral("pause_on_start_treadmill");
|
||||
@@ -154,6 +155,7 @@ const QString QZSettings::tile_ftp_enabled = QStringLiteral("tile_ftp_enabled");
|
||||
const QString QZSettings::tile_ftp_order = QStringLiteral("tile_ftp_order");
|
||||
const QString QZSettings::tile_heart_enabled = QStringLiteral("tile_heart_enabled");
|
||||
const QString QZSettings::tile_heart_order = QStringLiteral("tile_heart_order");
|
||||
const QString QZSettings::tile_heart_show_as_percent = QStringLiteral("tile_heart_show_as_percent");
|
||||
const QString QZSettings::tile_fan_enabled = QStringLiteral("tile_fan_enabled");
|
||||
const QString QZSettings::tile_fan_order = QStringLiteral("tile_fan_order");
|
||||
const QString QZSettings::tile_jouls_enabled = QStringLiteral("tile_jouls_enabled");
|
||||
@@ -355,6 +357,7 @@ const QString QZSettings::virtual_device_onlyheart = QStringLiteral("virtual_dev
|
||||
const QString QZSettings::virtual_device_echelon = QStringLiteral("virtual_device_echelon");
|
||||
const QString QZSettings::virtual_device_ifit = QStringLiteral("virtual_device_ifit");
|
||||
const QString QZSettings::virtual_device_rower = QStringLiteral("virtual_device_rower");
|
||||
const QString QZSettings::virtual_device_rower_pm5 = QStringLiteral("virtual_device_rower_pm5");
|
||||
const QString QZSettings::virtual_device_force_bike = QStringLiteral("virtual_device_force_bike");
|
||||
const QString QZSettings::virtual_device_force_treadmill = QStringLiteral("virtual_device_force_treadmill");
|
||||
const QString QZSettings::volume_change_gears = QStringLiteral("volume_change_gears");
|
||||
@@ -677,6 +680,7 @@ const QString QZSettings::treadmill_inclination_override_150 = QStringLiteral("t
|
||||
const QString QZSettings::sole_elliptical_e55 = QStringLiteral("sole_elliptical_e55");
|
||||
const QString QZSettings::horizon_treadmill_force_ftms = QStringLiteral("horizon_treadmill_force_ftms");
|
||||
const QString QZSettings::horizon_treadmill_7_0_at_24 = QStringLiteral("horizon_treadmill_7_0_at_24");
|
||||
const QString QZSettings::treadmill_direct_distance = QStringLiteral("treadmill_direct_distance");
|
||||
const QString QZSettings::treadmill_pid_heart_min = QStringLiteral("treadmill_pid_heart_min");
|
||||
const QString QZSettings::treadmill_pid_heart_max = QStringLiteral("treadmill_pid_heart_max");
|
||||
const QString QZSettings::nordictrack_elliptical_c7_5 = QStringLiteral("nordictrack_elliptical_c7_5");
|
||||
@@ -743,6 +747,7 @@ const QString QZSettings::autolap_distance = QStringLiteral("autolap_distance");
|
||||
const QString QZSettings::nordictrack_s20_treadmill = QStringLiteral("nordictrack_s20_treadmill");
|
||||
const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotion_coachbike_b22_7");
|
||||
const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci");
|
||||
const QString QZSettings::nordictrack_gx_4_5_pro = QStringLiteral("nordictrack_gx_4_5_pro");
|
||||
const QString QZSettings::kingsmith_encrypt_g1_walking_pad = QStringLiteral("kingsmith_encrypt_g1_walking_pad");
|
||||
const QString QZSettings::proformtdf1ip = QStringLiteral("proformtdf1ip");
|
||||
const QString QZSettings::default_proformtdf1ip = QStringLiteral("");
|
||||
@@ -768,12 +773,14 @@ const QString QZSettings::domyos_treadmill_button_16kmh = QStringLiteral("domyos
|
||||
const QString QZSettings::domyos_treadmill_button_22kmh = QStringLiteral("domyos_treadmill_button_22kmh");
|
||||
const QString QZSettings::proform_treadmill_sport_8_5 = QStringLiteral("proform_treadmill_sport_8_5");
|
||||
const QString QZSettings::domyos_treadmill_t900a = QStringLiteral("domyos_treadmill_t900a");
|
||||
const QString QZSettings::domyos_treadmill_ts100 = QStringLiteral("domyos_treadmill_ts100");
|
||||
const QString QZSettings::domyos_treadmill_sync_start = QStringLiteral("domyos_treadmill_sync_start");
|
||||
const QString QZSettings::enerfit_SPX_9500 = QStringLiteral("enerfit_SPX_9500");
|
||||
const QString QZSettings::proform_treadmill_505_cst = QStringLiteral("proform_treadmill_505_cst");
|
||||
const QString QZSettings::nordictrack_treadmill_t8_5s = QStringLiteral("nordictrack_treadmill_t8_5s");
|
||||
const QString QZSettings::proform_treadmill_705_cst = QStringLiteral("proform_treadmill_705_cst");
|
||||
const QString QZSettings::zwift_click = QStringLiteral("zwift_click");
|
||||
const QString QZSettings::thinkrider_controller = QStringLiteral("thinkrider_controller");
|
||||
const QString QZSettings::hop_sport_hs_090h_bike = QStringLiteral("hop_sport_hs_090h_bike");
|
||||
const QString QZSettings::zwift_play = QStringLiteral("zwift_play");
|
||||
const QString QZSettings::zwift_play_vibration = QStringLiteral("zwift_play_vibration");
|
||||
@@ -1036,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");
|
||||
@@ -1045,9 +1054,10 @@ const QString QZSettings::taurua_ic90 = QStringLiteral("taurua_ic90");
|
||||
const QString QZSettings::proform_csx210 = QStringLiteral("proform_csx210");
|
||||
const QString QZSettings::skandika_wiri_x2000_protocol = QStringLiteral("skandika_wiri_x2000_protocol");
|
||||
const QString QZSettings::trainprogram_auto_lap_on_segment = QStringLiteral("trainprogram_auto_lap_on_segment");
|
||||
const QString QZSettings::kingsmith_r2_enable_hw_buttons = QStringLiteral("kingsmith_r2_enable_hw_buttons");
|
||||
|
||||
|
||||
const uint32_t allSettingsCount = 853;
|
||||
const uint32_t allSettingsCount = 861;
|
||||
|
||||
QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
|
||||
@@ -1107,6 +1117,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::user_email, QZSettings::default_user_email},
|
||||
{QZSettings::user_nickname, QZSettings::default_user_nickname},
|
||||
{QZSettings::miles_unit, QZSettings::default_miles_unit},
|
||||
{QZSettings::weight_kg_unit, QZSettings::default_weight_kg_unit},
|
||||
{QZSettings::pause_on_start, QZSettings::default_pause_on_start},
|
||||
{QZSettings::treadmill_force_speed, QZSettings::default_treadmill_force_speed},
|
||||
{QZSettings::pause_on_start_treadmill, QZSettings::default_pause_on_start_treadmill},
|
||||
@@ -1157,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},
|
||||
@@ -1336,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},
|
||||
@@ -1603,6 +1616,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::sole_elliptical_e55, QZSettings::default_sole_elliptical_e55},
|
||||
{QZSettings::horizon_treadmill_force_ftms, QZSettings::default_horizon_treadmill_force_ftms},
|
||||
{QZSettings::horizon_treadmill_7_0_at_24, QZSettings::default_horizon_treadmill_7_0_at_24},
|
||||
{QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance},
|
||||
{QZSettings::treadmill_pid_heart_min, QZSettings::default_treadmill_pid_heart_min},
|
||||
{QZSettings::treadmill_pid_heart_max, QZSettings::default_treadmill_pid_heart_max},
|
||||
{QZSettings::nordictrack_elliptical_c7_5, QZSettings::default_nordictrack_elliptical_c7_5},
|
||||
@@ -1663,6 +1677,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill},
|
||||
{QZSettings::freemotion_coachbike_b22_7, QZSettings::default_freemotion_coachbike_b22_7},
|
||||
{QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci},
|
||||
{QZSettings::nordictrack_gx_4_5_pro, QZSettings::default_nordictrack_gx_4_5_pro},
|
||||
{QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad},
|
||||
{QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx},
|
||||
{QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s},
|
||||
@@ -1685,12 +1700,14 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::domyos_treadmill_button_22kmh, QZSettings::default_domyos_treadmill_button_22kmh},
|
||||
{QZSettings::proform_treadmill_sport_8_5, QZSettings::default_proform_treadmill_sport_8_5},
|
||||
{QZSettings::domyos_treadmill_t900a, QZSettings::default_domyos_treadmill_t900a},
|
||||
{QZSettings::domyos_treadmill_ts100, QZSettings::default_domyos_treadmill_ts100},
|
||||
{QZSettings::domyos_treadmill_sync_start, QZSettings::default_domyos_treadmill_sync_start},
|
||||
{QZSettings::enerfit_SPX_9500, QZSettings::default_enerfit_SPX_9500},
|
||||
{QZSettings::proform_treadmill_505_cst, QZSettings::default_proform_treadmill_505_cst},
|
||||
{QZSettings::nordictrack_treadmill_t8_5s, QZSettings::default_nordictrack_treadmill_t8_5s},
|
||||
{QZSettings::proform_treadmill_705_cst, QZSettings::default_proform_treadmill_705_cst},
|
||||
{QZSettings::zwift_click, QZSettings::default_zwift_click},
|
||||
{QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller},
|
||||
{QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike},
|
||||
{QZSettings::zwift_play, QZSettings::default_zwift_play},
|
||||
{QZSettings::zwift_play_vibration, QZSettings::default_zwift_play_vibration},
|
||||
@@ -1906,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},
|
||||
@@ -1916,6 +1935,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::proform_csx210, QZSettings::default_proform_csx210},
|
||||
{QZSettings::skandika_wiri_x2000_protocol, QZSettings::default_skandika_wiri_x2000_protocol},
|
||||
{QZSettings::trainprogram_auto_lap_on_segment, QZSettings::default_trainprogram_auto_lap_on_segment},
|
||||
{QZSettings::kingsmith_r2_enable_hw_buttons, QZSettings::default_kingsmith_r2_enable_hw_buttons},
|
||||
{QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed},
|
||||
{QZSettings::proform_treadmill_sport_3_0, QZSettings::default_proform_treadmill_sport_3_0},
|
||||
{QZSettings::garmin_oauth1_token, QZSettings::default_garmin_oauth1_token},
|
||||
|
||||
@@ -272,6 +272,12 @@ class QZSettings {
|
||||
static const QString miles_unit;
|
||||
static constexpr bool default_miles_unit = false;
|
||||
|
||||
/**
|
||||
*@brief Use kg for weight even when miles_unit is true (for UK users).
|
||||
*/
|
||||
static const QString weight_kg_unit;
|
||||
static constexpr bool default_weight_kg_unit = false;
|
||||
|
||||
static const QString pause_on_start;
|
||||
static constexpr bool default_pause_on_start = false;
|
||||
|
||||
@@ -448,6 +454,9 @@ class QZSettings {
|
||||
static const QString tile_heart_order;
|
||||
static constexpr int default_tile_heart_order = 11;
|
||||
|
||||
static const QString tile_heart_show_as_percent;
|
||||
static constexpr bool default_tile_heart_show_as_percent = false;
|
||||
|
||||
static const QString tile_fan_enabled;
|
||||
static constexpr bool default_tile_fan_enabled = true;
|
||||
|
||||
@@ -1052,6 +1061,12 @@ class QZSettings {
|
||||
*/
|
||||
static const QString virtual_device_rower;
|
||||
static constexpr bool default_virtual_device_rower = false;
|
||||
/**
|
||||
*@brief When virtual_device_rower is enabled, use the Concept2 PM5 protocol instead of FTMS.
|
||||
* This enables compatibility with apps like Mywhoosh that only support PM5 rowers.
|
||||
*/
|
||||
static const QString virtual_device_rower_pm5;
|
||||
static constexpr bool default_virtual_device_rower_pm5 = false;
|
||||
/**
|
||||
*@brief Used to force a non-bike device to be presented to client apps as a bike.
|
||||
*/
|
||||
@@ -1871,6 +1886,9 @@ class QZSettings {
|
||||
static const QString horizon_treadmill_7_0_at_24;
|
||||
static constexpr bool default_horizon_treadmill_7_0_at_24 = false;
|
||||
|
||||
static const QString treadmill_direct_distance;
|
||||
static constexpr bool default_treadmill_direct_distance = false;
|
||||
|
||||
static const QString treadmill_pid_heart_min;
|
||||
static constexpr int default_treadmill_pid_heart_min = 0;
|
||||
|
||||
@@ -2051,7 +2069,9 @@ class QZSettings {
|
||||
static constexpr bool default_freemotion_coachbike_b22_7 = false;
|
||||
|
||||
static const QString proform_cycle_trainer_300_ci;
|
||||
static const QString nordictrack_gx_4_5_pro;
|
||||
static constexpr bool default_proform_cycle_trainer_300_ci = false;
|
||||
static constexpr bool default_nordictrack_gx_4_5_pro = false;
|
||||
|
||||
static const QString kingsmith_encrypt_g1_walking_pad;
|
||||
static constexpr bool default_kingsmith_encrypt_g1_walking_pad = false;
|
||||
@@ -2119,6 +2139,9 @@ class QZSettings {
|
||||
static const QString domyos_treadmill_t900a;
|
||||
static constexpr bool default_domyos_treadmill_t900a = false;
|
||||
|
||||
static const QString domyos_treadmill_ts100;
|
||||
static constexpr bool default_domyos_treadmill_ts100 = false;
|
||||
|
||||
static const QString domyos_treadmill_sync_start;
|
||||
static constexpr bool default_domyos_treadmill_sync_start = false;
|
||||
|
||||
@@ -2134,6 +2157,9 @@ class QZSettings {
|
||||
static const QString zwift_click;
|
||||
static constexpr bool default_zwift_click = false;
|
||||
|
||||
static const QString thinkrider_controller;
|
||||
static constexpr bool default_thinkrider_controller = false;
|
||||
|
||||
static const QString proform_treadmill_705_cst;
|
||||
static constexpr bool default_proform_treadmill_705_cst = false;
|
||||
|
||||
@@ -2813,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
|
||||
*/
|
||||
@@ -2861,6 +2899,12 @@ class QZSettings {
|
||||
static const QString trainprogram_auto_lap_on_segment;
|
||||
static constexpr bool default_trainprogram_auto_lap_on_segment = false;
|
||||
|
||||
/**
|
||||
* @brief Enable hardware button handling (Start/Pause/Stop) for KingSmith R2 Treadmill
|
||||
*/
|
||||
static const QString kingsmith_r2_enable_hw_buttons;
|
||||
static constexpr bool default_kingsmith_r2_enable_hw_buttons = false;
|
||||
|
||||
/**
|
||||
* @brief Write the QSettings values using the constants from this namespace.
|
||||
* @param showDefaults Optionally indicates if the default should be shown with the key.
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user