mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
258 Commits
build-1103
...
wahoo_comm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f49ac87ff8 | ||
|
|
2aba7ddfe1 | ||
|
|
d02ca5a934 | ||
|
|
704c7f1f80 | ||
|
|
678ac9d466 | ||
|
|
a8a6c5d736 | ||
|
|
e8408710df | ||
|
|
47825f0783 | ||
|
|
f7ce518812 | ||
|
|
f887a068b9 | ||
|
|
6ecbce4b87 | ||
|
|
9454d75f55 | ||
|
|
4063321418 | ||
|
|
bb88d58e47 | ||
|
|
7bc2f065c0 | ||
|
|
c773b45ddf | ||
|
|
eaf7db7813 | ||
|
|
a29f6350d0 | ||
|
|
65ad925d37 | ||
|
|
8fd486d582 | ||
|
|
8fa5dcadcb | ||
|
|
6abf6c9cfd | ||
|
|
b4603da714 | ||
|
|
b27e84de69 | ||
|
|
49337cbbc6 | ||
|
|
fe2f5e923c | ||
|
|
69f54dbd54 | ||
|
|
bc20ec0d8f | ||
|
|
278add7a11 | ||
|
|
6e90091883 | ||
|
|
ebda22d7b4 | ||
|
|
625ffb3932 | ||
|
|
fe6868911e | ||
|
|
1c73d15377 | ||
|
|
c33ee55efb | ||
|
|
56979a2122 | ||
|
|
3e1db8bfdf | ||
|
|
10fdc52446 | ||
|
|
23d23c40a5 | ||
|
|
90e8eeb983 | ||
|
|
dcf395ec46 | ||
|
|
d55cb553d3 | ||
|
|
b862d26bc3 | ||
|
|
d5e4f11849 | ||
|
|
5e9679f6c3 | ||
|
|
8799c447fb | ||
|
|
bcdb767b7e | ||
|
|
15e208d34c | ||
|
|
f16c41e6dd | ||
|
|
9110c55cb1 | ||
|
|
881e155cbc | ||
|
|
e2d187a7bd | ||
|
|
66821d884a | ||
|
|
73ad1dc46c | ||
|
|
c91a2d3ee5 | ||
|
|
87c0e95b01 | ||
|
|
174da2ac14 | ||
|
|
b61ba37b8f | ||
|
|
27333e7836 | ||
|
|
58a9e81bd8 | ||
|
|
d78e92f42f | ||
|
|
2a5eb7b057 | ||
|
|
ae5f70645a | ||
|
|
d26b14276e | ||
|
|
9166ce7218 | ||
|
|
5f0ec98b0c | ||
|
|
1bc7af0a88 | ||
|
|
df75d33ca6 | ||
|
|
34f7df6bfb | ||
|
|
1208b439fa | ||
|
|
14a9faa2ee | ||
|
|
ca4fb0b35e | ||
|
|
6ea6e6d9b2 | ||
|
|
2e17aa40ec | ||
|
|
098392684f | ||
|
|
6678e225c5 | ||
|
|
ca0bd15e69 | ||
|
|
1675240f13 | ||
|
|
b21c6325bb | ||
|
|
b2f9e3d754 | ||
|
|
6dc5d74de3 | ||
|
|
be560aae89 | ||
|
|
37858ca972 | ||
|
|
d3a1a2aafb | ||
|
|
49b890715e | ||
|
|
f19449107b | ||
|
|
8bbed4fa76 | ||
|
|
efc4950f92 | ||
|
|
23fd13ad0c | ||
|
|
64cd90dfaa | ||
|
|
ec5919d67f | ||
|
|
0fc9d7fb40 | ||
|
|
4f03554fbb | ||
|
|
4a16605f43 | ||
|
|
3ae60e1c41 | ||
|
|
edab888e31 | ||
|
|
2eefcee9b7 | ||
|
|
c1db263dcf | ||
|
|
cdf0d34b86 | ||
|
|
eb0528215b | ||
|
|
30d0940359 | ||
|
|
9f7cdd8b42 | ||
|
|
af00334455 | ||
|
|
4e8af61539 | ||
|
|
a17b78c56b | ||
|
|
8f536f487e | ||
|
|
82cad601bf | ||
|
|
a3579c42fa | ||
|
|
9af0046554 | ||
|
|
d59eabc9b3 | ||
|
|
d8d55cfbf8 | ||
|
|
bce3f3cef3 | ||
|
|
e2d5e602e1 | ||
|
|
054087a3bf | ||
|
|
123d1f9634 | ||
|
|
9130cabc65 | ||
|
|
3c893444e6 | ||
|
|
24935046e9 | ||
|
|
ecf596623e | ||
|
|
620be36635 | ||
|
|
3c98edfb6d | ||
|
|
b0c4690489 | ||
|
|
64f1fce8c8 | ||
|
|
f2df53b94b | ||
|
|
ae4aec68c6 | ||
|
|
68696a585a | ||
|
|
ee0279186a | ||
|
|
60c4747a0e | ||
|
|
23e2202bc0 | ||
|
|
e9c2ed8a76 | ||
|
|
9b9174b45a | ||
|
|
e9451c1c76 | ||
|
|
28bd6423b7 | ||
|
|
083fe13ce3 | ||
|
|
574a78ba0b | ||
|
|
54177f927e | ||
|
|
a9c0a23f0a | ||
|
|
5f92401c98 | ||
|
|
2d959a580f | ||
|
|
cc046278fd | ||
|
|
af82f731cf | ||
|
|
a9ff106e54 | ||
|
|
8e2cf858b9 | ||
|
|
d19eee81b3 | ||
|
|
9f5a2ae120 | ||
|
|
6bb520a0a9 | ||
|
|
5031e01e00 | ||
|
|
4ae0c5c638 | ||
|
|
fb45b52341 | ||
|
|
156ea9e7ae | ||
|
|
1b7e86481b | ||
|
|
e11d6d7f6a | ||
|
|
c72759c70a | ||
|
|
38570d855e | ||
|
|
122ff3e25f | ||
|
|
1d48c42aa4 | ||
|
|
daae5659cf | ||
|
|
7c11ff324f | ||
|
|
e4beee9baf | ||
|
|
815d8758b0 | ||
|
|
43fc6f795d | ||
|
|
075c316bfa | ||
|
|
1ff42c9658 | ||
|
|
2add1a9425 | ||
|
|
0fd7f40412 | ||
|
|
7e0604032a | ||
|
|
265275d8aa | ||
|
|
705eb57414 | ||
|
|
9c446bcaf6 | ||
|
|
86118c04e2 | ||
|
|
081d9d4e24 | ||
|
|
4ffc0867e3 | ||
|
|
3bfecadd1f | ||
|
|
06aa01d755 | ||
|
|
e432df9f6b | ||
|
|
e3f4384014 | ||
|
|
563ced3de1 | ||
|
|
e48c6525ea | ||
|
|
ca34e99277 | ||
|
|
446f5200ba | ||
|
|
edcb7ab359 | ||
|
|
3844808b60 | ||
|
|
8e1ddc502f | ||
|
|
e633f0f671 | ||
|
|
93a38a7b79 | ||
|
|
d2f8ed8c01 | ||
|
|
60a9d7cb0f | ||
|
|
4c0793c785 | ||
|
|
5fc377f648 | ||
|
|
0d6f207991 | ||
|
|
051f296913 | ||
|
|
45a4d6d0b1 | ||
|
|
d2612ad03f | ||
|
|
6bb4d99f29 | ||
|
|
c3dbce9ea8 | ||
|
|
989315fb5e | ||
|
|
ce3782f80b | ||
|
|
4ee77b392e | ||
|
|
03896d7384 | ||
|
|
9258bf6af2 | ||
|
|
7a0a990eb8 | ||
|
|
aecb0c97df | ||
|
|
eae3f59e4a | ||
|
|
9ad0137190 | ||
|
|
f62548ac60 | ||
|
|
986bc2252e | ||
|
|
7d744ee874 | ||
|
|
8f87074e69 | ||
|
|
acd141d32b | ||
|
|
b4fb3e339a | ||
|
|
1336460297 | ||
|
|
3a45b64d51 | ||
|
|
49be559ae8 | ||
|
|
e3e15bf24d | ||
|
|
b9a7ddcaa0 | ||
|
|
bfd6de1d49 | ||
|
|
447cb04376 | ||
|
|
29dd4cf10a | ||
|
|
c4ea190370 | ||
|
|
7e51db80e6 | ||
|
|
d475e50489 | ||
|
|
1c4a22041d | ||
|
|
b07ffac325 | ||
|
|
d1dab0cd79 | ||
|
|
f5a55a253e | ||
|
|
8f5b5bd5b7 | ||
|
|
9d946dd1c5 | ||
|
|
d6c65dd7d8 | ||
|
|
8d607ca0ba | ||
|
|
253c00f014 | ||
|
|
a032a5d51c | ||
|
|
b0c2fa5b17 | ||
|
|
7cf05276e9 | ||
|
|
708f28fffb | ||
|
|
c50b7655ba | ||
|
|
9db2b8c235 | ||
|
|
134a228473 | ||
|
|
f1ad62ce4a | ||
|
|
c28a16d8d2 | ||
|
|
83e229e55f | ||
|
|
bd9bbdb236 | ||
|
|
24095fc8cc | ||
|
|
a199ba3c2b | ||
|
|
bf07764bca | ||
|
|
1f7ce9b724 | ||
|
|
51ea12782d | ||
|
|
d7c2339783 | ||
|
|
0bed0ca76c | ||
|
|
eb4ae28fa7 | ||
|
|
d1827a48ee | ||
|
|
f5c1b175a5 | ||
|
|
76b14cccb4 | ||
|
|
19efaf6878 | ||
|
|
61a96dbc63 | ||
|
|
656f2b366b | ||
|
|
ee336436e7 | ||
|
|
739270b944 | ||
|
|
bcaf466fa1 |
483
.github/workflows/main.yml
vendored
483
.github/workflows/main.yml
vendored
@@ -409,7 +409,7 @@ jobs:
|
||||
path: "src/qthttpserver"
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -647,11 +647,52 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fdroid-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk
|
||||
|
||||
android-emulator-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: android-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
api-level: [24, 26, 28, 29, 30, 31, 33, 35, 36]
|
||||
include:
|
||||
- api-level: 24
|
||||
target: default
|
||||
arch: x86
|
||||
android-version: "Android 7.0"
|
||||
- api-level: 26
|
||||
target: default
|
||||
arch: x86
|
||||
android-version: "Android 8.0"
|
||||
- api-level: 28
|
||||
target: default
|
||||
arch: x86
|
||||
android-version: "Android 9.0"
|
||||
- api-level: 29
|
||||
target: default
|
||||
arch: x86
|
||||
android-version: "Android 10"
|
||||
- api-level: 30
|
||||
target: google_apis
|
||||
arch: x86
|
||||
android-version: "Android 11"
|
||||
- api-level: 31
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 12"
|
||||
- api-level: 33
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 13"
|
||||
- api-level: 35
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 15"
|
||||
- api-level: 36
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 16"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -677,12 +718,12 @@ jobs:
|
||||
java-version: '17'
|
||||
|
||||
# Use a smaller emulator configuration
|
||||
- name: Run tests on emulator
|
||||
- name: Run tests on emulator (${{ matrix.android-version }})
|
||||
uses: ReactiveCircus/android-emulator-runner@v2
|
||||
with:
|
||||
target: default # Use default instead of Google APIs
|
||||
arch: x86
|
||||
api-level: 29
|
||||
target: ${{ matrix.target }}
|
||||
arch: ${{ matrix.arch }}
|
||||
api-level: ${{ matrix.api-level }}
|
||||
profile: Nexus 6
|
||||
disable-animations: true
|
||||
script: |
|
||||
@@ -696,49 +737,108 @@ jobs:
|
||||
# Install the APK
|
||||
adb install apk-debug/android-debug.apk
|
||||
|
||||
# Grant necessary permissions for API 25
|
||||
echo "Granting permissions..."
|
||||
# Grant necessary permissions - comprehensive list for all Android APIs
|
||||
echo "Granting all required 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
|
||||
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.qtproject.qt5.android.bindings.QtActivity
|
||||
adb shell am start -n org.cagnulen.qdomyoszwift/org.cagnulen.qdomyoszwift.CustomQtActivity
|
||||
|
||||
# Wait for app to start
|
||||
sleep 40
|
||||
sleep 90
|
||||
|
||||
# Verify the app is running
|
||||
echo "Checking if app is running..."
|
||||
adb shell "ps -A" > process_list.txt
|
||||
# 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
|
||||
echo "=== Looking for our app ==="
|
||||
|
||||
grep -q "qdomyos" process_list.txt || (echo "App process not found in process list" && echo "TEST FAILED: App process not running" && exit 1)
|
||||
adb shell pm list packages | grep org.cagnulen.qdomyoszwift
|
||||
echo "=== Checking app info ==="
|
||||
adb shell dumpsys package org.cagnulen.qdomyoszwift | grep -A5 -B5 "state"
|
||||
echo "=== Logcat output for debugging ==="
|
||||
adb logcat -d | grep -i "qdomyos\|crash\|error\|exception\|fatal" | tail -n 50
|
||||
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)
|
||||
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
|
||||
|
||||
# Display logcat output for debugging (just the last 100 lines)
|
||||
adb logcat -d | grep -i qdomyos | tail -n 100
|
||||
# Save logcat for debugging
|
||||
echo "Saving logcat for analysis..."
|
||||
adb logcat -d > full_logcat.txt
|
||||
adb logcat -d | grep -i qdomyos > qdomyos_logcat.txt
|
||||
|
||||
- name: Upload test evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-emulator-test-evidence
|
||||
name: android-emulator-test-evidence-api${{ matrix.api-level }}
|
||||
path: |
|
||||
screenshot.png
|
||||
screenshot_orientation_*.png
|
||||
process_list.txt
|
||||
full_logcat.txt
|
||||
qdomyos_logcat.txt
|
||||
if-no-files-found: warn
|
||||
|
||||
ios-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -801,7 +901,7 @@ jobs:
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
qmake CONFIG+=debug && make -j4
|
||||
qmake CONFIG+=debug CONFIG+=iphonesimulator && make -j4
|
||||
|
||||
# causes iOS build on Mac to fail
|
||||
# - name: Commit moc files
|
||||
@@ -937,7 +1037,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=C:\a\qdomyos-zwift\vcpkg\installed
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Build
|
||||
@@ -1125,7 +1225,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=C:\a\qdomyos-zwift\vcpkg\installed
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Build
|
||||
@@ -1204,7 +1304,7 @@ jobs:
|
||||
bash -c "
|
||||
set -ex &&
|
||||
apt-get update &&
|
||||
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 &&
|
||||
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 &&
|
||||
export QT_SELECT=qt5 &&
|
||||
export PATH=/usr/lib/qt5/bin:$PATH &&
|
||||
cd /github/workspace &&
|
||||
@@ -1263,7 +1363,7 @@ jobs:
|
||||
bash -c "
|
||||
set -ex &&
|
||||
apt-get update &&
|
||||
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 &&
|
||||
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 &&
|
||||
export QT_SELECT=qt5 &&
|
||||
export PATH=/usr/lib/qt5/bin:$PATH &&
|
||||
cd /github/workspace &&
|
||||
@@ -1386,7 +1486,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=C:\a\qdomyos-zwift\vcpkg\installed
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Build
|
||||
@@ -1469,16 +1569,341 @@ jobs:
|
||||
name: windows-msvc2022-binary-no-python
|
||||
path: windows-msvc2022-binary-no-python.zip
|
||||
if: ${{ ! matrix.config.python }}
|
||||
|
||||
nordictrack-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- name: treadmill
|
||||
setting_key: nordictrack_2950_ip
|
||||
setting_value: localhost
|
||||
- name: bike
|
||||
setting_key: tdf_10_ip
|
||||
setting_value: localhost
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/3478/head
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
|
||||
|
||||
- name: Checkout submodules with specific branches
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Fix qmdnsengine submodule
|
||||
run: |
|
||||
cd src/qmdnsengine
|
||||
git fetch
|
||||
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt Android
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11.0.23+9'
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
# Set variant-specific IP setting
|
||||
sed -i 's/property string ${{ matrix.variant.setting_key }}: ""/property string ${{ matrix.variant.setting_key }}: "${{ matrix.variant.setting_value }}"/' settings.qml
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
|
||||
# QTHTTPSERVER must use the same NDK
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-nordictrack-${{ matrix.variant.name }}"/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-nordictrack-${{ matrix.variant.name }}.apk/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
|
||||
|
||||
- name: Archive nordictrack binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nordictrack-${{ matrix.variant.name }}-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
|
||||
|
||||
peloton-bike-plus-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/3632/head
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
|
||||
|
||||
- name: Checkout submodules with specific branches
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Fix qmdnsengine submodule
|
||||
run: |
|
||||
cd src/qmdnsengine
|
||||
git fetch
|
||||
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt Android
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11.0.23+9'
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
|
||||
# QTHTTPSERVER must use the same NDK
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-peloton-bike-plus"/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-peloton-bike-plus.apk/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
|
||||
|
||||
- name: Archive peloton-bike-plus binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: peloton-bike-plus-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
|
||||
|
||||
peloton-bike-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/3639/head
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
|
||||
|
||||
- name: Checkout submodules with specific branches
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Fix qmdnsengine submodule
|
||||
run: |
|
||||
cd src/qmdnsengine
|
||||
git fetch
|
||||
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt Android
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11.0.23+9'
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
|
||||
# QTHTTPSERVER must use the same NDK
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-peloton-bike"/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-peloton-bike.apk/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
|
||||
|
||||
- name: Archive peloton-bike binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: peloton-bike-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
|
||||
|
||||
upload_to_release:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
needs: [linux-x86-build, window-msvc2019-build, window-msvc2022-build, ios-build, window-build, android-build, raspberry-pi-build]
|
||||
needs: [window-msvc2019-build, window-msvc2022-build, window-build, android-build, raspberry-pi-build, nordictrack-build, peloton-bike-plus-build, peloton-bike-build, raspberry-pi-build-and-image-64bit]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
@@ -1487,9 +1912,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: nightly
|
||||
tag_name: nightly-${{ steps.date.outputs.date }}
|
||||
prerelease: false
|
||||
name: 'QZ nightly build $$'
|
||||
name: 'QZ nightly build - ${{ steps.date.outputs.date }}'
|
||||
body: |
|
||||
This is a nightly build of QZ.
|
||||
|
||||
@@ -1506,6 +1931,10 @@ jobs:
|
||||
|
||||
## Other Platforms:
|
||||
- **fdroid-android-trial**: Android build
|
||||
- **nordictrack-treadmill-android-trial**: Nordictrack Treadmill build for iFIT2 Tablets
|
||||
- **nordictrack-bike-android-trial**: Nordictrack Bike build for iFIT2 Tablets
|
||||
- **peloton-bike-plus-android-trial**: Peloton Bike Plus build with Grupetto backend
|
||||
- **peloton-bike-android-trial**: Peloton Bike build with Grupetto backend
|
||||
- **raspberry-pi-binary**: Raspberry Pi build
|
||||
|
||||
__Please help us improve QZ by reporting any issues you encounter!__ :wink:
|
||||
@@ -1517,6 +1946,10 @@ jobs:
|
||||
windows-msvc2019-ai-server-binary/*
|
||||
windows-binary-no-python/*
|
||||
windows-binary/*
|
||||
fdroid-android-trial/*
|
||||
fdroid-android-trial/android-debug.apk
|
||||
nordictrack-treadmill-android-trial/android-debug-nordictrack-treadmill.apk
|
||||
nordictrack-bike-android-trial/android-debug-nordictrack-bike.apk
|
||||
peloton-bike-plus-android-trial/android-debug-peloton-bike-plus.apk
|
||||
peloton-bike-android-trial/android-debug-peloton-bike.apk
|
||||
raspberry-pi-binary/qdomyos-zwift-32bit
|
||||
raspberry-pi-binary/qdomyos-zwift-64bit
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
src/qdomyos-zwift.pro.user
|
||||
|
||||
.idea/
|
||||
|
||||
src/Makefile
|
||||
|
||||
374
CLAUDE.md
Normal file
374
CLAUDE.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
QDomyos-Zwift is a Qt-based application that bridges fitness equipment (treadmills, bikes, ellipticals, rowers) with virtual training platforms like Zwift. It acts as a Bluetooth intermediary, connecting physical equipment to fitness apps while providing enhanced features like Peloton integration, power zone training, and workout programs.
|
||||
|
||||
## Build System & Commands
|
||||
|
||||
### Build Commands
|
||||
```bash
|
||||
# Build entire project (use subdirs TEMPLATE)
|
||||
qmake
|
||||
make
|
||||
|
||||
# Build specific configurations
|
||||
qmake -r # Recursive build
|
||||
make debug # Debug build
|
||||
make release # Release build
|
||||
|
||||
# Clean build
|
||||
make clean
|
||||
make distclean
|
||||
```
|
||||
|
||||
### Platform-Specific Builds
|
||||
```bash
|
||||
# Android
|
||||
qmake -spec android-clang
|
||||
make
|
||||
|
||||
# iOS
|
||||
qmake -spec macx-ios-clang
|
||||
make
|
||||
|
||||
# Windows (MinGW)
|
||||
qmake -spec win32-g++
|
||||
make
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Build and run tests (requires main app built first)
|
||||
cd tst
|
||||
qmake
|
||||
make
|
||||
./qdomyos-zwift-tests
|
||||
|
||||
# Run with XML output for CI
|
||||
GTEST_OUTPUT=xml:test-results/ GTEST_COLOR=1 ./qdomyos-zwift-tests
|
||||
```
|
||||
|
||||
### No-GUI Mode
|
||||
```bash
|
||||
# Run application without GUI
|
||||
sudo ./qdomyos-zwift -no-gui
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Device Architecture
|
||||
The application follows a hierarchical device architecture:
|
||||
|
||||
1. **Base Class**: `bluetoothdevice` - Abstract base for all fitness devices
|
||||
- Manages Bluetooth connectivity via Qt's QLowEnergyController
|
||||
- Defines common metrics (speed, cadence, heart rate, power, distance)
|
||||
- Integrates with virtual devices for app connectivity
|
||||
|
||||
2. **Device Type Classes**: Inherit from `bluetoothdevice`
|
||||
- `bike` - Bike-specific features (resistance, gears, power zones)
|
||||
- `treadmill` - Treadmill features (speed control, inclination, pace)
|
||||
- `elliptical` - Combined bike/treadmill features
|
||||
- `rower` - Rowing metrics (stroke count, 500m pace)
|
||||
- `stairclimber` - Step counting and climbing metrics
|
||||
- `jumprope` - Jump sequence tracking
|
||||
|
||||
3. **Concrete Implementations**: Inherit from device type classes
|
||||
- Located in `src/devices/[devicename]/` folders
|
||||
- Examples: `domyosbike`, `pelotonbike`, `ftmsbike`
|
||||
|
||||
### Virtual Device System
|
||||
- `virtualdevice` - Abstract base for virtual representations
|
||||
- `virtualbike`, `virtualtreadmill`, etc. - Advertise to external apps
|
||||
- Enables bidirectional communication between physical and virtual devices
|
||||
|
||||
### Bluetooth Management
|
||||
- `bluetooth` class acts as device factory and connection manager
|
||||
- `discoveryoptions` configures device discovery process
|
||||
- Supports multiple connection types (Bluetooth LE, TCP, UDP)
|
||||
|
||||
## Key Development Areas
|
||||
|
||||
### Adding New Device Support
|
||||
1. Create device folder in `src/devices/[devicename]/`
|
||||
2. Implement device class inheriting from appropriate base type
|
||||
3. Add device detection logic to `bluetooth.cpp`
|
||||
4. Update `qdomyos-zwift.pri` with new source files
|
||||
5. Add tests in `tst/Devices/` following existing patterns
|
||||
|
||||
### Characteristics & Protocols
|
||||
- Bluetooth characteristics handlers in `src/characteristics/`
|
||||
- FTMS (Fitness Machine Service) protocol support
|
||||
- ANT+ integration for sensors
|
||||
- Custom protocol implementations for specific brands
|
||||
|
||||
### UI & QML
|
||||
- QML-based UI with Qt Quick Controls 2
|
||||
- Main QML files in `src/` (main.qml, settings.qml, etc.)
|
||||
- Platform-specific UI adaptations (iOS, Android, desktop)
|
||||
|
||||
### Integration Features
|
||||
- Peloton workout/resistance integration (`peloton.cpp`)
|
||||
- Zwift workout parsing (`zwiftworkout.cpp`)
|
||||
- GPX file support for route following (`gpx.cpp`)
|
||||
- Training program support (ZWO, XML formats)
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### iOS
|
||||
- Swift bridge files in `src/ios/`
|
||||
- Apple Watch integration via `WatchKitConnection.swift`
|
||||
- HealthKit integration for fitness data
|
||||
- ConnectIQ SDK for Garmin devices
|
||||
|
||||
### Android
|
||||
- Java bridge files in `src/android/src/`
|
||||
- ANT+ integration via Android ANT SDK
|
||||
- Foreground service for background operation
|
||||
- USB serial support for wired connections
|
||||
|
||||
### Windows
|
||||
- ADB integration for Nordic Track iFit devices
|
||||
- PaddleOCR integration for Zwift workout detection
|
||||
- Windows-specific networking features
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
### Device Files
|
||||
```
|
||||
src/devices/[devicename]/
|
||||
├── [devicename].h # Header file
|
||||
├── [devicename].cpp # Implementation
|
||||
└── README.md # Device-specific documentation (optional)
|
||||
```
|
||||
|
||||
### Test Files
|
||||
```
|
||||
tst/Devices/
|
||||
├── DeviceTestData.h # Test data definitions
|
||||
├── Test[DeviceName].h # Device-specific test cases
|
||||
└── TestBluetooth.cpp # Main device detection test suite
|
||||
```
|
||||
|
||||
## Testing Framework
|
||||
|
||||
- Uses Google Test (gtest) with Google Mock
|
||||
- Comprehensive device detection testing
|
||||
- Configuration-based test scenarios
|
||||
- XML output support for CI/CD integration
|
||||
- Tests must be built after main application (links against libqdomyos-zwift.a)
|
||||
|
||||
## Configuration & Settings
|
||||
|
||||
- Settings managed via `qzsettings.cpp` (QSettings wrapper)
|
||||
- Platform-specific configuration paths
|
||||
- Profile system for multiple users/devices
|
||||
- Extensive customization options for device behavior
|
||||
|
||||
## External Dependencies
|
||||
|
||||
- Qt 5.15.2+ (Bluetooth, WebSockets, Charts, Quick, etc.)
|
||||
- Google Test (submodule for testing)
|
||||
- Platform SDKs (Android ANT+, iOS HealthKit, Windows ADB)
|
||||
- Protocol Buffers for Zwift API integration
|
||||
- MQTT client for IoT integration
|
||||
- Various fitness platform APIs (Strava, Garmin Connect, etc.)
|
||||
|
||||
## Adding New ProForm Treadmill Models
|
||||
|
||||
This section provides a complete guide for adding new ProForm treadmill models to the codebase, based on the ProForm 995i implementation.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Bluetooth Frame Capture File**: A file containing raw Bluetooth frames from the target treadmill
|
||||
2. **Frame Analysis**: Understanding of which frames are initialization vs. sendPoll frames
|
||||
3. **BLE Header Knowledge**: Each frame has an 11-byte BLE header that must be removed
|
||||
|
||||
### Step-by-Step Implementation Process
|
||||
|
||||
#### 1. Process Bluetooth Frames
|
||||
|
||||
First, process the raw Bluetooth frames by removing the first 11 bytes (BLE header) from each frame:
|
||||
|
||||
```bash
|
||||
# Example: if you have "proform_model.c" with raw frames
|
||||
# Process each frame by removing first 11 bytes
|
||||
# Separate initialization frames from sendPoll frames
|
||||
```
|
||||
|
||||
**Key Requirements:**
|
||||
- Remove exactly 11 bytes from each frame (BLE header)
|
||||
- Identify the boundary between initialization and sendPoll frames
|
||||
- Initialization frames come first, sendPoll frames follow
|
||||
- Document which packet number starts the sendPoll sequence
|
||||
|
||||
#### 2. Add Boolean Flag to Header File
|
||||
|
||||
Add the new model flag to `src/devices/proformtreadmill/proformtreadmill.h`:
|
||||
|
||||
```cpp
|
||||
// Add before #ifdef Q_OS_IOS section
|
||||
bool proform_treadmill_newmodel = false;
|
||||
```
|
||||
|
||||
#### 3. Add Settings Support
|
||||
|
||||
Update the following files for settings integration:
|
||||
|
||||
**In `src/qzsettings.h`:**
|
||||
```cpp
|
||||
static const QString proform_treadmill_newmodel;
|
||||
static constexpr bool default_proform_treadmill_newmodel = false;
|
||||
```
|
||||
|
||||
**In `src/qzsettings.cpp`:**
|
||||
```cpp
|
||||
const QString QZSettings::proform_treadmill_newmodel = QStringLiteral("proform_treadmill_newmodel");
|
||||
```
|
||||
|
||||
* Update the `allSettingsCount` in `qzsettings.cpp`
|
||||
|
||||
#### 4. Update QML Settings UI
|
||||
|
||||
**In `src/settings.qml`:**
|
||||
|
||||
1. Add property at the END of properties list:
|
||||
```qml
|
||||
property bool proform_treadmill_newmodel: false
|
||||
```
|
||||
|
||||
2. Update ComboBox model array:
|
||||
```qml
|
||||
model: ["Disabled", "Proform New Model", ...]
|
||||
```
|
||||
|
||||
3. Add case selection logic (find next available case number):
|
||||
```qml
|
||||
currentIndex: settings.proform_treadmill_newmodel ? XX : 0;
|
||||
```
|
||||
|
||||
4. Add reset logic:
|
||||
```qml
|
||||
settings.proform_treadmill_newmodel = false;
|
||||
```
|
||||
|
||||
5. Add switch case:
|
||||
```qml
|
||||
case XX: settings.proform_treadmill_newmodel = true; break;
|
||||
```
|
||||
|
||||
#### 5. Implement Device Logic
|
||||
|
||||
**In `src/devices/proformtreadmill/proformtreadmill.cpp`:**
|
||||
|
||||
1. **Load Settings** (in constructor):
|
||||
```cpp
|
||||
proform_treadmill_newmodel = settings.value(QZSettings::proform_treadmill_newmodel, QZSettings::default_proform_treadmill_newmodel).toBool();
|
||||
```
|
||||
|
||||
2. **Add Initialization Case** (in `btinit()` method):
|
||||
```cpp
|
||||
} else if (proform_treadmill_newmodel) {
|
||||
// ALL initialization frames go here
|
||||
uint8_t initData1[] = {0x00, 0xfe, 0x02, 0x08, 0x02};
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true);
|
||||
// ... continue with ALL init frames from capture file
|
||||
// Use frames from beginning until sendPoll boundary
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add SendPoll Case** (in `sendPoll()` method):
|
||||
```cpp
|
||||
} else if (proform_treadmill_newmodel) {
|
||||
switch (counterPoll) {
|
||||
case 0:
|
||||
// First sendPoll frame
|
||||
break;
|
||||
case 1:
|
||||
// Second sendPoll frame
|
||||
break;
|
||||
// ... continue with pattern from sendPoll frames
|
||||
default:
|
||||
// Reset counter and cycle
|
||||
counterPoll = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Update Force Functions** - Add flag to conditional checks in `forceIncline()` and `forceSpeed()`:
|
||||
```cpp
|
||||
} else if (proform_treadmill_8_0 || ... || proform_treadmill_newmodel) {
|
||||
write[14] = write[11] + write[12] + 0x12;
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Requirements
|
||||
|
||||
#### Frame Processing Rules
|
||||
- **Exactly 11 bytes** must be removed from each frame (BLE header)
|
||||
- **All initialization frames** must be included in the btinit() case
|
||||
- **All sendPoll frames** must be included in the sendPoll() switch statement
|
||||
- **Frame order** must be preserved exactly as captured
|
||||
|
||||
#### Settings Integration Rules
|
||||
- **Property placement**: Always add new properties at the END of the properties list in settings.qml
|
||||
- **Case numbering**: Find the next available case number in the ComboBox switch statement
|
||||
- **Naming convention**: Use descriptive names following existing patterns
|
||||
|
||||
#### Code Organization Rules
|
||||
- **Initialization**: All init frames go in btinit() method
|
||||
- **Communication**: All sendPoll frames go in sendPoll() method with switch/case structure
|
||||
- **Force functions**: Add new model flag to existing conditional chains
|
||||
|
||||
### Common Pitfalls and Solutions
|
||||
|
||||
#### Incorrect Byte Removal
|
||||
- **Problem**: Removing wrong number of bytes (12 instead of 11)
|
||||
- **Solution**: Always remove exactly 11 bytes (BLE header)
|
||||
|
||||
#### Wrong SendPoll Boundary
|
||||
- **Problem**: Using initialization frames in sendPoll logic
|
||||
- **Solution**: Identify exact packet number where sendPoll starts
|
||||
|
||||
#### Incomplete Initialization
|
||||
- **Problem**: Missing initialization frames
|
||||
- **Solution**: Include ALL frames from start until sendPoll boundary
|
||||
|
||||
#### Settings Placement
|
||||
- **Problem**: Adding property in wrong location in settings.qml
|
||||
- **Solution**: Always add at END of properties list
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
- [ ] All 11 bytes removed from each frame
|
||||
- [ ] Initialization frames correctly identified and included
|
||||
- [ ] SendPoll frames correctly identified and implemented
|
||||
- [ ] Settings properly integrated in all required files
|
||||
- [ ] ComboBox updated with new model option
|
||||
- [ ] Force functions updated with new model flag
|
||||
- [ ] Property added at END of settings.qml properties list
|
||||
|
||||
### Example Reference
|
||||
|
||||
The ProForm 995i implementation serves as the reference example:
|
||||
- 25 initialization frames (pkt4658-pkt4756)
|
||||
- 33 sendPoll frames (pkt4761-pkt4897)
|
||||
- 6-case sendPoll switch statement with cycling logic
|
||||
- Complete settings integration across all required files
|
||||
|
||||
## Development Tips
|
||||
|
||||
- Use Qt Creator for development with proper project file support
|
||||
- The project uses Qt's signal/slot mechanism extensively
|
||||
- Device implementations should follow existing patterns for consistency
|
||||
- Add comprehensive logging using the project's logging framework
|
||||
- Test device detection thoroughly using the existing test infrastructure
|
||||
- Consider platform differences when adding new features
|
||||
|
||||
## Additional Memories
|
||||
|
||||
- When adding a new setting in QML (setting-tiles.qml), you must:
|
||||
* Add the property at the END of the properties list
|
||||
@@ -286,6 +286,8 @@
|
||||
8752C0E92B15D85600C3D1A5 /* ios_eliteariafan.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8752C0E62B15D85600C3D1A5 /* ios_eliteariafan.mm */; };
|
||||
87540FAD2848FD70005E0D44 /* libqtexttospeech_speechios.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */; };
|
||||
8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8754D24B27F786F0003D7054 /* virtualrower.swift */; };
|
||||
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */; };
|
||||
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D22E4E260B006A12E4 /* fontmanager.cpp */; };
|
||||
87586A4125B8340E00A243C4 /* proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4025B8340E00A243C4 /* proformbike.cpp */; };
|
||||
87586A4325B8341B00A243C4 /* moc_proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4225B8341B00A243C4 /* moc_proformbike.cpp */; };
|
||||
875CA9462D0C740000667EE6 /* moc_kineticinroadbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */; };
|
||||
@@ -340,6 +342,8 @@
|
||||
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9927BE35C4001D7645 /* bowflext216treadmill.cpp */; };
|
||||
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */; };
|
||||
876BFCA127BE35D8001D7645 /* moc_bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */; };
|
||||
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */; };
|
||||
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */; };
|
||||
876E4E142594748000BD5714 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 876E4E132594748000BD5714 /* Assets.xcassets */; };
|
||||
876E4E1B2594748000BD5714 /* watchkit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
876E4E202594748000BD5714 /* qdomyoszwiftApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876E4E1F2594748000BD5714 /* qdomyoszwiftApp.swift */; };
|
||||
@@ -388,6 +392,9 @@
|
||||
8781908526150C8E0085E656 /* libqtlabsplatformplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 8781908126150B490085E656 /* libqtlabsplatformplugin.a */; };
|
||||
8783153B25E8D81E0007817C /* moc_sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */; };
|
||||
8783153C25E8DAFD0007817C /* sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3EBBA25D2CFED0040EB4C /* sportstechbike.cpp */; };
|
||||
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */; };
|
||||
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */; };
|
||||
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */; };
|
||||
878531642711A3E1004B153D /* fakebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531602711A3E0004B153D /* fakebike.cpp */; };
|
||||
878531652711A3E1004B153D /* activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531612711A3E1004B153D /* activiotreadmill.cpp */; };
|
||||
878531682711A3EC004B153D /* moc_activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531662711A3EB004B153D /* moc_activiotreadmill.cpp */; };
|
||||
@@ -454,6 +461,8 @@
|
||||
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A4B75F25AF27CB0027EF3C /* metric.cpp */; };
|
||||
87A6825A2CE3AB3100586A2A /* moc_sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */; };
|
||||
87A6825D2CE3AB4000586A2A /* sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */; };
|
||||
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */; };
|
||||
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */; };
|
||||
87ADD2BB27634C1500B7A0AB /* technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */; };
|
||||
87ADD2BD27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */; };
|
||||
87AE0CB227760DCB00E547E9 /* virtualtreadmill_zwift.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87AE0CB127760DCB00E547E9 /* virtualtreadmill_zwift.swift */; };
|
||||
@@ -580,6 +589,12 @@
|
||||
87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */; };
|
||||
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */; };
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */; };
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */; };
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A52D39214E00348B15 /* workoutmodel.cpp */; };
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */; };
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
|
||||
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
|
||||
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
|
||||
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
|
||||
@@ -1196,6 +1211,9 @@
|
||||
8752C0E72B15D85600C3D1A5 /* ios_eliteariafan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ios_eliteariafan.h; path = ../src/ios/ios_eliteariafan.h; sourceTree = "<group>"; };
|
||||
87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtexttospeech_speechios.a; path = ../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios.a; sourceTree = "<group>"; };
|
||||
8754D24B27F786F0003D7054 /* virtualrower.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = virtualrower.swift; path = ../src/ios/virtualrower.swift; sourceTree = "<group>"; };
|
||||
8755E5D12E4E260B006A12E4 /* fontmanager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fontmanager.h; path = ../src/fontmanager.h; sourceTree = SOURCE_ROOT; };
|
||||
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fontmanager.cpp; path = ../src/fontmanager.cpp; sourceTree = SOURCE_ROOT; };
|
||||
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fontmanager.cpp; sourceTree = "<group>"; };
|
||||
87586A3F25B8340D00A243C4 /* proformbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformbike.h; path = ../src/devices/proformbike/proformbike.h; sourceTree = "<group>"; };
|
||||
87586A4025B8340E00A243C4 /* proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformbike.cpp; path = ../src/devices/proformbike/proformbike.cpp; sourceTree = "<group>"; };
|
||||
87586A4225B8341B00A243C4 /* moc_proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformbike.cpp; sourceTree = "<group>"; };
|
||||
@@ -1282,6 +1300,9 @@
|
||||
876BFC9B27BE35C5001D7645 /* proformelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformelliptical.h; path = ../src/devices/proformelliptical/proformelliptical.h; sourceTree = "<group>"; };
|
||||
876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformelliptical.cpp; sourceTree = "<group>"; };
|
||||
876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bowflext216treadmill.cpp; sourceTree = "<group>"; };
|
||||
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitbackupwriter.h; path = ../src/fitbackupwriter.h; sourceTree = SOURCE_ROOT; };
|
||||
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitbackupwriter.cpp; path = ../src/fitbackupwriter.cpp; sourceTree = SOURCE_ROOT; };
|
||||
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitbackupwriter.cpp; sourceTree = "<group>"; };
|
||||
876E4E112594747F00BD5714 /* watchkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = watchkit.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
876E4E132594748000BD5714 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
876E4E152594748000BD5714 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -1365,6 +1386,10 @@
|
||||
878225C234983ACB863D2D29 /* fit_nmea_sentence_mesg.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_nmea_sentence_mesg.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_nmea_sentence_mesg.hpp"; sourceTree = "<absolute>"; };
|
||||
8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechbike.cpp; sourceTree = "<group>"; };
|
||||
87842E7E25AF88FB00321E69 /* secret.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = secret.h; path = ../src/secret.h; sourceTree = "<group>"; };
|
||||
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtlabscalendarplugin.a; path = ../../Qt/5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a; sourceTree = "<group>"; };
|
||||
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbrower.cpp; sourceTree = "<group>"; };
|
||||
878521D22E44B26600922796 /* nordictrackifitadbrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbrower.h; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h; sourceTree = SOURCE_ROOT; };
|
||||
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbrower.cpp; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp; sourceTree = SOURCE_ROOT; };
|
||||
878531602711A3E0004B153D /* fakebike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakebike.cpp; path = ../src/devices/fakebike/fakebike.cpp; sourceTree = "<group>"; };
|
||||
878531612711A3E1004B153D /* activiotreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = activiotreadmill.cpp; path = ../src/devices/activiotreadmill/activiotreadmill.cpp; sourceTree = "<group>"; };
|
||||
878531622711A3E1004B153D /* activiotreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = activiotreadmill.h; path = ../src/devices/activiotreadmill/activiotreadmill.h; sourceTree = "<group>"; };
|
||||
@@ -1471,6 +1496,9 @@
|
||||
87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sramAXSController.cpp; sourceTree = "<group>"; };
|
||||
87A6825B2CE3AB4000586A2A /* sramAXSController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sramAXSController.h; path = ../src/devices/sramAXSController/sramAXSController.h; sourceTree = SOURCE_ROOT; };
|
||||
87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sramAXSController.cpp; path = ../src/devices/sramAXSController/sramAXSController.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = androidstatusbar.h; path = ../src/androidstatusbar.h; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = androidstatusbar.cpp; path = ../src/androidstatusbar.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_androidstatusbar.cpp; sourceTree = "<group>"; };
|
||||
87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = technogymmyruntreadmill.cpp; path = ../src/devices/technogymmyruntreadmill/technogymmyruntreadmill.cpp; sourceTree = "<group>"; };
|
||||
87ADD2BA27634C1400B7A0AB /* technogymmyruntreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = technogymmyruntreadmill.h; path = ../src/devices/technogymmyruntreadmill/technogymmyruntreadmill.h; sourceTree = "<group>"; };
|
||||
87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymmyruntreadmill.cpp; sourceTree = "<group>"; };
|
||||
@@ -1653,6 +1681,15 @@
|
||||
87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = qdomyoszwift_qmltyperegistrations.cpp; sourceTree = "<group>"; };
|
||||
87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproductqmltype.cpp; sourceTree = "<group>"; };
|
||||
87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproduct.cpp; sourceTree = "<group>"; };
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitdatabaseprocessor.h; path = ../src/fitdatabaseprocessor.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitdatabaseprocessor.cpp; path = ../src/fitdatabaseprocessor.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitdatabaseprocessor.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutloaderworker.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutmodel.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutloaderworker.h; path = ../src/workoutloaderworker.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutloaderworker.cpp; path = ../src/workoutloaderworker.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutmodel.h; path = ../src/workoutmodel.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutmodel.cpp; path = ../src/workoutmodel.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
|
||||
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
@@ -1904,6 +1941,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */,
|
||||
8768C9282BBC13220099DBE1 /* libcrypto.a in Link Binary With Libraries */,
|
||||
87FA94672B6B89FD00B6AB9A /* SwiftUI.framework in Link Binary With Libraries */,
|
||||
879F74112893D5B8009A64C8 /* libqavfcamera.a in Link Binary With Libraries */,
|
||||
@@ -2240,6 +2278,27 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
|
||||
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
|
||||
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
|
||||
8755E5D12E4E260B006A12E4 /* fontmanager.h */,
|
||||
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */,
|
||||
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */,
|
||||
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */,
|
||||
878521D22E44B26600922796 /* nordictrackifitadbrower.h */,
|
||||
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */,
|
||||
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */,
|
||||
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */,
|
||||
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */,
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */,
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */,
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */,
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */,
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */,
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */,
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */,
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */,
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */,
|
||||
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */,
|
||||
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */,
|
||||
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */,
|
||||
@@ -2940,6 +2999,7 @@
|
||||
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */,
|
||||
8768C9262BBC12D10099DBE1 /* libcrypto.a */,
|
||||
87FA94682B6B8A5A00B6AB9A /* libSystem.B.tbd */,
|
||||
87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */,
|
||||
@@ -3680,12 +3740,16 @@
|
||||
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */,
|
||||
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */,
|
||||
3015F9B9FF4CA6D653D46CCA /* fit_developer_field_description.cpp in Compile Sources */,
|
||||
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */,
|
||||
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */,
|
||||
87310B22266FBB78008BA0D6 /* moc_homefitnessbuddy.cpp in Compile Sources */,
|
||||
87958F1B27628D5400124B24 /* moc_elitesterzosmart.cpp in Compile Sources */,
|
||||
8768C8D82BBC12890099DBE1 /* centraldir.c in Compile Sources */,
|
||||
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */,
|
||||
87CC3BA425A0885F001EC5A8 /* elliptical.cpp in Compile Sources */,
|
||||
4AD2C93A2B8FD5855E521630 /* fit_encode.cpp in Compile Sources */,
|
||||
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */,
|
||||
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */,
|
||||
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
|
||||
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
|
||||
@@ -3805,6 +3869,12 @@
|
||||
8768C9022BBC12B80099DBE1 /* socket_loopback_client.c in Compile Sources */,
|
||||
87C5F0B926285E5F0067A1B5 /* mimehtml.cpp in Compile Sources */,
|
||||
27E452D452B62D0948DF0755 /* sessionline.cpp in Compile Sources */,
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */,
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */,
|
||||
E40895A73216AC52D35083D9 /* signalhandler.cpp in Compile Sources */,
|
||||
873CD22427EF8E18000131BC /* inappproductqmltype.cpp in Compile Sources */,
|
||||
87DF68BF25E2675100FCDA46 /* moc_schwinnic4bike.cpp in Compile Sources */,
|
||||
@@ -3864,6 +3934,8 @@
|
||||
87440FBF2640292900E4DC0B /* moc_fitplusbike.cpp in Compile Sources */,
|
||||
8768C8CA2BBC11C80099DBE1 /* sockets.c in Compile Sources */,
|
||||
87B617EC25F25FED0094A1CB /* screencapture.cpp in Compile Sources */,
|
||||
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */,
|
||||
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */,
|
||||
876F9B5F275385C9006AE6FA /* fitmetria_fanfit.cpp in Compile Sources */,
|
||||
FB2566376FE0FB17ED3DE94D /* FitDeveloperField.mm in Compile Sources */,
|
||||
43FA2D5EA73D9C89F1A333B6 /* FitEncode.mm in Compile Sources */,
|
||||
@@ -3880,6 +3952,8 @@
|
||||
2B42755BF45173E11E2110CB /* FitFieldDefinition.mm in Compile Sources */,
|
||||
873824AE27E64706004F1B46 /* moc_browser.cpp in Compile Sources */,
|
||||
8738249727E646E3004F1B46 /* characteristicnotifier2a53.cpp in Compile Sources */,
|
||||
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */,
|
||||
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */,
|
||||
DF373364C5474D877506CB26 /* FitMesg.mm in Compile Sources */,
|
||||
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */,
|
||||
872261F0289EA887006A6F75 /* moc_nordictrackelliptical.cpp in Compile Sources */,
|
||||
@@ -4381,7 +4455,8 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1100;
|
||||
CURRENT_PROJECT_VERSION = 1161;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
|
||||
@@ -4417,6 +4492,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4462,8 +4538,9 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
|
||||
);
|
||||
MARKETING_VERSION = 2.19;
|
||||
MARKETING_VERSION = 2.20;
|
||||
OTHER_CFLAGS = (
|
||||
"-pipe",
|
||||
"-g",
|
||||
@@ -4557,6 +4634,9 @@
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = "";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_INSTALL_OBJC_HEADER = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
|
||||
@@ -4575,8 +4655,9 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1100;
|
||||
CURRENT_PROJECT_VERSION = 1161;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
@@ -4613,6 +4694,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4658,8 +4740,9 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
|
||||
);
|
||||
MARKETING_VERSION = 2.19;
|
||||
MARKETING_VERSION = 2.20;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = (
|
||||
"-pipe",
|
||||
@@ -4754,6 +4837,9 @@
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = _debug;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_INSTALL_OBJC_HEADER = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
|
||||
@@ -4805,7 +4891,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1100;
|
||||
CURRENT_PROJECT_VERSION = 1161;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4830,7 +4916,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.19;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
@@ -4901,7 +4987,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1100;
|
||||
CURRENT_PROJECT_VERSION = 1161;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -4922,7 +5008,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.19;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
@@ -4993,7 +5079,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1100;
|
||||
CURRENT_PROJECT_VERSION = 1161;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5038,7 +5124,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.19;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
@@ -5109,7 +5195,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1100;
|
||||
CURRENT_PROJECT_VERSION = 1161;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5150,7 +5236,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.19;
|
||||
MARKETING_VERSION = 2.20;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
|
||||
@@ -23,6 +23,7 @@ class WatchKitConnection: NSObject {
|
||||
static let shared = WatchKitConnection()
|
||||
public static var distance = 0.0
|
||||
public static var kcal = 0.0
|
||||
public static var totalKcal = 0.0
|
||||
public static var stepCadence = 0
|
||||
public static var speed = 0.0
|
||||
public static var cadence = 0.0
|
||||
@@ -70,6 +71,9 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
|
||||
WatchKitConnection.distance = dDistance
|
||||
let dKcal = Double(result["kcal"] as! Double)
|
||||
WatchKitConnection.kcal = dKcal
|
||||
if let totalKcalDouble = result["totalKcal"] as? Double {
|
||||
WatchKitConnection.totalKcal = totalKcalDouble
|
||||
}
|
||||
|
||||
let dSpeed = Double(result["speed"] as! Double)
|
||||
WatchKitConnection.speed = dSpeed
|
||||
|
||||
@@ -28,6 +28,7 @@ class WorkoutTracking: NSObject {
|
||||
static let shared = WorkoutTracking()
|
||||
public static var distance = Double()
|
||||
public static var kcal = Double()
|
||||
public static var totalKcal = Double()
|
||||
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
|
||||
public static var cadenceLastSteps = Double()
|
||||
public static var cadenceSteps = 0
|
||||
@@ -54,20 +55,26 @@ extension WorkoutTracking {
|
||||
switch statistics.quantityType {
|
||||
case HKQuantityType.quantityType(forIdentifier: .distanceCycling):
|
||||
let distanceUnit = HKUnit.mile()
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit) else {
|
||||
return
|
||||
}
|
||||
let roundedValue = Double( round( 1 * value ) / 1 )
|
||||
delegate?.didReceiveHealthKitDistanceCycling(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
|
||||
let energyUnit = HKUnit.kilocalorie()
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit) else {
|
||||
return
|
||||
}
|
||||
let roundedValue = Double( round( 1 * value ) / 1 )
|
||||
delegate?.didReceiveHealthKitActiveEnergyBurned(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .heartRate):
|
||||
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) else {
|
||||
return
|
||||
}
|
||||
let roundedValue = Double( round( 1 * value ) / 1 )
|
||||
delegate?.didReceiveHealthKitHeartRate(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .stepCount):
|
||||
@@ -160,6 +167,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
|
||||
@@ -179,6 +187,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
}
|
||||
@@ -217,23 +226,30 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
workoutSession.stopActivity(with: Date())
|
||||
workoutSession.end()
|
||||
|
||||
guard let quantityType = HKQuantityType.quantityType(
|
||||
// Write active calories
|
||||
guard let activeQuantityType = HKQuantityType.quantityType(
|
||||
forIdentifier: .activeEnergyBurned) else {
|
||||
return
|
||||
}
|
||||
|
||||
let unit = HKUnit.kilocalorie()
|
||||
let totalEnergyBurned = WorkoutTracking.kcal
|
||||
let quantity = HKQuantity(unit: unit,
|
||||
doubleValue: totalEnergyBurned)
|
||||
let activeEnergyBurned = WorkoutTracking.kcal
|
||||
let activeQuantity = HKQuantity(unit: unit,
|
||||
doubleValue: activeEnergyBurned)
|
||||
|
||||
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
|
||||
quantity: quantity,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
|
||||
|
||||
workoutBuilder.add([sample]) {(success, error) in}
|
||||
|
||||
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
|
||||
quantity: activeQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([activeSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
let unitDistance = HKUnit.mile()
|
||||
let miles = WorkoutTracking.distance
|
||||
let quantityMiles = HKQuantity(unit: unitDistance,
|
||||
@@ -249,7 +265,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
@@ -265,6 +281,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +301,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
let sampleSteps = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeSteps,
|
||||
quantity: stepsQuantity,
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
// Add the steps sample to workout builder
|
||||
@@ -310,7 +330,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: typeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
@@ -326,6 +346,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,7 +367,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
let sampleSteps = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeSteps,
|
||||
quantity: stepsQuantity, // Use your steps quantity here
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
// Add the steps sample to workout builder
|
||||
@@ -375,7 +399,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
@@ -391,6 +415,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia sql
|
||||
QTPLUGIN += qavfmediaplayer
|
||||
QT+= charts
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ These instructions build the app itself, not the test project.
|
||||
|
||||
```buildoutcfg
|
||||
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
$ cd qdomyos-zwift
|
||||
$ git submodule update --init src/smtpclient/
|
||||
@@ -106,7 +106,7 @@ This operation takes a moment to complete.
|
||||
#### Install qdomyos-zwift from sources
|
||||
|
||||
```bash
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
cd qdomyos-zwift
|
||||
git submodule update --init src/smtpclient/
|
||||
|
||||
4
src/CLAUDE.md
Normal file
4
src/CLAUDE.md
Normal file
@@ -0,0 +1,4 @@
|
||||
when you add a setting remember:
|
||||
- you have to add always as the last settings declared in the settings.qml
|
||||
- if you have to add a setting also on another qml file, you need also to declare it there always putting as the last one
|
||||
- in the qzsettings.cpp there is a allsettingscount that must be updated if you add a setting
|
||||
@@ -9,6 +9,7 @@ ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
property int chart_display_mode: 0
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
@@ -19,6 +20,9 @@ ColumnLayout {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
} else if (loadRequest.status === WebView.LoadSucceededStatus) {
|
||||
// Send chart display mode to the web view
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
@@ -28,4 +32,22 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes in chart display mode setting
|
||||
Connections {
|
||||
target: settings
|
||||
function onChart_display_modeChanged() {
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
|
||||
function sendDisplayModeToWebView() {
|
||||
if (webView.loading === false) {
|
||||
webView.runJavaScript("
|
||||
if (window.setChartDisplayMode) {
|
||||
window.setChartDisplayMode(" + settings.chart_display_mode + ");
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,22 +13,32 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +273,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'gpx')
|
||||
fileDialogTrainProgram.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
51
src/PreviewChart.qml
Normal file
51
src/PreviewChart.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
signal popupclose()
|
||||
id: column1
|
||||
spacing: 10
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/previewchart/chart.htm"
|
||||
visible: true
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: chartJscheckStartFromWeb
|
||||
interval: 200; running: true; repeat: true
|
||||
onTriggered: {if(rootItem.startRequested) {rootItem.startRequested = false; rootItem.stopRequested = false; stackView.pop(); }}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: closeButton
|
||||
height: 50
|
||||
width: parent.width
|
||||
text: "Close"
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
popupclose();
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
headerToolbar.visible = true;
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,28 @@ import QtQuick.Dialogs 1.0
|
||||
|
||||
ColumnLayout {
|
||||
signal loadSettings(url name)
|
||||
FileDialog {
|
||||
id: fileDialogSettings
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogSettings.fileUrl)
|
||||
loadSettings(fileDialogSettings.fileUrl)
|
||||
fileDialogSettings.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogSettings.close()
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
loadSettings(fileUrl)
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +116,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'settings')
|
||||
fileDialogSettings.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import QtQuick 2.0
|
||||
import AndroidStatusBar 1.0
|
||||
import QtQuick.Window 2.12
|
||||
|
||||
/**
|
||||
* adapted from StackOverflow:
|
||||
@@ -29,7 +31,9 @@ ListView {
|
||||
z: Infinity
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 10
|
||||
anchors.bottomMargin: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ?
|
||||
((Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.navigationBarHeight + 10 : 10) : 10
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
interactive: false
|
||||
|
||||
@@ -11,22 +11,32 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +306,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'training')
|
||||
fileDialogTrainProgram.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
71
src/WorkoutTypeTag.qml
Normal file
71
src/WorkoutTypeTag.qml
Normal file
@@ -0,0 +1,71 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string workoutSource: "QZ"
|
||||
property alias text: tagText.text
|
||||
|
||||
// Auto-size based on text
|
||||
width: tagText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
|
||||
// Color scheme based on workout source
|
||||
color: {
|
||||
switch(workoutSource.toUpperCase()) {
|
||||
case "PELOTON": return "#ff6b35"
|
||||
case "ZWIFT": return "#ff6900"
|
||||
case "ERG": return "#8bc34a"
|
||||
case "QZ": return "#2196f3"
|
||||
case "MANUAL": return "#757575"
|
||||
default: return "#9e9e9e"
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle border for better definition
|
||||
border.color: Qt.darker(color, 1.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: tagText
|
||||
anchors.centerIn: parent
|
||||
text: workoutSource.toUpperCase()
|
||||
color: "white"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: "Arial"
|
||||
}
|
||||
|
||||
// Subtle shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 1
|
||||
anchors.leftMargin: 1
|
||||
radius: parent.radius
|
||||
color: "#20000000"
|
||||
z: -1
|
||||
}
|
||||
|
||||
// Hover effect for interactivity feedback
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
910
src/WorkoutsHistory.qml
Normal file
910
src/WorkoutsHistory.qml
Normal file
@@ -0,0 +1,910 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtCharts 2.15
|
||||
import Qt.labs.calendar 1.0
|
||||
|
||||
Page {
|
||||
id: workoutHistoryPage
|
||||
|
||||
|
||||
// Signal for chart preview
|
||||
signal fitfile_preview_clicked(var url)
|
||||
|
||||
// Helper function to wrap text with emoji font only on Android
|
||||
function wrapEmoji(emoji) {
|
||||
return Qt.platform.os === "android" ?
|
||||
'<font face="' + fontManager.emojiFontFamily + '">' + emoji + '</font>' :
|
||||
emoji;
|
||||
}
|
||||
|
||||
// Sport type to icon mapping (using FIT_SPORT values)
|
||||
function getSportIcon(sport) {
|
||||
switch(parseInt(sport)) {
|
||||
case 1: // FIT_SPORT_RUNNING
|
||||
case 11: // FIT_SPORT_WALKING
|
||||
return "🏃"; // Running/Walking
|
||||
case 2: // FIT_SPORT_CYCLING
|
||||
return "🚴"; // Cycling
|
||||
case 4: // FIT_SPORT_FITNESS_EQUIPMENT (Elliptical)
|
||||
return "⭕"; // Elliptical
|
||||
case 15: // FIT_SPORT_ROWING
|
||||
return "🚣"; // Rowing
|
||||
case 84: // FIT_SPORT_JUMPROPE
|
||||
return "🪢"; // Jump Rope
|
||||
default:
|
||||
return "💪"; // Generic workout
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 60
|
||||
color: "#f5f5f5"
|
||||
|
||||
// Calendar Icon Button - positioned absolutely on the left
|
||||
Button {
|
||||
id: calendarButton
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 12
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: calendarButton.pressed ? "#e0e0e0" : "#f0f0f0"
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📅") :
|
||||
"📅"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 20
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
calendarPopup.open()
|
||||
}
|
||||
}
|
||||
|
||||
// Title with filter status - centered
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Workout History"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: workoutModel && workoutModel.isDateFiltered ?
|
||||
"Filtered: " + workoutModel.filteredDate.toLocaleDateString() : ""
|
||||
font.pixelSize: 12
|
||||
color: "#666666"
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Filter Button - positioned absolutely on the right
|
||||
Button {
|
||||
id: clearFilterButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 12
|
||||
width: 100
|
||||
height: 36
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
|
||||
background: Rectangle {
|
||||
radius: 6
|
||||
color: clearFilterButton.pressed ? "#ff6666" : "#ff8888"
|
||||
border.color: "#ff4444"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: "Clear Filter"
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.clearDateFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
BusyIndicator {
|
||||
id: loadingIndicator
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? (workoutModel.isLoading || workoutModel.isDatabaseProcessing) : false
|
||||
running: visible
|
||||
}
|
||||
|
||||
// Database processing message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? workoutModel.isDatabaseProcessing : false
|
||||
text: "Processing workout files...\nThis may take a few moments on first startup."
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#666666"
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
// Workout List
|
||||
ListView {
|
||||
id: workoutListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: streakBanner.visible ? streakBanner.height + 10 : 10
|
||||
model: workoutModel
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
onContentYChanged: {
|
||||
// Hide banner when scrolling down, show when at top
|
||||
streakBanner.visible = contentY <= 20
|
||||
}
|
||||
|
||||
delegate: SwipeDelegate {
|
||||
id: swipeDelegate
|
||||
width: parent.width
|
||||
height: 135
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Delegate data:", JSON.stringify({
|
||||
sport: sport,
|
||||
title: title,
|
||||
date: date,
|
||||
duration: duration,
|
||||
distance: distance,
|
||||
calories: calories,
|
||||
id: id
|
||||
}))
|
||||
}
|
||||
|
||||
swipe.right: Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: "#FF4444"
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 20
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🗑️") + " Delete" :
|
||||
"🗑️ Delete"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
color: "white"
|
||||
font.pixelSize: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipe.onCompleted: {
|
||||
// Show confirmation dialog
|
||||
confirmDialog.workoutId = model.id
|
||||
confirmDialog.workoutTitle = model.title
|
||||
confirmDialog.open()
|
||||
}
|
||||
|
||||
// Card-like container
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
radius: 10
|
||||
color: "white"
|
||||
border.color: "#e0e0e0"
|
||||
|
||||
// Workout Type Tag - positioned absolutely in top-right
|
||||
WorkoutTypeTag {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
workoutSource: workoutModel ? workoutModel.getWorkoutSource(model.id) : "QZ"
|
||||
}
|
||||
|
||||
// Action buttons - positioned absolutely in bottom-right
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
// Peloton URL button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.getWorkoutSource(model.id) === "PELOTON" &&
|
||||
workoutModel.getPelotonUrl(model.id) !== ""
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#ff8855" : "#ff6b35"
|
||||
radius: 6
|
||||
border.color: "#cc5529"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🌐") :
|
||||
"🌐"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.openPelotonUrl(model.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.hasTrainingProgram(model.id)
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#1976d2" : "#2196f3"
|
||||
radius: 6
|
||||
border.color: "#1565c0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📋") :
|
||||
"📋"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var success = workoutModel.loadTrainingProgram(model.id)
|
||||
if (success) {
|
||||
trainingProgramDialog.title = "Success"
|
||||
trainingProgramDialog.message = "Training program loaded successfully!"
|
||||
trainingProgramDialog.isSuccess = true
|
||||
} else {
|
||||
trainingProgramDialog.title = "Error"
|
||||
trainingProgramDialog.message = "Failed to load training program. Please check if the file exists."
|
||||
trainingProgramDialog.isSuccess = false
|
||||
}
|
||||
trainingProgramDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 16
|
||||
|
||||
// Sport icon
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji(getSportIcon(sport)) :
|
||||
getSportIcon(sport)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 32
|
||||
}
|
||||
}
|
||||
|
||||
// Workout info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
// Title row (without tag) with auto-scrolling
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 80 // Reserve space for tag
|
||||
Layout.preferredHeight: 24
|
||||
clip: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
text: title
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Auto-scroll animation for long titles
|
||||
SequentialAnimation on x {
|
||||
running: titleText.contentWidth > titleText.parent.width
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0
|
||||
to: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 1500 }
|
||||
NumberAnimation {
|
||||
from: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
to: 0
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 2000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: date
|
||||
color: "#666666"
|
||||
}
|
||||
|
||||
// Stats row
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "⏱ " + duration
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "📏 " + distance.toFixed(2) + " km"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " + Math.round(calories) + " kcal" :
|
||||
"🔥 " + Math.round(calories) + " kcal"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
console.log("Workout clicked, ID:", model.id)
|
||||
|
||||
// Get workout details from the model
|
||||
var details = workoutModel.getWorkoutDetails(model.id)
|
||||
console.log("Workout details:", JSON.stringify(details))
|
||||
|
||||
// Emit signal with file URL for chart preview - same pattern as profiles.qml
|
||||
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
|
||||
// Convert to URL like profiles.qml does with FolderListModel
|
||||
var fileUrl = "file://" + details.filePath
|
||||
console.log("Converted to URL:", fileUrl)
|
||||
workoutHistoryPage.fitfile_preview_clicked(fileUrl)
|
||||
|
||||
// Push the ChartJsTest view
|
||||
stackView.push("PreviewChart.qml")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation Dialog
|
||||
Dialog {
|
||||
id: confirmDialog
|
||||
|
||||
property int workoutId
|
||||
property string workoutTitle
|
||||
|
||||
title: "Delete Workout"
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
Text {
|
||||
text: "Are you sure you want to delete '" + confirmDialog.workoutTitle + "'?"
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
workoutModel.deleteWorkout(confirmDialog.workoutId)
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
onRejected: {
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program Loading Dialog
|
||||
Dialog {
|
||||
id: trainingProgramDialog
|
||||
|
||||
property string message: ""
|
||||
property bool isSuccess: true
|
||||
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 8
|
||||
border.color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
border.width: 2
|
||||
}
|
||||
|
||||
header: Rectangle {
|
||||
height: 50
|
||||
color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
radius: 8
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: trainingProgramDialog.title
|
||||
color: "white"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
Layout.margins: 20
|
||||
Layout.preferredWidth: 300
|
||||
Layout.preferredHeight: 120
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " +
|
||||
wrapEmoji(trainingProgramDialog.isSuccess ? '✅' : '❌') +
|
||||
" " + trainingProgramDialog.message :
|
||||
"🔥 " + (trainingProgramDialog.isSuccess ? '✅ ' : '❌ ') + trainingProgramDialog.message
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streak Banner at the bottom
|
||||
Rectangle {
|
||||
id: streakBanner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 80
|
||||
visible: workoutModel
|
||||
|
||||
Behavior on visible {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Special pulsing effect for major milestones
|
||||
SequentialAnimation on opacity {
|
||||
running: workoutModel && workoutModel.currentStreak >= 30
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0.9; to: 1.0; duration: 1500; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { from: 1.0; to: 0.9; duration: 1500; easing.type: Easing.InOutSine }
|
||||
}
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFD700" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#9932CC" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#FF1493" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF4500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF6347" : "#FF6B35"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFA500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#8A2BE2" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#DC143C" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF6B35" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF4500" : "#F7931E"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40FFFFFF" }
|
||||
GradientStop { position: 1.0; color: "#00FFFFFF" }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
// Current streak with count
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 15
|
||||
|
||||
// Fire emoji with animation
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("👑🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🎖️🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🦁🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🎊🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🏆🔥") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "👑🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🎖️🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🦁🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🎊🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🏆🔥" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 7 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 365 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Special sparkle effect for year achievement
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: 360; duration: 3000; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
|
||||
// Current streak count
|
||||
Text {
|
||||
text: workoutModel ? workoutModel.currentStreak + " day" + (workoutModel.currentStreak !== 1 ? "s" : "") + " streak" : ""
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: "white"
|
||||
visible: workoutModel
|
||||
}
|
||||
|
||||
// Another fire emoji
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("🔥👑") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🔥🎖️") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🔥🦁") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🔥🎊") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🔥🏆") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "🔥👑" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🔥🎖️" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🔥🦁" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🔥🎊" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🔥🏆" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 365 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Counter-rotation for variety
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: -360; duration: 3500; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Motivational message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? workoutModel.streakMessage : ""
|
||||
font.pixelSize: 14
|
||||
font.italic: true
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.streakMessage !== ""
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
// Best streak (smaller text)
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? "Personal best: " + workoutModel.longestStreak + " day" + (workoutModel.longestStreak !== 1 ? "s" : "") : ""
|
||||
font.pixelSize: 12
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.longestStreak > workoutModel.currentStreak && workoutModel.longestStreak > 0
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle shadow effect at the top
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40000000" }
|
||||
GradientStop { position: 1.0; color: "#00000000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Popup
|
||||
Popup {
|
||||
id: calendarPopup
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
width: Math.min(parent.width * 0.9, 400)
|
||||
height: Math.min(parent.height * 0.8, 500)
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onOpened: {
|
||||
// Refresh workout dates when calendar opens
|
||||
if (workoutModel) {
|
||||
calendar.workoutDates = workoutModel.getWorkoutDates()
|
||||
console.log("Calendar opened, refreshed workout dates:", JSON.stringify(calendar.workoutDates))
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 12
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
|
||||
// Shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 2
|
||||
anchors.leftMargin: 2
|
||||
radius: parent.radius
|
||||
color: "#40000000"
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
// Calendar Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
text: "<"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() - 1, 1)
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: calendar.selectedDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
text: ">"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Grid
|
||||
GridLayout {
|
||||
id: calendar
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
columns: 7
|
||||
|
||||
property date selectedDate: new Date()
|
||||
property var workoutDates: workoutModel ? workoutModel.getWorkoutDates() : []
|
||||
|
||||
// Debug: print workout dates when they change
|
||||
onWorkoutDatesChanged: {
|
||||
console.log("Calendar workout dates updated:", JSON.stringify(workoutDates))
|
||||
}
|
||||
|
||||
// Day headers
|
||||
Repeater {
|
||||
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
text: modelData
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: "#666666"
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar days
|
||||
Repeater {
|
||||
model: getCalendarDays()
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 40
|
||||
|
||||
property date dayDate: modelData.date
|
||||
property bool isCurrentMonth: modelData.currentMonth
|
||||
property bool hasWorkout: modelData.hasWorkout
|
||||
property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
|
||||
color: {
|
||||
if (mouseArea.pressed) return "#e3f2fd"
|
||||
if (isToday) return "#bbdefb"
|
||||
if (!isCurrentMonth) return "#f5f5f5"
|
||||
return "white"
|
||||
}
|
||||
|
||||
border.color: isToday ? "#2196f3" : "#e0e0e0"
|
||||
border.width: isToday ? 2 : 1
|
||||
radius: 4
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: dayDate.getDate()
|
||||
color: isCurrentMonth ? "black" : "#cccccc"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
// Workout indicator dot
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: "#ff6b35"
|
||||
visible: hasWorkout
|
||||
border.width: 1
|
||||
border.color: "#cc5529"
|
||||
|
||||
// Debug: log when a dot should be visible
|
||||
Component.onCompleted: {
|
||||
if (hasWorkout) {
|
||||
console.log("Workout dot visible for date:", dayDate.toDateString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
if (isCurrentMonth) {
|
||||
var year = dayDate.getFullYear();
|
||||
var month = dayDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = dayDate.getDate();
|
||||
var dateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
workoutModel.setDateFilter(dateString);
|
||||
calendarPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "Close"
|
||||
onClicked: calendarPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript functions for calendar
|
||||
function getCalendarDays() {
|
||||
var days = []
|
||||
var firstDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth(), 1)
|
||||
var lastDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 0)
|
||||
var startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay()) // Go back to start of week
|
||||
|
||||
var workoutDates = calendar.workoutDates || []
|
||||
console.log("getCalendarDays: workoutDates received:", JSON.stringify(workoutDates))
|
||||
|
||||
// workoutDates is now a QStringList (array of strings in format "yyyy-MM-dd")
|
||||
var workoutDateStrings = workoutDates || []
|
||||
console.log("Final workout date strings:", JSON.stringify(workoutDateStrings))
|
||||
|
||||
for (var i = 0; i < 42; i++) { // 6 rows x 7 days
|
||||
var currentDate = new Date(startDate)
|
||||
currentDate.setDate(startDate.getDate() + i)
|
||||
|
||||
// Costruisci la stringa YYYY-MM-DD dai componenti della data locale per evitare problemi di fuso orario
|
||||
var year = currentDate.getFullYear();
|
||||
var month = currentDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = currentDate.getDate();
|
||||
var localDateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
var hasWorkout = workoutDateStrings.indexOf(localDateString) !== -1;
|
||||
if (hasWorkout) {
|
||||
// Questo console.log ora utilizza la stringa della data locale corretta per la corrispondenza
|
||||
console.log("Found workout match for:", localDateString);
|
||||
}
|
||||
|
||||
var isCurrentMonth = currentDate.getMonth() === calendar.selectedDate.getMonth()
|
||||
|
||||
days.push({
|
||||
date: currentDate,
|
||||
currentMonth: isCurrentMonth,
|
||||
hasWorkout: hasWorkout
|
||||
})
|
||||
}
|
||||
|
||||
console.log("getCalendarDays: returning", days.length, "days")
|
||||
return days
|
||||
}
|
||||
}
|
||||
@@ -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.18.25" android:versionCode="1090" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.11" android:versionCode="1155" android:installLocation="auto">
|
||||
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default permissions. -->
|
||||
<!-- %%INSERT_PERMISSIONS -->
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon" android:usesCleartextTraffic="true">
|
||||
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="QZ" android:launchMode="singleTop">
|
||||
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.cagnulen.qdomyoszwift.CustomQtActivity" android:label="QZ" android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -120,7 +120,7 @@
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
</application>
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="36" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
|
||||
|
||||
@@ -44,7 +44,7 @@ dependencies {
|
||||
|
||||
def appcompat_version = "1.3.1"
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "com.android.billingclient:billing:6.0.1"
|
||||
implementation "com.android.billingclient:billing:8.0.0"
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appcompat_version"
|
||||
@@ -129,7 +129,7 @@ android {
|
||||
resConfig "en"
|
||||
compileSdkVersion 33
|
||||
minSdkVersion = 21
|
||||
targetSdkVersion = 34
|
||||
targetSdkVersion = 36
|
||||
}
|
||||
|
||||
tasks.all { task ->
|
||||
|
||||
@@ -35,15 +35,21 @@ public class Ant {
|
||||
static boolean bikeRequest = false; // Added bike request flag
|
||||
static boolean garminKey = false;
|
||||
static boolean treadmill = false;
|
||||
static boolean technoGymGroupCycle = false;
|
||||
static int antBikeDeviceNumber = 0;
|
||||
static int antHeartDeviceNumber = 0;
|
||||
|
||||
// Updated antStart method with BikeRequest parameter at the end
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest) {
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber) {
|
||||
QLog.v(TAG, "antStart");
|
||||
speedRequest = SpeedRequest;
|
||||
heartRequest = HeartRequest;
|
||||
treadmill = Treadmill;
|
||||
garminKey = GarminKey;
|
||||
bikeRequest = BikeRequest; // Set bike request flag
|
||||
technoGymGroupCycle = TechnoGymGroupCycle;
|
||||
antBikeDeviceNumber = AntBikeDeviceNumber;
|
||||
antHeartDeviceNumber = AntHeartDeviceNumber;
|
||||
activity = a;
|
||||
if(a != null)
|
||||
QLog.v(TAG, "antStart activity is valid");
|
||||
|
||||
@@ -12,6 +12,15 @@ import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IGeneralFitnes
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentType;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.HeartRateDataSource;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.IRawPowerOnlyDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.ICalculatedPowerReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedSpeedReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedAccumulatedDistanceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.IRawSpeedAndDistanceDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc.ICalculatedCadenceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
|
||||
@@ -29,9 +38,17 @@ public class BikeChannelController {
|
||||
private Context context;
|
||||
private AntPlusFitnessEquipmentPcc fePcc = null;
|
||||
private PccReleaseHandle<AntPlusFitnessEquipmentPcc> releaseHandle = null;
|
||||
private AntPlusBikePowerPcc powerPcc = null;
|
||||
private PccReleaseHandle<AntPlusBikePowerPcc> powerReleaseHandle = null;
|
||||
private AntPlusBikeSpeedDistancePcc speedCadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeSpeedDistancePcc> speedCadenceReleaseHandle = null;
|
||||
private AntPlusBikeCadencePcc cadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeCadencePcc> cadenceReleaseHandle = null;
|
||||
private boolean isConnected = false;
|
||||
private boolean isPowerConnected = false;
|
||||
private boolean isSpeedCadenceConnected = false;
|
||||
|
||||
// Bike data fields
|
||||
// Bike data fields - from fitness equipment
|
||||
public int cadence = 0; // Current cadence in RPM
|
||||
public int power = 0; // Current power in watts
|
||||
public BigDecimal speed = new BigDecimal(0); // Current speed in m/s
|
||||
@@ -42,6 +59,12 @@ public class BikeChannelController {
|
||||
public int heartRate = 0; // Heart rate from equipment
|
||||
public HeartRateDataSource heartRateSource = HeartRateDataSource.UNKNOWN;
|
||||
public BigDecimal elapsedTime = new BigDecimal(0); // Elapsed time in seconds
|
||||
|
||||
// Bike data fields - from dedicated sensors
|
||||
public int powerSensorPower = 0; // Power from dedicated power sensor
|
||||
public int speedSensorCadence = 0; // Cadence from speed/cadence sensor
|
||||
public BigDecimal speedSensorSpeed = new BigDecimal(0); // Speed from speed/cadence sensor
|
||||
public long speedSensorDistance = 0; // Distance from speed/cadence sensor
|
||||
|
||||
// Fitness equipment state receiver
|
||||
private final IFitnessEquipmentStateReceiver mFitnessEquipmentStateReceiver =
|
||||
@@ -63,9 +86,18 @@ public class BikeChannelController {
|
||||
}
|
||||
};
|
||||
|
||||
public BikeChannelController() {
|
||||
public BikeChannelController(boolean technoGymGroupCycle, int antBikeDeviceNumber) {
|
||||
this.context = Ant.activity;
|
||||
openChannel();
|
||||
|
||||
if (technoGymGroupCycle) {
|
||||
// For Technogym Group Cycle: disable openChannel, enable openPowerSensorChannel
|
||||
openPowerSensorChannel(antBikeDeviceNumber);
|
||||
} else {
|
||||
// Standard behavior: enable openChannel, disable openPowerSensorChannel
|
||||
openChannel();
|
||||
}
|
||||
|
||||
//openSpeedCadenceSensorChannel();
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
@@ -123,6 +155,106 @@ public class BikeChannelController {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public boolean openPowerSensorChannel(int deviceNumber) {
|
||||
// Request access to power sensor device (deviceNumber = 0 means first available)
|
||||
powerReleaseHandle = AntPlusBikePowerPcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
new IPluginAccessResultReceiver<AntPlusBikePowerPcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikePowerPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
powerPcc = result;
|
||||
isPowerConnected = true;
|
||||
QLog.d(TAG, "Connected to power sensor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
subscribeToPowerSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Power Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Power Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Power Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Power Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Power Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Power Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized power sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Power Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isPowerConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean openSpeedCadenceSensorChannel() {
|
||||
// Request access to first available speed/cadence sensor device
|
||||
speedCadenceReleaseHandle = AntPlusBikeSpeedDistancePcc.requestAccess((Activity)context, context,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeSpeedDistancePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeSpeedDistancePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
speedCadencePcc = result;
|
||||
isSpeedCadenceConnected = true;
|
||||
QLog.d(TAG, "Connected to speed/cadence sensor: " + result.getDeviceName());
|
||||
subscribeToSpeedCadenceSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Speed/Cadence Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Speed/Cadence Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Speed/Cadence Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Speed/Cadence Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized speed/cadence sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Speed/Cadence Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isSpeedCadenceConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
private void subscribeToBikeEvents() {
|
||||
if (fePcc != null) {
|
||||
// General fitness equipment data
|
||||
@@ -181,36 +313,181 @@ public class BikeChannelController {
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToPowerSensorEvents() {
|
||||
if (powerPcc != null) {
|
||||
// Subscribe to raw power-only data events
|
||||
powerPcc.subscribeRawPowerOnlyDataEvent(new IRawPowerOnlyDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawPowerOnlyData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
long powerOnlyUpdateEventCount, int instantaneousPower,
|
||||
long accumulatedPower) {
|
||||
if (instantaneousPower != -1) {
|
||||
powerSensorPower = instantaneousPower;
|
||||
QLog.d(TAG, "Power Sensor Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also subscribe to calculated power events
|
||||
powerPcc.subscribeCalculatedPowerEvent(new ICalculatedPowerReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedPower(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
AntPlusBikePowerPcc.DataSource dataSource,
|
||||
BigDecimal calculatedPower) {
|
||||
if (calculatedPower != null && calculatedPower.intValue() != -1) {
|
||||
powerSensorPower = calculatedPower.intValue();
|
||||
QLog.d(TAG, "Power Sensor Calculated Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToSpeedCadenceSensorEvents() {
|
||||
if (speedCadencePcc != null) {
|
||||
// 2.095m circumference = average 700cx23mm road tire
|
||||
BigDecimal wheelCircumference = new BigDecimal("2.095");
|
||||
|
||||
// Subscribe to calculated speed events
|
||||
speedCadencePcc.subscribeCalculatedSpeedEvent(new CalculatedSpeedReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedSpeed(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedSpeed) {
|
||||
if (calculatedSpeed != null && calculatedSpeed.doubleValue() > 0) {
|
||||
speedSensorSpeed = calculatedSpeed;
|
||||
QLog.d(TAG, "Speed Sensor Data - Speed: " + speedSensorSpeed + "m/s");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to calculated distance events
|
||||
speedCadencePcc.subscribeCalculatedAccumulatedDistanceEvent(new CalculatedAccumulatedDistanceReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedAccumulatedDistance(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedAccumulatedDistance) {
|
||||
if (calculatedAccumulatedDistance != null && calculatedAccumulatedDistance.longValue() > 0) {
|
||||
speedSensorDistance = calculatedAccumulatedDistance.longValue();
|
||||
QLog.d(TAG, "Speed Sensor Data - Distance: " + speedSensorDistance + "m");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to raw speed and distance data
|
||||
speedCadencePcc.subscribeRawSpeedAndDistanceDataEvent(new IRawSpeedAndDistanceDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawSpeedAndDistanceData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal timestampOfLastEvent, long cumulativeRevolutions) {
|
||||
QLog.d(TAG, "Speed/Distance Raw Data - Revs: " + cumulativeRevolutions + ", Time: " + timestampOfLastEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if this is a combined speed/cadence sensor
|
||||
if (speedCadencePcc.isSpeedAndCadenceCombinedSensor()) {
|
||||
// Connect to cadence functionality
|
||||
cadenceReleaseHandle = AntPlusBikeCadencePcc.requestAccess(
|
||||
(Activity)context, speedCadencePcc.getAntDeviceNumber(), 0, true,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeCadencePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeCadencePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
if (resultCode == RequestAccessResult.SUCCESS) {
|
||||
cadencePcc = result;
|
||||
cadencePcc.subscribeCalculatedCadenceEvent(new ICalculatedCadenceReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedCadence(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedCadence) {
|
||||
if (calculatedCadence != null && calculatedCadence.intValue() > 0) {
|
||||
speedSensorCadence = calculatedCadence.intValue();
|
||||
QLog.d(TAG, "Cadence Sensor Data - Cadence: " + speedSensorCadence + "rpm");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Cadence Sensor State Changed to: " + newDeviceState);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (releaseHandle != null) {
|
||||
releaseHandle.close();
|
||||
releaseHandle = null;
|
||||
}
|
||||
if (powerReleaseHandle != null) {
|
||||
powerReleaseHandle.close();
|
||||
powerReleaseHandle = null;
|
||||
}
|
||||
if (speedCadenceReleaseHandle != null) {
|
||||
speedCadenceReleaseHandle.close();
|
||||
speedCadenceReleaseHandle = null;
|
||||
}
|
||||
if (cadenceReleaseHandle != null) {
|
||||
cadenceReleaseHandle.close();
|
||||
cadenceReleaseHandle = null;
|
||||
}
|
||||
fePcc = null;
|
||||
powerPcc = null;
|
||||
speedCadencePcc = null;
|
||||
cadencePcc = null;
|
||||
isConnected = false;
|
||||
QLog.d(TAG, "Channel Closed");
|
||||
isPowerConnected = false;
|
||||
isSpeedCadenceConnected = false;
|
||||
QLog.d(TAG, "All Channels Closed");
|
||||
}
|
||||
|
||||
// Getter methods for bike data
|
||||
// Getter methods for bike data with sensor reconciliation
|
||||
public int getCadence() {
|
||||
return cadence;
|
||||
// Priority: 1) Fitness Equipment, 2) Speed/Cadence Sensor, 3) Power Sensor
|
||||
if (isConnected && cadence > 0) {
|
||||
return cadence; // From fitness equipment
|
||||
} else if (isSpeedCadenceConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From dedicated speed/cadence sensor
|
||||
} else if (isPowerConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From power sensor (if it provides cadence)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getPower() {
|
||||
return power;
|
||||
// Priority: 1) Dedicated Power Sensor, 2) Fitness Equipment
|
||||
if (isPowerConnected && powerSensorPower > 0) {
|
||||
return powerSensorPower; // From dedicated power sensor (most accurate)
|
||||
} else if (isConnected && power > 0) {
|
||||
return power; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double getSpeedKph() {
|
||||
// Convert from m/s to km/h
|
||||
return speed.doubleValue() * 3.6;
|
||||
return getSpeedMps() * 3.6;
|
||||
}
|
||||
|
||||
public double getSpeedMps() {
|
||||
return speed.doubleValue();
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorSpeed.doubleValue() > 0) {
|
||||
return speedSensorSpeed.doubleValue(); // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && speed.doubleValue() > 0) {
|
||||
return speed.doubleValue(); // From fitness equipment
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public long getDistance() {
|
||||
return distance;
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorDistance > 0) {
|
||||
return speedSensorDistance; // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && distance > 0) {
|
||||
return distance; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long getCalories() {
|
||||
@@ -236,4 +513,50 @@ public class BikeChannelController {
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
// Additional connection status methods
|
||||
public boolean isPowerSensorConnected() {
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean isSpeedCadenceSensorConnected() {
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
public boolean isAnyDeviceConnected() {
|
||||
return isConnected || isPowerConnected || isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
// Raw sensor data getters (for debugging/advanced use)
|
||||
public int getRawFitnessEquipmentPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public int getRawPowerSensorPower() {
|
||||
return powerSensorPower;
|
||||
}
|
||||
|
||||
public int getRawFitnessEquipmentCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public int getRawSpeedSensorCadence() {
|
||||
return speedSensorCadence;
|
||||
}
|
||||
|
||||
public double getRawFitnessEquipmentSpeed() {
|
||||
return speed.doubleValue();
|
||||
}
|
||||
|
||||
public double getRawSpeedSensorSpeed() {
|
||||
return speedSensorSpeed.doubleValue();
|
||||
}
|
||||
|
||||
public long getRawFitnessEquipmentDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
public long getRawSpeedSensorDistance() {
|
||||
return speedSensorDistance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ public class ChannelService extends Service {
|
||||
|
||||
public void openAllChannels() throws ChannelNotAvailableException {
|
||||
if (Ant.heartRequest && heartChannelController == null)
|
||||
heartChannelController = new HeartChannelController();
|
||||
heartChannelController = new HeartChannelController(Ant.antHeartDeviceNumber);
|
||||
|
||||
if (Ant.speedRequest) {
|
||||
if(Ant.treadmill && sdmChannelController == null) {
|
||||
@@ -330,7 +330,7 @@ public class ChannelService extends Service {
|
||||
|
||||
// Add initialization for BikeChannelController (receiver)
|
||||
if (Ant.bikeRequest && bikeChannelController == null) {
|
||||
bikeChannelController = new BikeChannelController();
|
||||
bikeChannelController = new BikeChannelController(Ant.technoGymGroupCycle, Ant.antBikeDeviceNumber);
|
||||
}
|
||||
|
||||
// Add initialization for BikeTransmitterController (transmitter) - only when NOT treadmill
|
||||
|
||||
91
src/android/src/CustomQtActivity.java
Normal file
91
src/android/src/CustomQtActivity.java
Normal file
@@ -0,0 +1,91 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.DisplayCutout;
|
||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||
|
||||
public class CustomQtActivity extends QtActivity {
|
||||
private static final String TAG = "CustomQtActivity";
|
||||
|
||||
// Declare the native method that will be implemented in C++
|
||||
private static native void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Log.d(TAG, "onCreate: CustomQtActivity initialized");
|
||||
|
||||
// This tells the OS that we want to handle the display cutout area ourselves
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
|
||||
// This is the core of the new solution. We set a listener on the main view.
|
||||
// The OS will call this listener whenever the insets change (e.g., on rotation).
|
||||
final View decorView = getWindow().getDecorView();
|
||||
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
final float density = getResources().getDisplayMetrics().density;
|
||||
int top = 0;
|
||||
int bottom = 0;
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
|
||||
if (density > 0) {
|
||||
// Use system window insets as primary source
|
||||
top = Math.round(insets.getSystemWindowInsetTop() / density);
|
||||
bottom = Math.round(insets.getSystemWindowInsetBottom() / density);
|
||||
left = Math.round(insets.getSystemWindowInsetLeft() / density);
|
||||
right = Math.round(insets.getSystemWindowInsetRight() / density);
|
||||
|
||||
// For API 28+, also check display cutout for additional padding
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
// Use the maximum between system window inset and cutout safe inset
|
||||
left = Math.max(left, Math.round(cutout.getSafeInsetLeft() / density));
|
||||
right = Math.max(right, Math.round(cutout.getSafeInsetRight() / density));
|
||||
top = Math.max(top, Math.round(cutout.getSafeInsetTop() / density));
|
||||
bottom = Math.max(bottom, Math.round(cutout.getSafeInsetBottom() / density));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "onApplyWindowInsets - Top:" + top + " Bottom:" + bottom + " Left:" + left + " Right:" + right);
|
||||
Log.d(TAG, "Raw insets - SystemTop:" + insets.getSystemWindowInsetTop() +
|
||||
" SystemBottom:" + insets.getSystemWindowInsetBottom() +
|
||||
" SystemLeft:" + insets.getSystemWindowInsetLeft() +
|
||||
" SystemRight:" + insets.getSystemWindowInsetRight());
|
||||
Log.d(TAG, "Stable insets - StableTop:" + insets.getStableInsetTop() +
|
||||
" StableBottom:" + insets.getStableInsetBottom() +
|
||||
" StableLeft:" + insets.getStableInsetLeft() +
|
||||
" StableRight:" + insets.getStableInsetRight());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
Log.d(TAG, "Cutout insets - Top:" + cutout.getSafeInsetTop() +
|
||||
" Bottom:" + cutout.getSafeInsetBottom() +
|
||||
" Left:" + cutout.getSafeInsetLeft() +
|
||||
" Right:" + cutout.getSafeInsetRight());
|
||||
}
|
||||
}
|
||||
|
||||
// Push the new, correct inset values to the C++ layer
|
||||
onInsetsChanged(top, bottom, left, right);
|
||||
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This method is still needed for the QML check
|
||||
public static int getApiLevel() {
|
||||
return Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,10 @@ import android.webkit.WebSettings;
|
||||
import android.webkit.WebViewClient;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
public class FloatingWindowGFG extends Service {
|
||||
|
||||
@@ -37,6 +41,14 @@ public class FloatingWindowGFG extends Service {
|
||||
private WindowManager.LayoutParams floatWindowLayoutParam;
|
||||
private WindowManager windowManager;
|
||||
private Button maximizeBtn;
|
||||
private Handler handler;
|
||||
private Runnable paddingTimeoutRunnable;
|
||||
private boolean isDraggingEnabled = false;
|
||||
private int originalHeight;
|
||||
private boolean isExpanded = false;
|
||||
private WebView webView;
|
||||
private int originalMargin = 20; // in dp, matching the XML layout
|
||||
private int reducedMargin = 2; // minimal margin when not dragging
|
||||
|
||||
// Retrieve the user preference node for the package com.mycompany
|
||||
SharedPreferences sharedPreferences;
|
||||
@@ -56,6 +68,9 @@ public class FloatingWindowGFG extends Service {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Initialize handler for timeout operations
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// The screen height and width are calculated, cause
|
||||
// the height and width of the floating window is set depending on this
|
||||
/*DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
|
||||
@@ -73,23 +88,30 @@ public class FloatingWindowGFG extends Service {
|
||||
// inflate a new view hierarchy from the floating_layout xml
|
||||
floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
|
||||
|
||||
WebView wv = (WebView)floatView.findViewById(R.id.webview);
|
||||
wv.setWebViewClient(new WebViewClient(){
|
||||
webView = (WebView)floatView.findViewById(R.id.webview);
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
WebSettings settings = wv.getSettings();
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
wv.clearView();
|
||||
wv.measure(100, 100);
|
||||
wv.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
|
||||
// Add JavaScript interface for communication with HTML
|
||||
webView.addJavascriptInterface(new WebAppInterface(), "Android");
|
||||
|
||||
webView.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
webView.clearView();
|
||||
webView.measure(100, 100);
|
||||
webView.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setUseWideViewPort(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
QLog.d("QZ","loadurl");
|
||||
|
||||
// Initially set reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
|
||||
|
||||
// WindowManager.LayoutParams takes a lot of parameters to set the
|
||||
@@ -116,17 +138,18 @@ public class FloatingWindowGFG extends Service {
|
||||
// 5) Next parameter is Layout_Format. System chooses a format that supports
|
||||
// translucency by PixelFormat.TRANSLUCENT
|
||||
|
||||
originalHeight = FloatingHandler._height;
|
||||
floatWindowLayoutParam = new WindowManager.LayoutParams(
|
||||
(int) (FloatingHandler._width ),
|
||||
(int) (FloatingHandler._height ),
|
||||
(int) (originalHeight ),
|
||||
LAYOUT_TYPE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
|
||||
// The Gravity of the Floating Window is set.
|
||||
// The Window will appear in the center of the screen
|
||||
floatWindowLayoutParam.gravity = Gravity.CENTER;
|
||||
// Use TOP | LEFT for free positioning without constraints
|
||||
floatWindowLayoutParam.gravity = Gravity.TOP | Gravity.LEFT;
|
||||
|
||||
// X and Y value of the window is set
|
||||
floatWindowLayoutParam.x = 0;
|
||||
@@ -145,48 +168,86 @@ public class FloatingWindowGFG extends Service {
|
||||
// The window can be moved at any position on the screen.
|
||||
floatView.setOnTouchListener(new View.OnTouchListener() {
|
||||
final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;
|
||||
double x;
|
||||
double y;
|
||||
double px;
|
||||
double py;
|
||||
int initialX;
|
||||
int initialY;
|
||||
float initialTouchX;
|
||||
float initialTouchY;
|
||||
boolean isDragging = false;
|
||||
final int TOUCH_THRESHOLD = 10; // Threshold for distinguishing tap vs drag
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
|
||||
QLog.d("QZ","onTouch");
|
||||
QLog.d("QZ","onTouch action: " + event.getAction());
|
||||
|
||||
switch (event.getAction()) {
|
||||
// When the window will be touched,
|
||||
// the x and y position of that position
|
||||
// will be retrieved
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
x = floatWindowLayoutUpdateParam.x;
|
||||
y = floatWindowLayoutUpdateParam.y;
|
||||
|
||||
// returns the original raw X
|
||||
// coordinate of this event
|
||||
px = event.getRawX();
|
||||
|
||||
// returns the original raw Y
|
||||
// coordinate of this event
|
||||
py = event.getRawY();
|
||||
// Store initial positions
|
||||
initialX = floatWindowLayoutUpdateParam.x;
|
||||
initialY = floatWindowLayoutUpdateParam.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
isDragging = false;
|
||||
|
||||
// Enable dragging for 5 seconds
|
||||
enableDraggingTemporarily();
|
||||
break;
|
||||
// When the window will be dragged around,
|
||||
// it will update the x, y of the Window Layout Parameter
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
|
||||
floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
|
||||
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.commit();
|
||||
|
||||
// updated parameter is applied to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
break;
|
||||
// Calculate distance moved
|
||||
float deltaX = event.getRawX() - initialTouchX;
|
||||
float deltaY = event.getRawY() - initialTouchY;
|
||||
|
||||
// Check if we've moved enough to consider this a drag
|
||||
if (!isDragging && (Math.abs(deltaX) > TOUCH_THRESHOLD || Math.abs(deltaY) > TOUCH_THRESHOLD)) {
|
||||
isDragging = true;
|
||||
}
|
||||
return false;
|
||||
|
||||
// Only allow dragging if it's temporarily enabled
|
||||
if (isDragging && isDraggingEnabled) {
|
||||
// Get screen dimensions for boundary checking
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenWidth = displayMetrics.widthPixels;
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
// Calculate new position
|
||||
int newX = initialX + (int) deltaX;
|
||||
int newY = initialY + (int) deltaY;
|
||||
|
||||
// Apply boundary constraints
|
||||
// Keep window within screen bounds
|
||||
int windowWidth = FloatingHandler._width;
|
||||
int windowHeight = FloatingHandler._height;
|
||||
|
||||
if (newX < 0) newX = 0;
|
||||
if (newY < 0) newY = 0;
|
||||
if (newX + windowWidth > screenWidth) newX = screenWidth - windowWidth;
|
||||
if (newY + windowHeight > screenHeight) newY = screenHeight - windowHeight;
|
||||
|
||||
// Update position
|
||||
floatWindowLayoutUpdateParam.x = newX;
|
||||
floatWindowLayoutUpdateParam.y = newY;
|
||||
|
||||
// Save position to preferences
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.apply(); // Use apply() instead of commit() for better performance
|
||||
|
||||
// Apply updated parameter to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
// If it wasn't a drag, it's a tap - let the WebView handle it
|
||||
if (!isDragging) {
|
||||
return false; // Let the event propagate to WebView
|
||||
}
|
||||
isDragging = false;
|
||||
break;
|
||||
}
|
||||
return isDragging && isDraggingEnabled; // Consume the event only if we're dragging and dragging is enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -200,4 +261,107 @@ public class FloatingWindowGFG extends Service {
|
||||
// Window is removed from the screen
|
||||
windowManager.removeView(floatView);
|
||||
}
|
||||
|
||||
// Method to enable dragging temporarily for 5 seconds
|
||||
private void enableDraggingTemporarily() {
|
||||
isDraggingEnabled = true;
|
||||
|
||||
// Increase margin for better dragging experience
|
||||
setWebViewMargin(originalMargin);
|
||||
|
||||
// Cancel any existing timeout
|
||||
if (paddingTimeoutRunnable != null) {
|
||||
handler.removeCallbacks(paddingTimeoutRunnable);
|
||||
}
|
||||
|
||||
// Create new timeout runnable
|
||||
paddingTimeoutRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
isDraggingEnabled = false;
|
||||
// Restore reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
QLog.d("QZ", "Dragging disabled after timeout, margin restored");
|
||||
}
|
||||
};
|
||||
|
||||
// Schedule timeout for 5 seconds
|
||||
handler.postDelayed(paddingTimeoutRunnable, 5000);
|
||||
}
|
||||
|
||||
// Method to expand window height dynamically
|
||||
private void expandWindow(int additionalHeight) {
|
||||
if (!isExpanded) {
|
||||
isExpanded = true;
|
||||
floatWindowLayoutParam.height = originalHeight + additionalHeight;
|
||||
|
||||
// Adjust Y position to keep window within screen bounds
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
if (floatWindowLayoutParam.y + floatWindowLayoutParam.height > screenHeight) {
|
||||
floatWindowLayoutParam.y = screenHeight - floatWindowLayoutParam.height;
|
||||
if (floatWindowLayoutParam.y < 0) {
|
||||
floatWindowLayoutParam.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window expanded to height: " + floatWindowLayoutParam.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to restore original window height
|
||||
private void restoreWindow() {
|
||||
if (isExpanded) {
|
||||
isExpanded = false;
|
||||
floatWindowLayoutParam.height = originalHeight;
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window restored to original height: " + originalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to set WebView margin dynamically
|
||||
private void setWebViewMargin(int marginDp) {
|
||||
if (webView != null) {
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) webView.getLayoutParams();
|
||||
int marginPx = (int) (marginDp * getResources().getDisplayMetrics().density);
|
||||
params.setMargins(marginPx, marginPx, marginPx, marginPx);
|
||||
webView.setLayoutParams(params);
|
||||
QLog.d("QZ", "WebView margin set to: " + marginDp + "dp (" + marginPx + "px)");
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript interface class
|
||||
public class WebAppInterface {
|
||||
@JavascriptInterface
|
||||
public void expandFloatingWindow(int additionalHeight) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
expandWindow(additionalHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void restoreFloatingWindow() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
restoreWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void enableDraggingMargins() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
enableDraggingTemporarily();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,14 @@ public class HeartChannelController {
|
||||
private boolean isConnected = false;
|
||||
public int heart = 0; // Public to be accessible from ChannelService
|
||||
|
||||
public HeartChannelController() {
|
||||
public HeartChannelController(int antHeartDeviceNumber) {
|
||||
this.context = Ant.activity;
|
||||
openChannel();
|
||||
openChannel(antHeartDeviceNumber);
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
// Request access to first available heart rate device
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, 0, 0, // 0 means first available device
|
||||
public boolean openChannel(int deviceNumber) {
|
||||
// Request access to heart rate device (deviceNumber = 0 means first available)
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
new IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
@@ -57,7 +57,7 @@ public class HeartChannelController {
|
||||
case SUCCESS:
|
||||
hrPcc = result;
|
||||
isConnected = true;
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
subscribeToHrEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
|
||||
@@ -21,77 +21,133 @@ public class QLog {
|
||||
|
||||
// Debug level methods
|
||||
public static int d(String tag, String msg) {
|
||||
sendToQt(3, tag, msg);
|
||||
try {
|
||||
sendToQt(3, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg);
|
||||
}
|
||||
|
||||
public static int d(String tag, String msg, Throwable tr) {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Error level methods
|
||||
public static int e(String tag, String msg) {
|
||||
sendToQt(6, tag, msg);
|
||||
try {
|
||||
sendToQt(6, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg);
|
||||
}
|
||||
|
||||
public static int e(String tag, String msg, Throwable tr) {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Info level methods
|
||||
public static int i(String tag, String msg) {
|
||||
sendToQt(4, tag, msg);
|
||||
try {
|
||||
sendToQt(4, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg);
|
||||
}
|
||||
|
||||
public static int i(String tag, String msg, Throwable tr) {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Verbose level methods
|
||||
public static int v(String tag, String msg) {
|
||||
sendToQt(2, tag, msg);
|
||||
try {
|
||||
sendToQt(2, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg);
|
||||
}
|
||||
|
||||
public static int v(String tag, String msg, Throwable tr) {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Warning level methods
|
||||
public static int w(String tag, String msg) {
|
||||
sendToQt(5, tag, msg);
|
||||
try {
|
||||
sendToQt(5, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg);
|
||||
}
|
||||
|
||||
public static int w(String tag, String msg, Throwable tr) {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg, tr);
|
||||
}
|
||||
|
||||
public static int w(String tag, Throwable tr) {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, tr);
|
||||
}
|
||||
|
||||
// What a Terrible Failure: Report an exception that should never happen
|
||||
public static int wtf(String tag, String msg) {
|
||||
sendToQt(7, tag, "WTF: " + msg);
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, tr);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, String msg, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg, tr);
|
||||
}
|
||||
|
||||
@@ -106,7 +162,11 @@ public class QLog {
|
||||
|
||||
// Additional utility methods
|
||||
public static int println(int priority, String tag, String msg) {
|
||||
sendToQt(priority, tag, msg);
|
||||
try {
|
||||
sendToQt(priority, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.println(priority, tag, msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
private static final String LOG_TAG = "QZ:AdbRemote";
|
||||
private static String lastCommand = "";
|
||||
private static boolean ADBConnected = false;
|
||||
private static boolean cryptoReady = false;
|
||||
private static final Object cryptoLock = new Object();
|
||||
|
||||
private static String _address = "127.0.0.1";
|
||||
private static Context _context;
|
||||
@@ -62,31 +64,46 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
|
||||
@Override
|
||||
public void notifyConnectionEstablished(DeviceConnection devConn) {
|
||||
QLog.d(LOG_TAG, "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = true;
|
||||
QLog.i(LOG_TAG, "notifyConnectionEstablished" + lastCommand);
|
||||
QLog.i(LOG_TAG, "notifyConnectionEstablished - CONNECTED=true, lastCommand=" + lastCommand);
|
||||
QLog.d(LOG_TAG, "notifyConnectionEstablished - END: ADBConnected=" + ADBConnected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d(LOG_TAG, "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = false;
|
||||
QLog.e(LOG_TAG, e.getMessage());
|
||||
QLog.e(LOG_TAG, "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
|
||||
if (e != null) {
|
||||
QLog.e(LOG_TAG, "notifyConnectionFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d(LOG_TAG, "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = false;
|
||||
QLog.e(LOG_TAG, e.getMessage());
|
||||
QLog.e(LOG_TAG, "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
|
||||
if (e != null) {
|
||||
QLog.e(LOG_TAG, "notifyStreamFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamClosed(DeviceConnection devConn) {
|
||||
QLog.d(LOG_TAG, "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = false;
|
||||
QLog.e(LOG_TAG, "notifyStreamClosed");
|
||||
QLog.e(LOG_TAG, "notifyStreamClosed - ADBConnected=" + ADBConnected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdbCrypto loadAdbCrypto(DeviceConnection devConn) {
|
||||
return AdbUtils.readCryptoConfig(_context.getFilesDir());
|
||||
QLog.d(LOG_TAG, "loadAdbCrypto - START: devConn=" + devConn + ", context=" + _context);
|
||||
|
||||
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
|
||||
QLog.d(LOG_TAG, "loadAdbCrypto - RESULT: crypto=" + (crypto != null ? "valid" : "null"));
|
||||
return crypto;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,92 +128,128 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
|
||||
|
||||
private DeviceConnection startConnection(String host, int port) {
|
||||
QLog.d(LOG_TAG, "startConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
|
||||
/* Create the connection object */
|
||||
DeviceConnection conn = binder.createConnection(host, port);
|
||||
QLog.d(LOG_TAG, "startConnection - CONNECTION_CREATED: conn=" + conn);
|
||||
|
||||
/* Add this activity as a connection listener */
|
||||
binder.addListener(conn, this);
|
||||
QLog.d(LOG_TAG, "startConnection - LISTENER_ADDED: this=" + this);
|
||||
|
||||
/* Begin the async connection process */
|
||||
QLog.d(LOG_TAG, "startConnection - STARTING_CONNECT: about to call conn.startConnect()");
|
||||
conn.startConnect();
|
||||
QLog.d(LOG_TAG, "startConnection - END: startConnect() called, returning conn=" + conn);
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
private DeviceConnection connectOrLookupConnection(String host, int port) {
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
|
||||
DeviceConnection conn = binder.findConnection(host, port);
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - EXISTING_CONN: conn=" + (conn != null ? "found" : "null"));
|
||||
if (conn == null) {
|
||||
/* No existing connection, so start the connection process */
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - NEW_CONNECTION: starting new connection");
|
||||
conn = startConnection(host, port);
|
||||
}
|
||||
else {
|
||||
/* Add ourselves as a new listener of this connection */
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - REUSE_CONNECTION: adding listener to existing connection");
|
||||
binder.addListener(conn, this);
|
||||
}
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - END: returning conn=" + conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
public ServiceConnection serviceConn = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName arg0, IBinder arg1) {
|
||||
QLog.d(LOG_TAG, "onServiceConnected - START: componentName=" + arg0 + ", binder=" + arg1 + ", _address=" + _address);
|
||||
binder = (ShellService.ShellServiceBinder)arg1;
|
||||
QLog.d(LOG_TAG, "onServiceConnected - BINDER_SET: binder=" + binder + ", existing_connection=" + connection);
|
||||
if (connection != null) {
|
||||
QLog.d(LOG_TAG, "onServiceConnected - REMOVING_OLD_LISTENER: connection=" + connection);
|
||||
binder.removeListener(connection, QZAdbRemote.getInstance());
|
||||
}
|
||||
QLog.d(LOG_TAG, "onServiceConnected - CONNECTING: about to call connectOrLookupConnection");
|
||||
connection = connectOrLookupConnection(_address, 5555);
|
||||
QLog.d(LOG_TAG, "onServiceConnected - END: connection=" + connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0) {
|
||||
QLog.d(LOG_TAG, "onServiceDisconnected - START: componentName=" + arg0 + ", old_binder=" + binder);
|
||||
binder = null;
|
||||
QLog.d(LOG_TAG, "onServiceDisconnected - END: binder set to null");
|
||||
}
|
||||
};
|
||||
|
||||
static public void createConnection(String ip, Context context) {
|
||||
QLog.d(LOG_TAG, "createConnection - START: ip=" + ip + ", context=" + context + ", existing_binder=" + binder);
|
||||
_address = ip;
|
||||
_context = context;
|
||||
QLog.d(LOG_TAG, "createConnection - PARAMS_SET: _address=" + _address + ", _context=" + _context);
|
||||
|
||||
/* If we have old RSA keys, just use them */
|
||||
QLog.d(LOG_TAG, "createConnection - CHECKING_CRYPTO: reading existing crypto config");
|
||||
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
|
||||
QLog.d(LOG_TAG, "createConnection - CRYPTO_CHECK: crypto=" + (crypto != null ? "exists" : "null"));
|
||||
if (crypto == null)
|
||||
{
|
||||
/* We need to make a new pair */
|
||||
QLog.i(LOG_TAG,
|
||||
"This will only be done once.");
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AdbCrypto crypto;
|
||||
|
||||
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
|
||||
|
||||
if (crypto == null)
|
||||
{
|
||||
QLog.e(LOG_TAG,
|
||||
"Unable to generate and save RSA key pair");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}).start();
|
||||
QLog.d(LOG_TAG, "createConnection - GENERATING_CRYPTO: synchronously generating crypto keys");
|
||||
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
|
||||
|
||||
if (crypto == null) {
|
||||
QLog.e(LOG_TAG, "Unable to generate and save RSA key pair");
|
||||
cryptoReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
QLog.d(LOG_TAG, "createConnection - CRYPTO_GENERATED: crypto keys generated successfully");
|
||||
synchronized (cryptoLock) {
|
||||
cryptoReady = true;
|
||||
}
|
||||
} else {
|
||||
QLog.d(LOG_TAG, "createConnection - CRYPTO_EXISTS: marking crypto as ready");
|
||||
synchronized (cryptoLock) {
|
||||
cryptoReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_CHECK: binder=" + (binder != null ? "exists" : "null"));
|
||||
if (binder == null) {
|
||||
QLog.i(LOG_TAG, "createConnection - STARTING_SERVICE: Starting ShellService.class");
|
||||
|
||||
service = new Intent(_context, ShellService.class);
|
||||
service.putExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_INTENT: service=" + service);
|
||||
|
||||
/* Bind the service if we're not bound already. After binding, the callback will
|
||||
* perform the initial connection. */
|
||||
QLog.d(LOG_TAG, "createConnection - BINDING_SERVICE: about to bind service");
|
||||
_context.bindService(service, QZAdbRemote.getInstance().serviceConn, Service.BIND_AUTO_CREATE);
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_BOUND: bindService called");
|
||||
|
||||
QLog.d(LOG_TAG, "createConnection - STARTING_SERVICE: SDK_INT=" + Build.VERSION.SDK_INT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
QLog.d(LOG_TAG, "createConnection - FOREGROUND_SERVICE: starting foreground service");
|
||||
_context.startForegroundService(service);
|
||||
}
|
||||
else {
|
||||
QLog.d(LOG_TAG, "createConnection - REGULAR_SERVICE: starting regular service");
|
||||
_context.startService(service);
|
||||
}
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_STARTED: service start completed");
|
||||
} else {
|
||||
QLog.d(LOG_TAG, "createConnection - SKIP_SERVICE: binder already exists, skipping service creation");
|
||||
}
|
||||
QLog.d(LOG_TAG, "createConnection - END: method completed");
|
||||
}
|
||||
|
||||
static public void sendCommand(String command) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
/**
|
||||
* This class represents an ADB connection.
|
||||
@@ -124,10 +125,13 @@ public class AdbConnection implements Closeable {
|
||||
try {
|
||||
/* Read and parse a message off the socket's input stream */
|
||||
AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream);
|
||||
QLog.d("AdbConnection", "connectionThread - Received packet: command=0x" + Integer.toHexString(msg.command) + ", arg0=" + msg.arg0 + ", arg1=" + msg.arg1);
|
||||
|
||||
/* Verify magic and checksum */
|
||||
if (!AdbProtocol.validateMessage(msg))
|
||||
if (!AdbProtocol.validateMessage(msg)) {
|
||||
QLog.w("AdbConnection", "connectionThread - Invalid message, dropping packet");
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (msg.command)
|
||||
{
|
||||
@@ -175,21 +179,25 @@ public class AdbConnection implements Closeable {
|
||||
break;
|
||||
|
||||
case AdbProtocol.CMD_AUTH:
|
||||
QLog.d("AdbConnection", "connectionThread - Received AUTH packet, type=" + msg.arg0);
|
||||
|
||||
byte[] packet;
|
||||
|
||||
if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN)
|
||||
{
|
||||
/* This is an authentication challenge */
|
||||
QLog.d("AdbConnection", "connectionThread - AUTH_TYPE_TOKEN challenge, sentSignature=" + conn.sentSignature);
|
||||
if (conn.sentSignature)
|
||||
{
|
||||
/* We've already tried our signature, so send our public key */
|
||||
QLog.d("AdbConnection", "connectionThread - Sending RSA public key");
|
||||
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC,
|
||||
conn.crypto.getAdbPublicKeyPayload());
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We'll sign the token */
|
||||
QLog.d("AdbConnection", "connectionThread - Signing token with private key");
|
||||
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE,
|
||||
conn.crypto.signAdbTokenPayload(msg.payload));
|
||||
conn.sentSignature = true;
|
||||
@@ -198,16 +206,22 @@ public class AdbConnection implements Closeable {
|
||||
/* Write the AUTH reply */
|
||||
conn.outputStream.write(packet);
|
||||
conn.outputStream.flush();
|
||||
QLog.d("AdbConnection", "connectionThread - AUTH response sent");
|
||||
}
|
||||
else {
|
||||
QLog.w("AdbConnection", "connectionThread - Unhandled AUTH type: " + msg.arg0);
|
||||
}
|
||||
break;
|
||||
|
||||
case AdbProtocol.CMD_CNXN:
|
||||
QLog.d("AdbConnection", "connectionThread - Received CNXN packet! maxData=" + msg.arg1);
|
||||
synchronized (conn) {
|
||||
/* We need to store the max data size */
|
||||
conn.maxData = msg.arg1;
|
||||
|
||||
/* Mark us as connected and unwait anyone waiting on the connection */
|
||||
conn.connected = true;
|
||||
QLog.d("AdbConnection", "connectionThread - Connection established! Notifying waiting threads");
|
||||
conn.notifyAll();
|
||||
}
|
||||
break;
|
||||
@@ -219,6 +233,7 @@ public class AdbConnection implements Closeable {
|
||||
} catch (Exception e) {
|
||||
/* The cleanup is taken care of by a combination of this thread
|
||||
* and close() */
|
||||
QLog.e("AdbConnection", "connectionThread - Exception in connection thread: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -270,23 +285,32 @@ public class AdbConnection implements Closeable {
|
||||
if (connected)
|
||||
throw new IllegalStateException("Already connected");
|
||||
|
||||
QLog.d("AdbConnection", "connect() - Starting ADB connection");
|
||||
|
||||
/* Write the CONNECT packet */
|
||||
outputStream.write(AdbProtocol.generateConnect());
|
||||
outputStream.flush();
|
||||
QLog.d("AdbConnection", "connect() - CONNECT packet sent, starting connection thread");
|
||||
|
||||
/* Start the connection thread to respond to the peer */
|
||||
connectAttempted = true;
|
||||
connectionThread.start();
|
||||
QLog.d("AdbConnection", "connect() - Connection thread started, waiting for connection...");
|
||||
|
||||
/* Wait for the connection to go live */
|
||||
synchronized (this) {
|
||||
if (!connected)
|
||||
if (!connected) {
|
||||
QLog.d("AdbConnection", "connect() - Waiting for connection to complete...");
|
||||
wait();
|
||||
QLog.d("AdbConnection", "connect() - Wait completed, connected=" + connected);
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
QLog.e("AdbConnection", "connect() - Connection failed after wait");
|
||||
throw new IOException("Connection failed");
|
||||
}
|
||||
}
|
||||
QLog.d("AdbConnection", "connect() - Successfully connected!");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.cgutman.adblib.AdbConnection;
|
||||
import com.cgutman.adblib.AdbCrypto;
|
||||
import com.cgutman.adblib.AdbStream;
|
||||
import com.cgutman.androidremotedebugger.AdbUtils;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class DeviceConnection implements Closeable {
|
||||
private static final int CONN_TIMEOUT = 5000;
|
||||
@@ -59,42 +60,58 @@ public class DeviceConnection implements Closeable {
|
||||
}
|
||||
|
||||
public void startConnect() {
|
||||
QLog.d("DeviceConnection", "startConnect - START: host=" + host + ", port=" + port + ", listener=" + listener);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d("DeviceConnection", "startConnect.run - THREAD_START: host=" + host + ", port=" + port);
|
||||
boolean connected = false;
|
||||
Socket socket = new Socket();
|
||||
AdbCrypto crypto;
|
||||
|
||||
/* Load the crypto config */
|
||||
QLog.d("DeviceConnection", "startConnect.run - LOADING_CRYPTO: calling loadAdbCrypto");
|
||||
crypto = listener.loadAdbCrypto(DeviceConnection.this);
|
||||
if (crypto == null) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - CRYPTO_FAILED: crypto is null, returning");
|
||||
return;
|
||||
}
|
||||
QLog.d("DeviceConnection", "startConnect.run - CRYPTO_LOADED: crypto=" + crypto);
|
||||
|
||||
try {
|
||||
/* Establish a connect to the remote host */
|
||||
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECT: connecting to " + host + ":" + port + " with timeout=" + CONN_TIMEOUT);
|
||||
socket.connect(new InetSocketAddress(host, port), CONN_TIMEOUT);
|
||||
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECTED: socket connected successfully");
|
||||
} catch (IOException e) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - SOCKET_FAILED: connection failed", e);
|
||||
listener.notifyConnectionFailed(DeviceConnection.this, e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
/* Establish the application layer connection */
|
||||
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTION: creating AdbConnection");
|
||||
connection = AdbConnection.create(socket, crypto);
|
||||
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECT: calling connection.connect()");
|
||||
connection.connect();
|
||||
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTED: ADB connection established");
|
||||
|
||||
/* Open the shell stream */
|
||||
QLog.d("DeviceConnection", "startConnect.run - SHELL_STREAM: opening shell stream");
|
||||
shellStream = connection.open("shell:");
|
||||
QLog.d("DeviceConnection", "startConnect.run - SHELL_OPENED: shell stream opened successfully");
|
||||
connected = true;
|
||||
} catch (IOException e) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - ADB_IO_ERROR: IOException during ADB connection", e);
|
||||
listener.notifyConnectionFailed(DeviceConnection.this, e);
|
||||
} catch (InterruptedException e) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - ADB_INTERRUPTED: InterruptedException during ADB connection", e);
|
||||
listener.notifyConnectionFailed(DeviceConnection.this, e);
|
||||
} finally {
|
||||
/* Cleanup if the connection failed */
|
||||
if (!connected) {
|
||||
QLog.d("DeviceConnection", "startConnect.run - CLEANUP: connection failed, cleaning up");
|
||||
AdbUtils.safeClose(shellStream);
|
||||
|
||||
/* The AdbConnection object will close the underlying socket
|
||||
@@ -112,12 +129,16 @@ public class DeviceConnection implements Closeable {
|
||||
}
|
||||
|
||||
/* Notify the listener that the connection is complete */
|
||||
QLog.d("DeviceConnection", "startConnect.run - NOTIFY_SUCCESS: calling listener.notifyConnectionEstablished");
|
||||
listener.notifyConnectionEstablished(DeviceConnection.this);
|
||||
QLog.d("DeviceConnection", "startConnect.run - NOTIFIED: notifyConnectionEstablished called");
|
||||
|
||||
/* Start the receive thread */
|
||||
QLog.d("DeviceConnection", "startConnect.run - START_RECEIVE: starting receive thread");
|
||||
startReceiveThread();
|
||||
|
||||
/* Enter the blocking send loop */
|
||||
QLog.d("DeviceConnection", "startConnect.run - SEND_LOOP: entering send loop");
|
||||
sendLoop();
|
||||
}
|
||||
}).start();
|
||||
@@ -148,23 +169,32 @@ public class DeviceConnection implements Closeable {
|
||||
}
|
||||
|
||||
private void startReceiveThread() {
|
||||
QLog.d("DeviceConnection", "startReceiveThread - START: creating receive thread");
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - THREAD_START: receive thread started");
|
||||
try {
|
||||
while (!shellStream.isClosed()) {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - READING: waiting for data from shellStream");
|
||||
byte[] data = shellStream.read();
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - DATA_RECEIVED: " + data.length + " bytes received");
|
||||
listener.receivedData(DeviceConnection.this, data, 0, data.length);
|
||||
}
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - STREAM_CLOSED: shellStream is closed");
|
||||
listener.notifyStreamClosed(DeviceConnection.this);
|
||||
} catch (IOException e) {
|
||||
QLog.e("DeviceConnection", "startReceiveThread.run - IO_ERROR: IOException in receive thread", e);
|
||||
listener.notifyStreamFailed(DeviceConnection.this, e);
|
||||
} catch (InterruptedException e) {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - INTERRUPTED: receive thread interrupted");
|
||||
} finally {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - CLEANUP: cleaning up receive thread");
|
||||
AdbUtils.safeClose(DeviceConnection.this);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
QLog.d("DeviceConnection", "startReceiveThread - END: receive thread started");
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
|
||||
@@ -48,14 +48,20 @@ public class ShellService extends Service implements DeviceConnectionListener {
|
||||
|
||||
public class ShellServiceBinder extends Binder {
|
||||
public DeviceConnection createConnection(String host, int port) {
|
||||
QLog.d("ShellService", "createConnection - START: host=" + host + ", port=" + port + ", listener=" + listener);
|
||||
DeviceConnection conn = new DeviceConnection(listener, host, port);
|
||||
QLog.d("ShellService", "createConnection - CONNECTION_CREATED: conn=" + conn);
|
||||
listener.addListener(conn, ShellService.this);
|
||||
QLog.d("ShellService", "createConnection - LISTENER_ADDED: returning conn=" + conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
public DeviceConnection findConnection(String host, int port) {
|
||||
String connStr = host+":"+port;
|
||||
return currentConnectionMap.get(connStr);
|
||||
QLog.d("ShellService", "findConnection - SEARCH: connStr=" + connStr + ", mapSize=" + currentConnectionMap.size());
|
||||
DeviceConnection found = currentConnectionMap.get(connStr);
|
||||
QLog.d("ShellService", "findConnection - RESULT: found=" + (found != null ? "exists" : "null"));
|
||||
return found;
|
||||
}
|
||||
|
||||
public void notifyPausingActivity(DeviceConnection devConn) {
|
||||
@@ -76,68 +82,95 @@ public class ShellService extends Service implements DeviceConnectionListener {
|
||||
}
|
||||
|
||||
public void addListener(DeviceConnection conn, DeviceConnectionListener listener) {
|
||||
QLog.d("ShellService", "addListener - START: conn=" + conn + ", listener=" + listener);
|
||||
ShellService.this.listener.addListener(conn, listener);
|
||||
QLog.d("ShellService", "addListener - END: listener added");
|
||||
}
|
||||
|
||||
public void removeListener(DeviceConnection conn, DeviceConnectionListener listener) {
|
||||
QLog.d("ShellService", "removeListener - START: conn=" + conn + ", listener=" + listener);
|
||||
ShellService.this.listener.removeListener(conn, listener);
|
||||
QLog.d("ShellService", "removeListener - END: listener removed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent arg0) {
|
||||
QLog.d("ShellService", "onBind - START: intent=" + arg0 + ", binder=" + binder);
|
||||
QLog.d("ShellService", "onBind - END: returning binder");
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
QLog.d("ShellService", "onUnbind - START: intent=" + intent + ", connections=" + currentConnectionMap.size());
|
||||
/* Stop the service if no connections remain */
|
||||
if (currentConnectionMap.isEmpty()) {
|
||||
QLog.d("ShellService", "onUnbind - STOPPING_SERVICE: no connections remain");
|
||||
stopSelf();
|
||||
} else {
|
||||
QLog.d("ShellService", "onUnbind - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
|
||||
}
|
||||
|
||||
QLog.d("ShellService", "onUnbind - END: returning false");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
QLog.d("ShellService", "onStartCommand - START: intent=" + intent + ", flags=" + flags + ", startId=" + startId + ", foregroundId=" + foregroundId);
|
||||
if (foregroundId == 0) {
|
||||
try {
|
||||
int serviceType = intent.getIntExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
|
||||
// If we're not already running in the foreground, use a placeholder
|
||||
// notification until a real connection is established. After connection
|
||||
// establishment, the real notification will replace this one.
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_START: serviceType=" + serviceType + ", SDK_INT=" + Build.VERSION.SDK_INT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_Q+: starting with service type");
|
||||
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification(), serviceType);
|
||||
} else {
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_LEGACY: starting without service type");
|
||||
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification());
|
||||
}
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_SUCCESS: foreground service started");
|
||||
} catch (Exception e) {
|
||||
QLog.e("ForegroundService", "Failed to start foreground service", e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
} else {
|
||||
QLog.d("ShellService", "onStartCommand - SKIP_FOREGROUND: already running in foreground with id=" + foregroundId);
|
||||
}
|
||||
|
||||
// Don't restart if we've been killed. We will have already lost our connections
|
||||
// when we died, so we'll just be running doing nothing if the OS restarted us.
|
||||
QLog.d("ShellService", "onStartCommand - END: returning START_NOT_STICKY");
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
QLog.d("ShellService", "onCreate - START: initializing service");
|
||||
super.onCreate();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: creating notification channel");
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Connection Info", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: channel created");
|
||||
}
|
||||
|
||||
QLog.d("ShellService", "onCreate - WIFI_LOCK: creating wifi lock");
|
||||
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
wlanLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL, "RemoteADBShell:ShellService");
|
||||
QLog.d("ShellService", "onCreate - WIFI_LOCK: wlanLock=" + wlanLock);
|
||||
|
||||
QLog.d("ShellService", "onCreate - WAKE_LOCK: creating wake lock");
|
||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "RemoteADBShell:ShellService");
|
||||
QLog.d("ShellService", "onCreate - WAKE_LOCK: wakeLock=" + wakeLock);
|
||||
QLog.d("ShellService", "onCreate - END: service initialization complete");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -248,44 +281,76 @@ public class ShellService extends Service implements DeviceConnectionListener {
|
||||
}
|
||||
|
||||
private synchronized void addNewConnection(DeviceConnection devConn) {
|
||||
QLog.d("ShellService", "addNewConnection - START: devConn=" + devConn + ", currentSize=" + currentConnectionMap.size());
|
||||
if (currentConnectionMap.isEmpty()) {
|
||||
QLog.d("ShellService", "addNewConnection - ACQUIRING_LOCKS: first connection, acquiring locks");
|
||||
wakeLock.acquire();
|
||||
wlanLock.acquire();
|
||||
QLog.d("ShellService", "addNewConnection - LOCKS_ACQUIRED: wakeLock and wlanLock acquired");
|
||||
}
|
||||
|
||||
currentConnectionMap.put(getConnectionString(devConn), devConn);
|
||||
String connString = getConnectionString(devConn);
|
||||
QLog.d("ShellService", "addNewConnection - ADDING: connString=" + connString);
|
||||
currentConnectionMap.put(connString, devConn);
|
||||
QLog.d("ShellService", "addNewConnection - END: connection added, newSize=" + currentConnectionMap.size());
|
||||
}
|
||||
|
||||
private synchronized void removeConnection(DeviceConnection devConn) {
|
||||
currentConnectionMap.remove(getConnectionString(devConn));
|
||||
String connString = getConnectionString(devConn);
|
||||
QLog.d("ShellService", "removeConnection - START: devConn=" + devConn + ", connString=" + connString + ", currentSize=" + currentConnectionMap.size());
|
||||
currentConnectionMap.remove(connString);
|
||||
QLog.d("ShellService", "removeConnection - REMOVED: newSize=" + currentConnectionMap.size());
|
||||
|
||||
/* Stop the service if no connections remain */
|
||||
if (currentConnectionMap.isEmpty()) {
|
||||
QLog.d("ShellService", "removeConnection - STOPPING_SERVICE: no connections remain");
|
||||
stopSelf();
|
||||
} else {
|
||||
QLog.d("ShellService", "removeConnection - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
|
||||
}
|
||||
QLog.d("ShellService", "removeConnection - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyConnectionEstablished(DeviceConnection devConn) {
|
||||
QLog.d("ShellService", "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
addNewConnection(devConn);
|
||||
QLog.d("ShellService", "notifyConnectionEstablished - CONNECTION_ADDED: updating notification");
|
||||
updateNotification(devConn, true);
|
||||
QLog.d("ShellService", "notifyConnectionEstablished - END: connection established successfully");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d("ShellService", "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
QLog.e("ShellService", "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
|
||||
if (e != null) {
|
||||
QLog.e("ShellService", "notifyConnectionFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
/* No notification is displaying here */
|
||||
QLog.d("ShellService", "notifyConnectionFailed - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d("ShellService", "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
QLog.e("ShellService", "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
|
||||
if (e != null) {
|
||||
QLog.e("ShellService", "notifyStreamFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
updateNotification(devConn, false);
|
||||
QLog.d("ShellService", "notifyStreamFailed - NOTIFICATION_UPDATED: removing connection");
|
||||
removeConnection(devConn);
|
||||
QLog.d("ShellService", "notifyStreamFailed - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamClosed(DeviceConnection devConn) {
|
||||
QLog.d("ShellService", "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
updateNotification(devConn, false);
|
||||
QLog.d("ShellService", "notifyStreamClosed - NOTIFICATION_UPDATED: removing connection");
|
||||
removeConnection(devConn);
|
||||
QLog.d("ShellService", "notifyStreamClosed - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -65,13 +65,16 @@ import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.ConsumeParams;
|
||||
import com.android.billingclient.api.ConsumeResponseListener;
|
||||
import com.android.billingclient.api.PendingPurchasesParams;
|
||||
import com.android.billingclient.api.ProductDetails;
|
||||
import com.android.billingclient.api.ProductDetailsResponseListener;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.Purchase.PurchaseState;
|
||||
import com.android.billingclient.api.PurchasesResponseListener;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
import com.android.billingclient.api.QueryProductDetailsParams;
|
||||
import com.android.billingclient.api.QueryPurchasesParams;
|
||||
import com.android.billingclient.api.QueryProductDetailsResult;
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
@@ -79,7 +82,7 @@ import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
** Add Dependencies below to build.gradle file:
|
||||
|
||||
dependencies {
|
||||
def billing_version = "4.0.0"
|
||||
def billing_version = "8.0.0"
|
||||
implementation "com.android.billingclient:billing:$billing_version"
|
||||
}
|
||||
|
||||
@@ -97,8 +100,8 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
|
||||
public static final int RESULT_OK = BillingClient.BillingResponseCode.OK;
|
||||
public static final int RESULT_USER_CANCELED = BillingClient.BillingResponseCode.USER_CANCELED;
|
||||
public static final String TYPE_INAPP = BillingClient.SkuType.INAPP;
|
||||
public static final String TYPE_SUBS = BillingClient.SkuType.SUBS;
|
||||
public static final String TYPE_INAPP = BillingClient.ProductType.INAPP;
|
||||
public static final String TYPE_SUBS = BillingClient.ProductType.SUBS;
|
||||
public static final String TAG = "InAppPurchase";
|
||||
|
||||
// Should be in sync with InAppTransaction::FailureReason
|
||||
@@ -120,8 +123,11 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
|
||||
public void initializeConnection(){
|
||||
QLog.w(TAG, "initializeConnection start");
|
||||
PendingPurchasesParams pendingPurchasesParams = PendingPurchasesParams.newBuilder()
|
||||
.enableOneTimeProducts()
|
||||
.build();
|
||||
billingClient = BillingClient.newBuilder(m_context)
|
||||
.enablePendingPurchases()
|
||||
.enablePendingPurchases(pendingPurchasesParams)
|
||||
.setListener(this)
|
||||
.build();
|
||||
billingClient.startConnection(new BillingClientStateListener() {
|
||||
@@ -146,18 +152,23 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
|
||||
|
||||
int responseCode = billingResult.getResponseCode();
|
||||
QLog.d(TAG, "onPurchasesUpdated called. Response code: " + responseCode + ", Debug message: " + billingResult.getDebugMessage());
|
||||
|
||||
if (purchases == null) {
|
||||
QLog.e(TAG, "Purchase failed: Data missing from result (purchases is null)");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
|
||||
return;
|
||||
}
|
||||
|
||||
if (billingResult.getResponseCode() == RESULT_OK) {
|
||||
QLog.d(TAG, "Purchase successful, handling " + purchases.size() + " purchases");
|
||||
handlePurchase(purchases);
|
||||
} else if (responseCode == RESULT_USER_CANCELED) {
|
||||
QLog.d(TAG, "Purchase cancelled by user");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_USERCANCELED, "");
|
||||
} else {
|
||||
String errorString = getErrorString(responseCode);
|
||||
QLog.e(TAG, "Purchase failed with error: " + errorString + " (code: " + responseCode + ")");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, errorString);
|
||||
}
|
||||
}
|
||||
@@ -209,31 +220,44 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
}
|
||||
index += productIdList.size();
|
||||
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
params.setSkusList(productIdList).setType(TYPE_SUBS);
|
||||
billingClient.querySkuDetailsAsync(params.build(),
|
||||
new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
|
||||
for (String productId : productIdList) {
|
||||
productList.add(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(productId)
|
||||
.setProductType(TYPE_SUBS)
|
||||
.build());
|
||||
}
|
||||
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productList)
|
||||
.build();
|
||||
billingClient.queryProductDetailsAsync(params,
|
||||
(billingResult, productDetailsResult) -> {
|
||||
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
|
||||
int responseCode = billingResult.getResponseCode();
|
||||
QLog.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
|
||||
QLog.d(TAG, "onProductDetailsResponse: responseCode " + responseCode);
|
||||
|
||||
if (responseCode != RESULT_OK) {
|
||||
QLog.e(TAG, "queryDetails: Couldn't retrieve sku details.");
|
||||
QLog.e(TAG, "queryDetails: Couldn't retrieve product details.");
|
||||
return;
|
||||
}
|
||||
if (skuDetailsList == null) {
|
||||
if (productDetailsList == null || productDetailsList.isEmpty()) {
|
||||
QLog.e(TAG, "queryDetails: No details list in response.");
|
||||
return;
|
||||
}
|
||||
|
||||
QLog.d(TAG, "onSkuDetailsResponse: skuDetailsList " + skuDetailsList);
|
||||
for (SkuDetails skuDetails : skuDetailsList) {
|
||||
QLog.d(TAG, "onProductDetailsResponse: productDetailsList " + productDetailsList);
|
||||
for (ProductDetails productDetails : productDetailsList) {
|
||||
try {
|
||||
String queriedProductId = skuDetails.getSku();
|
||||
String queriedPrice = skuDetails.getPrice();
|
||||
String queriedTitle = skuDetails.getTitle();
|
||||
String queriedDescription = skuDetails.getDescription();
|
||||
String queriedProductId = productDetails.getProductId();
|
||||
String queriedPrice = "";
|
||||
String queriedTitle = productDetails.getTitle();
|
||||
String queriedDescription = productDetails.getDescription();
|
||||
|
||||
// Get price from subscription offer details
|
||||
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
|
||||
queriedPrice = productDetails.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
|
||||
}
|
||||
registerProduct(m_nativePointer,
|
||||
queriedProductId,
|
||||
queriedPrice,
|
||||
@@ -243,7 +267,6 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -255,33 +278,51 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
public void launchBillingFlow(String identifier, final int requestCode){
|
||||
|
||||
purchaseRequestCode = requestCode;
|
||||
List<String> skuList = new ArrayList<>();
|
||||
skuList.add(identifier);
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
params.setSkusList(skuList).setType(TYPE_SUBS);
|
||||
billingClient.querySkuDetailsAsync(params.build(),
|
||||
new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
|
||||
productList.add(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(identifier)
|
||||
.setProductType(TYPE_SUBS)
|
||||
.build());
|
||||
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productList)
|
||||
.build();
|
||||
billingClient.queryProductDetailsAsync(params,
|
||||
(billingResult, productDetailsResult) -> {
|
||||
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
|
||||
|
||||
if (billingResult.getResponseCode() != RESULT_OK) {
|
||||
QLog.e(TAG, "Unable to launch Google Play purchase screen");
|
||||
QLog.e(TAG, "Unable to launch Google Play purchase screen. Response code: " + billingResult.getResponseCode() + ", Debug message: " + billingResult.getDebugMessage());
|
||||
String errorString = getErrorString(requestCode);
|
||||
purchaseFailed(requestCode, FAILUREREASON_ERROR, errorString);
|
||||
return;
|
||||
}
|
||||
else if (skuDetailsList == null){
|
||||
else if (productDetailsList == null || productDetailsList.isEmpty()){
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
|
||||
return;
|
||||
}
|
||||
|
||||
ProductDetails productDetails = productDetailsList.get(0);
|
||||
BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails);
|
||||
|
||||
// For subscriptions, we need to set the offer token
|
||||
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
|
||||
String offerToken = productDetails.getSubscriptionOfferDetails().get(0).getOfferToken();
|
||||
QLog.d(TAG, "Setting offer token for subscription: " + offerToken);
|
||||
productDetailsParamsBuilder.setOfferToken(offerToken);
|
||||
} else {
|
||||
QLog.w(TAG, "No subscription offer details found for product: " + identifier);
|
||||
}
|
||||
|
||||
BillingFlowParams.ProductDetailsParams productDetailsParams = productDetailsParamsBuilder.build();
|
||||
|
||||
BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(skuDetailsList.get(0))
|
||||
.setProductDetailsParamsList(java.util.Arrays.asList(productDetailsParams))
|
||||
.build();
|
||||
|
||||
//Results will be delivered to onPurchasesUpdated
|
||||
billingClient.launchBillingFlow((Activity) m_context, purchaseParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,18 +362,21 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
|
||||
public void queryPurchasedProducts(final List<String> productIdList) {
|
||||
|
||||
billingClient.queryPurchasesAsync(TYPE_INAPP, new PurchasesResponseListener() {
|
||||
QueryPurchasesParams queryPurchasesParams = QueryPurchasesParams.newBuilder()
|
||||
.setProductType(TYPE_SUBS)
|
||||
.build();
|
||||
billingClient.queryPurchasesAsync(queryPurchasesParams, new PurchasesResponseListener() {
|
||||
@Override
|
||||
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> list) {
|
||||
for (Purchase purchase : list) {
|
||||
|
||||
if (productIdList.contains(purchase.getSkus().get(0))) {
|
||||
if (productIdList.contains(purchase.getProducts().get(0))) {
|
||||
registerPurchased(m_nativePointer,
|
||||
purchase.getSkus().get(0),
|
||||
purchase.getProducts().get(0),
|
||||
purchase.getSignature(),
|
||||
purchase.getOriginalJson(),
|
||||
purchase.getPurchaseToken(),
|
||||
purchase.getDeveloperPayload(),
|
||||
"", // getDeveloperPayload() is deprecated
|
||||
purchase.getPurchaseTime());
|
||||
}
|
||||
}
|
||||
|
||||
64
src/androidstatusbar.cpp
Normal file
64
src/androidstatusbar.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "androidstatusbar.h"
|
||||
#include <QQmlEngine>
|
||||
#include <QDebug>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QtAndroid>
|
||||
#include <QAndroidJniEnvironment>
|
||||
#endif
|
||||
|
||||
AndroidStatusBar* AndroidStatusBar::m_instance = nullptr;
|
||||
|
||||
AndroidStatusBar::AndroidStatusBar(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_instance = this;
|
||||
}
|
||||
|
||||
AndroidStatusBar* AndroidStatusBar::instance()
|
||||
{
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void AndroidStatusBar::registerQmlType()
|
||||
{
|
||||
qmlRegisterSingletonType<AndroidStatusBar>("AndroidStatusBar", 1, 0, "AndroidStatusBar",
|
||||
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {
|
||||
Q_UNUSED(engine)
|
||||
Q_UNUSED(scriptEngine)
|
||||
return new AndroidStatusBar();
|
||||
});
|
||||
}
|
||||
|
||||
int AndroidStatusBar::apiLevel() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
return QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/CustomQtActivity", "getApiLevel", "()I");
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AndroidStatusBar::onInsetsChanged(int top, int bottom, int left, int right)
|
||||
{
|
||||
if (m_top != top || m_bottom != bottom || m_left != left || m_right != right) {
|
||||
m_top = top;
|
||||
m_bottom = bottom;
|
||||
m_left = left;
|
||||
m_right = right;
|
||||
qDebug() << "Insets changed - Top:" << m_top << "Bottom:" << m_bottom << "Left:" << m_left << "Right:" << m_right;
|
||||
emit insetsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
// JNI method with standard naming convention
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_cagnulen_qdomyoszwift_CustomQtActivity_onInsetsChanged(JNIEnv *env, jobject thiz, jint top, jint bottom, jint left, jint right)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
if (AndroidStatusBar::instance()) {
|
||||
AndroidStatusBar::instance()->onInsetsChanged(top, bottom, left, right);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
43
src/androidstatusbar.h
Normal file
43
src/androidstatusbar.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef ANDROIDSTATUSBAR_H
|
||||
#define ANDROIDSTATUSBAR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class AndroidStatusBar : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int height READ height NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int leftInset READ leftInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int rightInset READ rightInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int apiLevel READ apiLevel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit AndroidStatusBar(QObject *parent = nullptr);
|
||||
|
||||
static void registerQmlType();
|
||||
static AndroidStatusBar* instance();
|
||||
|
||||
int height() const { return m_top; }
|
||||
int navigationBarHeight() const { return m_bottom; }
|
||||
int leftInset() const { return m_left; }
|
||||
int rightInset() const { return m_right; }
|
||||
int apiLevel() const;
|
||||
|
||||
public slots:
|
||||
void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
signals:
|
||||
void insetsChanged();
|
||||
|
||||
private:
|
||||
int m_top = 0;
|
||||
int m_bottom = 0;
|
||||
int m_left = 0;
|
||||
int m_right = 0;
|
||||
|
||||
static AndroidStatusBar* m_instance;
|
||||
};
|
||||
|
||||
#endif // ANDROIDSTATUSBAR_H
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "characteristicnotifier2acd.h"
|
||||
#include "devices/treadmill.h"
|
||||
#include <qmath.h>
|
||||
#include <QTime> // Include QTime for Bike->elapsedTime()
|
||||
|
||||
CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QObject *parent)
|
||||
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
|
||||
@@ -9,7 +10,8 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
value.append(0x0C); // Inclination available and distance for peloton
|
||||
value.append((char)0x01); // heart rate available
|
||||
//value.append((char)0x01); // heart rate available
|
||||
value.append((char)0x05); // HeartRate(8) | ElapsedTime(10)
|
||||
|
||||
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
|
||||
char a = (normalizeSpeed >> 8) & 0XFF;
|
||||
@@ -61,6 +63,18 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
rampBytes.append(b);
|
||||
rampBytes.append(a);
|
||||
|
||||
// Get session elapsed time - makes Runna calculations work
|
||||
QTime sessionElapsedTime = Bike->elapsedTime();
|
||||
double elapsed_time_seconds =
|
||||
(double)sessionElapsedTime.hour() * 3600.0 +
|
||||
(double)sessionElapsedTime.minute() * 60.0 +
|
||||
(double)sessionElapsedTime.second() +
|
||||
(double)sessionElapsedTime.msec() / 1000.0;
|
||||
uint16_t ftms_elapsed_time_field = (uint16_t)qRound(elapsed_time_seconds);
|
||||
QByteArray elapsedBytes;
|
||||
elapsedBytes.append(static_cast<char>(ftms_elapsed_time_field & 0xFF));
|
||||
elapsedBytes.append(static_cast<char>((ftms_elapsed_time_field >> 8) & 0xFF));
|
||||
|
||||
value.append(speedBytes); // Actual value.
|
||||
|
||||
value.append(distanceBytes); // Actual value.
|
||||
@@ -70,6 +84,9 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
value.append(rampBytes); // ramp angle
|
||||
|
||||
value.append(Bike->currentHeart().value()); // current heart rate
|
||||
|
||||
value.append(elapsedBytes); // Elapsed Time
|
||||
|
||||
return CN_OK;
|
||||
} else
|
||||
return CN_INVALID;
|
||||
|
||||
@@ -14,7 +14,7 @@ class CharacteristicWriteProcessor : public QObject {
|
||||
public:
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
double bikeResistanceGain = 1.0;
|
||||
bluetoothdevice *Bike;
|
||||
bluetoothdevice *Bike = nullptr;
|
||||
|
||||
explicit CharacteristicWriteProcessor(double bikeResistanceGain, int8_t bikeResistanceOffset,
|
||||
bluetoothdevice *bike, QObject *parent = nullptr);
|
||||
|
||||
@@ -31,7 +31,7 @@ CharacteristicWriteProcessor0003::VarintResult CharacteristicWriteProcessor0003:
|
||||
}
|
||||
|
||||
double CharacteristicWriteProcessor0003::currentGear() {
|
||||
if(zwiftGearReceived)
|
||||
if(zwiftGearReceived || !((bike*)Bike))
|
||||
return currentZwiftGear;
|
||||
else
|
||||
return ((bike*)Bike)->gears();
|
||||
|
||||
@@ -14,7 +14,7 @@ CharacteristicWriteProcessor2AD9::CharacteristicWriteProcessor2AD9(double bikeRe
|
||||
int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
|
||||
if (data.size()) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::BIKE) {
|
||||
if (dt == bluetoothdevice::BIKE || dt == bluetoothdevice::ROWING) {
|
||||
QSettings settings;
|
||||
bool force_resistance =
|
||||
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
|
||||
@@ -63,6 +63,12 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
|
||||
reply.append((quint8)FTMS_RESPONSE_CODE);
|
||||
reply.append((quint8)FTMS_START_RESUME);
|
||||
reply.append((quint8)FTMS_SUCCESS);
|
||||
} else if (cmd == FTMS_STOP_PAUSE) {
|
||||
qDebug() << QStringLiteral("stop/pause simulation! ignoring it");
|
||||
|
||||
reply.append((quint8)FTMS_RESPONSE_CODE);
|
||||
reply.append((quint8)FTMS_STOP_PAUSE);
|
||||
reply.append((quint8)FTMS_SUCCESS);
|
||||
} else if (cmd == FTMS_REQUEST_CONTROL) {
|
||||
qDebug() << QStringLiteral("control requested");
|
||||
|
||||
|
||||
@@ -158,28 +158,7 @@ uint16_t android_antbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t android_antbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
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();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ void antbike::update() {
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -168,28 +168,7 @@ uint16_t antbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t antbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
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();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,12 +59,11 @@ void apexbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin
|
||||
|
||||
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
} else {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
}
|
||||
|
||||
|
||||
if (!disable_log) {
|
||||
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info;
|
||||
@@ -147,42 +146,37 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
lastPacket = newValue;
|
||||
|
||||
if (newValue.length() == 10 && newValue.at(2) == 0x31) {
|
||||
Resistance = newValue.at(5);
|
||||
// Invert resistance: bike resistance 1-32 maps to app display 32-1
|
||||
uint8_t rawResistance = newValue.at(5);
|
||||
Resistance = 33 - rawResistance; // Invert: 1->32, 32->1
|
||||
emit resistanceRead(Resistance.value());
|
||||
m_pelotonResistance = Resistance.value();
|
||||
|
||||
qDebug() << QStringLiteral("Current resistance: ") + QString::number(Resistance.value());
|
||||
// Parse cadence from 5th byte (index 4) and multiply by 2
|
||||
uint8_t rawCadence = newValue.at(4);
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = rawCadence * 2;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value()) + QStringLiteral(", Raw cadence: ") + QString::number(rawCadence) + QStringLiteral(", Final cadence: ") + QString::number(Cadence.value());
|
||||
}
|
||||
|
||||
if (newValue.length() != 10 || newValue.at(2) != 0x30) {
|
||||
if (newValue.length() != 10 || newValue.at(2) != 0x31) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t distanceData = (newValue.at(3) << 8) | ((uint8_t)newValue.at(4));
|
||||
uint16_t distanceData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
|
||||
double distance = ((double)distanceData);
|
||||
|
||||
if(distance != lastDistance) {
|
||||
if(lastDistance != 0) {
|
||||
double deltaDistance = distance - lastDistance;
|
||||
double deltaTime = fabs(now.msecsTo(lastTS));
|
||||
double timeHours = deltaTime / (1000.0 * 60.0 * 60.0);
|
||||
double k = 0.005333;
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = (deltaDistance *k) / timeHours; // Speed in distance units per hour
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = Speed.value() / 0.37497622;
|
||||
}
|
||||
}
|
||||
lastDistance = distance;
|
||||
lastTS = now;
|
||||
qDebug() << "lastDistance" << lastDistance << "lastTS" << lastTS;
|
||||
// Calculate speed using the same method as echelon bike
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = 0.37497622 * ((double)Cadence.value());
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
|
||||
if (watts())
|
||||
@@ -220,7 +214,7 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -417,7 +411,98 @@ bool apexbike::connected() {
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
uint16_t apexbike::watts() { return wattFromHR(true); }
|
||||
uint16_t apexbike::watts() {
|
||||
double resistance = Resistance.value();
|
||||
double cadence = Cadence.value();
|
||||
|
||||
if (cadence <= 0 || resistance <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Power table based on user-provided data
|
||||
// Format: resistance level (1-19), RPM (10-150 in steps of 10), power (watts)
|
||||
static const int powerTable[19][15] = {
|
||||
// Resistance 1: RPM 10,20,30,40,50,60,70,80,90,100,110,120,130,140,150
|
||||
{12, 24, 36, 48, 61, 73, 85, 97, 109, 121, 133, 145, 157, 170, 182},
|
||||
// Resistance 2
|
||||
{13, 27, 40, 53, 67, 80, 93, 107, 120, 133, 147, 160, 173, 187, 200},
|
||||
// Resistance 3
|
||||
{15, 29, 44, 58, 73, 87, 102, 117, 131, 146, 160, 175, 189, 204, 219},
|
||||
// Resistance 4
|
||||
{16, 32, 48, 64, 80, 95, 111, 127, 143, 159, 175, 191, 207, 223, 239},
|
||||
// Resistance 5
|
||||
{17, 34, 51, 68, 85, 102, 118, 135, 152, 169, 186, 203, 220, 237, 254},
|
||||
// Resistance 6
|
||||
{18, 37, 55, 74, 92, 110, 129, 147, 165, 184, 202, 221, 239, 257, 276},
|
||||
// Resistance 7
|
||||
{19, 39, 58, 77, 97, 116, 136, 155, 174, 194, 213, 232, 252, 271, 291},
|
||||
// Resistance 8
|
||||
{21, 42, 62, 83, 104, 125, 146, 166, 187, 208, 229, 250, 271, 291, 312},
|
||||
// Resistance 9
|
||||
{22, 44, 66, 88, 110, 132, 154, 176, 198, 220, 242, 264, 286, 308, 330},
|
||||
// Resistance 10
|
||||
{23, 46, 69, 92, 116, 139, 162, 185, 208, 231, 254, 277, 300, 324, 347},
|
||||
// Resistance 11
|
||||
{24, 49, 73, 98, 122, 146, 171, 195, 219, 244, 268, 293, 317, 341, 366},
|
||||
// Resistance 12
|
||||
{26, 51, 77, 102, 128, 153, 179, 204, 230, 255, 281, 307, 332, 358, 383},
|
||||
// Resistance 13
|
||||
{27, 54, 80, 107, 134, 161, 188, 214, 241, 268, 295, 322, 348, 375, 402},
|
||||
// Resistance 14
|
||||
{28, 56, 83, 111, 139, 167, 195, 222, 250, 278, 306, 334, 362, 389, 417},
|
||||
// Resistance 15
|
||||
{29, 58, 87, 117, 146, 175, 204, 233, 262, 292, 321, 350, 379, 408, 437},
|
||||
// Resistance 16
|
||||
{30, 61, 91, 121, 152, 182, 212, 242, 273, 303, 333, 364, 394, 424, 455},
|
||||
// Resistance 17
|
||||
{32, 63, 95, 126, 158, 189, 221, 253, 284, 316, 347, 379, 410, 442, 473},
|
||||
// Resistance 18
|
||||
{33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495},
|
||||
// Resistance 19
|
||||
{34, 68, 102, 136, 171, 205, 239, 273, 307, 341, 375, 409, 443, 478, 512}
|
||||
};
|
||||
|
||||
// Clamp resistance to valid range (1-19)
|
||||
int res = qMax(1, qMin(19, (int)qRound(resistance)));
|
||||
|
||||
// Convert to array index (0-18)
|
||||
int resIndex = res - 1;
|
||||
|
||||
// RPM ranges from 10 to 150 in steps of 10
|
||||
// Find the two closest RPM values for interpolation
|
||||
double rpm = qMax(1.0, cadence); // Ensure RPM is at least 1
|
||||
|
||||
if (rpm <= 10.0) {
|
||||
// Below minimum RPM, extrapolate from first data point
|
||||
double factor = rpm / 10.0;
|
||||
return (uint16_t)qMax(0.0, powerTable[resIndex][0] * factor);
|
||||
}
|
||||
|
||||
if (rpm >= 150.0) {
|
||||
// Above maximum RPM, extrapolate from last data point
|
||||
double factor = rpm / 150.0;
|
||||
return (uint16_t)qMax(0.0, powerTable[resIndex][14] * factor);
|
||||
}
|
||||
|
||||
// Find the two RPM values to interpolate between
|
||||
// RPM values are: 10, 20, 30, ..., 150 (indices 0-14)
|
||||
int lowerRpmIndex = ((int)rpm - 1) / 10; // Convert RPM to array index
|
||||
if (lowerRpmIndex > 13) lowerRpmIndex = 13; // Ensure we don't go out of bounds
|
||||
|
||||
int upperRpmIndex = lowerRpmIndex + 1;
|
||||
|
||||
double lowerRpm = (lowerRpmIndex + 1) * 10.0; // Convert index back to RPM
|
||||
double upperRpm = (upperRpmIndex + 1) * 10.0;
|
||||
|
||||
int lowerPower = powerTable[resIndex][lowerRpmIndex];
|
||||
int upperPower = powerTable[resIndex][upperRpmIndex];
|
||||
|
||||
// Linear interpolation between the two power values
|
||||
double ratio = (rpm - lowerRpm) / (upperRpm - lowerRpm);
|
||||
double interpolatedPower = lowerPower + ratio * (upperPower - lowerPower);
|
||||
|
||||
return (uint16_t)qMax(0.0, interpolatedPower);
|
||||
}
|
||||
|
||||
void apexbike::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
|
||||
@@ -70,6 +70,11 @@ void bike::changePower(int32_t power) {
|
||||
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
|
||||
double erg_filter_lower =
|
||||
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
|
||||
|
||||
// Apply bike power offset
|
||||
int bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toInt();
|
||||
power += bike_power_offset;
|
||||
qDebug() << QStringLiteral("changePower: original power with offset applied: ") + QString::number(power) + QStringLiteral(" (offset: ") + QString::number(bike_power_offset) + QStringLiteral(")");
|
||||
|
||||
requestPower = power; // used by some bikes that have ERG mode builtin
|
||||
|
||||
@@ -115,27 +120,60 @@ void bike::setGears(double gears) {
|
||||
gears -= gears_offset;
|
||||
qDebug() << "setGears" << gears;
|
||||
|
||||
// Check for boundaries and emit failure signals
|
||||
// Gear boundary handling with smart clamping logic:
|
||||
// - If we're trying to set a gear outside valid range AND we're already at a valid gear,
|
||||
// reject the change (normal case: user at gear 1 tries to go to 0.5, should fail)
|
||||
// - If we're trying to set a gear outside valid range BUT we're currently below minimum,
|
||||
// clamp to valid range (startup case: system starts at 0, first gearUp with 0.5 gain
|
||||
// goes to 0.5, should be clamped to 1 to allow the system to reach valid state)
|
||||
// This prevents the system from getting stuck below minGears due to fractional gains
|
||||
// while preserving normal boundary rejection behavior for users at valid gear positions
|
||||
if(gears_zwift_ratio && (gears > 24 || gears < 1)) {
|
||||
qDebug() << "new gear value ignored because of gears_zwift_ratio setting!";
|
||||
if(gears > 24) {
|
||||
emit gearFailedUp();
|
||||
if(m_gears >= 24) {
|
||||
qDebug() << "new gear value ignored - already at zwift ratio maximum: 24";
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to zwift ratio maximum: 24";
|
||||
gears = 24;
|
||||
emit gearFailedUp();
|
||||
}
|
||||
} else {
|
||||
emit gearFailedDown();
|
||||
if(m_gears >= 1) {
|
||||
qDebug() << "new gear value ignored - already at zwift ratio minimum: 1";
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to zwift ratio minimum: 1";
|
||||
gears = 1;
|
||||
emit gearFailedDown();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(gears > maxGears()) {
|
||||
qDebug() << "new gear value ignored because of maxGears" << maxGears();
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
if(m_gears >= maxGears()) {
|
||||
qDebug() << "new gear value ignored - already at maxGears" << maxGears();
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to maxGears" << maxGears();
|
||||
gears = maxGears();
|
||||
emit gearFailedUp();
|
||||
}
|
||||
}
|
||||
|
||||
if(gears < minGears()) {
|
||||
qDebug() << "new gear value ignored because of minGears" << minGears();
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
if(m_gears >= minGears()) {
|
||||
qDebug() << "new gear value ignored - already at or above minGears" << minGears();
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to minGears" << minGears();
|
||||
gears = minGears();
|
||||
emit gearFailedDown();
|
||||
}
|
||||
}
|
||||
|
||||
if(m_gears > gears) {
|
||||
|
||||
@@ -52,6 +52,7 @@ class bike : public bluetoothdevice {
|
||||
metric currentSteeringAngle() { return m_steeringAngle; }
|
||||
virtual bool inclinationAvailableByHardware();
|
||||
bool ergModeSupportedAvailableByHardware() { return ergModeSupported; }
|
||||
virtual bool ergModeSupportedAvailableBySoftware() { return ergModeSupported; }
|
||||
|
||||
public Q_SLOTS:
|
||||
void changeResistance(resistance_t res) override;
|
||||
|
||||
@@ -36,6 +36,10 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc
|
||||
|
||||
QString nordictrack_2950_ip =
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
bool fake_bike =
|
||||
settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool();
|
||||
bool fake_treadmill =
|
||||
settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool();
|
||||
|
||||
if (settings.value(QZSettings::peloton_bike_ocr, QZSettings::default_peloton_bike_ocr).toBool() && !pelotonBike) {
|
||||
pelotonBike = new pelotonbike(noWriteResistance, noHeartService);
|
||||
@@ -47,7 +51,29 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(pelotonBike);
|
||||
}
|
||||
}/* else if (fake_bike) {
|
||||
fakeBike = new fakebike(noWriteResistance, noHeartService, false);
|
||||
emit deviceConnected(QBluetoothDeviceInfo());
|
||||
connect(fakeBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
connect(fakeBike, &fakebike::debug, this, &bluetooth::debug);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(fakeBike);
|
||||
return;
|
||||
} else if (fake_treadmill) {
|
||||
fakeTreadmill = new faketreadmill(noWriteResistance, noHeartService, false);
|
||||
emit deviceConnected(QBluetoothDeviceInfo());
|
||||
connect(fakeTreadmill, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
connect(fakeTreadmill, &faketreadmill::debug, this, &bluetooth::debug);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(fakeBike);
|
||||
return;
|
||||
}*/
|
||||
|
||||
#ifdef TEST
|
||||
schwinnIC4Bike = (schwinnic4bike *)new bike();
|
||||
@@ -115,6 +141,7 @@ void bluetooth::finished() {
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
|
||||
QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString();
|
||||
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
bool fake_bike =
|
||||
settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool();
|
||||
bool fakedevice_elliptical =
|
||||
@@ -123,7 +150,7 @@ void bluetooth::finished() {
|
||||
bool fakedevice_treadmill =
|
||||
settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool();
|
||||
// wifi devices on windows
|
||||
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || antbike || android_antbike) {
|
||||
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || !proform_rower_ip.isEmpty() || antbike || android_antbike) {
|
||||
// faking a bluetooth device
|
||||
qDebug() << "faking a bluetooth device for nordictrack_2950_ip";
|
||||
deviceDiscovered(QBluetoothDeviceInfo());
|
||||
@@ -470,6 +497,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
|
||||
QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString();
|
||||
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
QString computrainerSerialPort =
|
||||
settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();
|
||||
QString csaferowerSerialPort = settings.value(QZSettings::csafe_rower, QZSettings::default_csafe_rower).toString();
|
||||
@@ -493,6 +521,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
QString ftms_rower = settings.value(QZSettings::ftms_rower, QZSettings::default_ftms_rower).toString();
|
||||
QString ftms_bike = settings.value(QZSettings::ftms_bike, QZSettings::default_ftms_bike).toString();
|
||||
QString ftms_treadmill = settings.value(QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill).toString();
|
||||
QString ftms_elliptical = settings.value(QZSettings::ftms_elliptical, QZSettings::default_ftms_elliptical).toString();
|
||||
bool saris_trainer = settings.value(QZSettings::saris_trainer, QZSettings::default_saris_trainer).toBool();
|
||||
bool iconsole_elliptical = settings.value(QZSettings::iconsole_elliptical, QZSettings::default_iconsole_elliptical).toBool();
|
||||
bool iconsole_rower = settings.value(QZSettings::iconsole_rower, QZSettings::default_iconsole_rower).toBool();
|
||||
@@ -890,6 +919,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbElliptical);
|
||||
} else if (!proform_rower_ip.isEmpty() && !nordictrackifitadbRower) {
|
||||
this->stopDiscovery();
|
||||
nordictrackifitadbRower = new nordictrackifitadbrower(noWriteResistance, noHeartService,
|
||||
bikeResistanceOffset, bikeResistanceGain);
|
||||
emit deviceConnected(b);
|
||||
connect(nordictrackifitadbRower, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
connect(nordictrackifitadbRower, &nordictrackifitadbrower::debug, this, &bluetooth::debug);
|
||||
// nordictrackifitadbRower->deviceDiscovered(b);
|
||||
// connect(this, SIGNAL(searchingStop()), nordictrackifitadbRower, SLOT(searchingStop())); //NOTE: Commented due to #358
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbRower);
|
||||
} else if (((csc_as_bike && b.name().startsWith(cscName)) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) &&
|
||||
!cscBike && filter) {
|
||||
@@ -991,7 +1034,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(trxappgateusbRower);
|
||||
} else if (((b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && !toorx_bike) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_elliptical)) &&
|
||||
!trxappgateusbElliptical && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
!trxappgateusbElliptical && ftms_bike.contains(QZSettings::default_ftms_bike) && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbElliptical = new trxappgateusbelliptical(noWriteResistance, noHeartService,
|
||||
@@ -1027,12 +1070,15 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("YPOO-U3-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SCH_590E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER ")) ||
|
||||
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()) ||
|
||||
(b.name().toUpper().startsWith("SF-") && b.name().midRef(3).toInt() > 0) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MYELLIPTICAL ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("CARDIOPOWER EEGO")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("E35")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical)) && !ypooElliptical && filter) {
|
||||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical) ||
|
||||
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
ypooElliptical =
|
||||
@@ -1049,7 +1095,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(ypooElliptical);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("NAUTILUS E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("NAUTILUS M"))) &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("NAUTILUS M")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("NAUTILUS 616"))) && // actually this is a bike that uses the same Bluetooth characteristics of the elliptical
|
||||
!nautilusElliptical && // NAUTILUS E616
|
||||
filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1328,6 +1375,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("TRX7.5")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("S77")) && sole_inclination) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F85")) && sole_inclination)) &&
|
||||
ftms_treadmill.contains(QZSettings::default_ftms_treadmill) &&
|
||||
!soleF80 && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1441,18 +1489,22 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
|
||||
(b.name().toUpper().startsWith("SCHWINN 810")) ||
|
||||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
|
||||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FOCUS M3")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KICKR RUN")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-AP-"))) || // Kingsmith WalkingPad R3 Hybrid+
|
||||
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TT8")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ST90")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("XT485")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI WMTP")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("LB600")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T60-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T90-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
|
||||
@@ -1697,10 +1749,11 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("LYDSTO")) ||
|
||||
(b.name().toUpper().startsWith("CYCLO_")) ||
|
||||
(b.name().toUpper().startsWith("SL010-")) ||
|
||||
(b.name().toUpper().startsWith("EXPERT-SX9")) ||
|
||||
(b.name().toUpper().startsWith("LCR")) ||
|
||||
(b.name().toUpper().startsWith("EXPERT-SX9")) ||
|
||||
(b.name().toUpper().startsWith("MRK-S26S-")) ||
|
||||
(b.name().toUpper().startsWith("MRK-S26C-")) ||
|
||||
(b.name().toUpper().startsWith("ROBX")) ||
|
||||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
|
||||
(b.name().toUpper().startsWith("XCX-")) ||
|
||||
(b.name().toUpper().startsWith("NEO BIKE PLUS ")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) && !b.name().toUpper().endsWith(QStringLiteral("SKI")) && !b.name().toUpper().endsWith(QStringLiteral("ROW"))) ||
|
||||
@@ -1715,7 +1768,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("DIRETO X")) || (b.name().toUpper().startsWith("MERACH-667-")) ||
|
||||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) &&
|
||||
!ftmsBike && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
ftms_rower.contains(QZSettings::default_ftms_rower) &&
|
||||
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
ftmsBike = new ftmsbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
@@ -1729,6 +1783,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith("KICKR ROLLR") ||
|
||||
b.name().toUpper().startsWith("KICKR CORE") ||
|
||||
(b.name().toUpper().startsWith("KICKR MOVE ")) ||
|
||||
(b.name().toUpper().startsWith("HOI FRAME ")) ||
|
||||
(b.name().toUpper().startsWith("HAMMER ") && saris_trainer) ||
|
||||
(b.name().toUpper().startsWith("WAHOO KICKR"))) &&
|
||||
!wahooKickrSnapBike && !ftmsBike && filter) {
|
||||
@@ -1743,7 +1798,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(wahooKickrSnapBike, &wahookickrsnapbike::debug, this, &bluetooth::debug);
|
||||
wahooKickrSnapBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(wahooKickrSnapBike);
|
||||
} else if (b.name().toUpper().startsWith("BIKE ") && b.name().midRef(5).toInt() > 0 &&
|
||||
} else if (((b.name().toUpper().startsWith("BIKE ") && b.name().midRef(5).toInt() > 0) ||
|
||||
b.name().toUpper().startsWith("MYCYCLING")) &&
|
||||
!technogymBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1790,7 +1846,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
((b.name().toUpper().startsWith("KU")) && b.name().length() == 2) ||
|
||||
(b.name().toUpper().startsWith("ELITETRAINER")) ||
|
||||
(b.name().toUpper().startsWith("TOUR 600")) ||
|
||||
(b.name().toUpper().startsWith("SMART+ #")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("QD")) && b.name().length() == 2) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("RM")) && b.name().length() == 2) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("DR")) && b.name().length() == 2) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("DFC")) && b.name().length() == 3) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) &&
|
||||
@@ -1848,6 +1906,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YOROTO-RW-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SF-RW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("NORDLYS")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROWER ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DFIT-L-R")) ||
|
||||
@@ -2393,7 +2452,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("DKN RUN"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK")))) &&
|
||||
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower &&
|
||||
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) &&
|
||||
ftms_bike.contains(QZSettings::default_ftms_bike) &&
|
||||
filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2422,7 +2482,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
(toorx_bike))) &&
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower) {
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical)) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbBike =
|
||||
@@ -2458,6 +2518,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
keepBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(keepBike);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("LCB")) ||
|
||||
b.name().toUpper().startsWith("LCR") ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("R92"))) &&
|
||||
!soleBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -2557,6 +2618,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().startsWith(QStringLiteral("SW")) && b.name().length() == 14 &&
|
||||
!b.name().contains('(') && !b.name().contains(')') && !deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WINFITA"))) || // also FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-")) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("SW-BLE"))) || // FTMS
|
||||
(b.name().startsWith(QStringLiteral("BF70")))) &&
|
||||
!fitshowTreadmill && !iconsole_elliptical && !horizonTreadmill && filter) {
|
||||
@@ -3027,18 +3089,23 @@ void bluetooth::connectedAndDiscovered() {
|
||||
}
|
||||
}
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() ||
|
||||
bool android_antbike =
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
|
||||
if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() || android_antbike ||
|
||||
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
|
||||
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
|
||||
"activity", "()Landroid/app/Activity;");
|
||||
KeepAwakeHelper::antObject(true)->callMethod<void>(
|
||||
"antStart", "(Landroid/app/Activity;ZZZZZ)V", activity.object<jobject>(),
|
||||
"antStart", "(Landroid/app/Activity;ZZZZZZII)V", activity.object<jobject>(),
|
||||
settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool(),
|
||||
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool(),
|
||||
settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool(),
|
||||
device()->deviceType() == bluetoothdevice::TREADMILL ||
|
||||
device()->deviceType() == bluetoothdevice::ELLIPTICAL,
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool());
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool(),
|
||||
settings.value(QZSettings::technogym_group_cycle, QZSettings::default_technogym_group_cycle).toBool(),
|
||||
settings.value(QZSettings::ant_bike_device_number, QZSettings::default_ant_bike_device_number).toInt(),
|
||||
settings.value(QZSettings::ant_heart_device_number, QZSettings::default_ant_heart_device_number).toInt());
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) {
|
||||
@@ -3326,6 +3393,11 @@ void bluetooth::restart() {
|
||||
delete nordictrackifitadbElliptical;
|
||||
nordictrackifitadbElliptical = nullptr;
|
||||
}
|
||||
if (nordictrackifitadbRower) {
|
||||
|
||||
delete nordictrackifitadbRower;
|
||||
nordictrackifitadbRower = nullptr;
|
||||
}
|
||||
if (powerBike) {
|
||||
|
||||
delete powerBike;
|
||||
@@ -3800,6 +3872,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return android_antBike;
|
||||
} else if (nordictrackifitadbTreadmill) {
|
||||
return nordictrackifitadbTreadmill;
|
||||
} else if (nordictrackifitadbRower) {
|
||||
return nordictrackifitadbRower;
|
||||
} else if (nordictrackifitadbBike) {
|
||||
return nordictrackifitadbBike;
|
||||
} else if (nordictrackifitadbElliptical) {
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
#include "devices/nordictrackelliptical/nordictrackelliptical.h"
|
||||
#include "devices/nordictrackifitadbbike/nordictrackifitadbbike.h"
|
||||
#include "devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h"
|
||||
#include "devices/nordictrackifitadbrower/nordictrackifitadbrower.h"
|
||||
#include "devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h"
|
||||
#include "devices/npecablebike/npecablebike.h"
|
||||
#include "devices/octaneelliptical/octaneelliptical.h"
|
||||
@@ -222,6 +223,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
nordictrackifitadbtreadmill *nordictrackifitadbTreadmill = nullptr;
|
||||
nordictrackifitadbbike *nordictrackifitadbBike = nullptr;
|
||||
nordictrackifitadbelliptical *nordictrackifitadbElliptical = nullptr;
|
||||
nordictrackifitadbrower *nordictrackifitadbRower = nullptr;
|
||||
octaneelliptical *octaneElliptical = nullptr;
|
||||
octanetreadmill *octaneTreadmill = nullptr;
|
||||
pelotonbike *pelotonBike = nullptr;
|
||||
|
||||
@@ -109,7 +109,52 @@ QTime bluetoothdevice::maxPace() {
|
||||
double bluetoothdevice::odometerFromStartup() { return Distance.valueRaw(); }
|
||||
double bluetoothdevice::odometer() { return Distance.value(); }
|
||||
double bluetoothdevice::lapOdometer() { return Distance.lapValue(); }
|
||||
metric bluetoothdevice::calories() { return KCal; }
|
||||
metric bluetoothdevice::calories() {
|
||||
QSettings settings;
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
|
||||
|
||||
if (fromHR && Heart.value() > 0) {
|
||||
// Calculate calories based on heart rate
|
||||
double totalHRKCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value());
|
||||
hrKCal.setValue(totalHRKCal);
|
||||
|
||||
if (activeOnly) {
|
||||
activeKCal.setValue(metric::calculateActiveKCal(hrKCal.value(), elapsed.value()));
|
||||
return activeKCal;
|
||||
} else {
|
||||
return hrKCal;
|
||||
}
|
||||
} else {
|
||||
// Power-based calculation (current behavior)
|
||||
if (activeOnly) {
|
||||
activeKCal.setValue(metric::calculateActiveKCal(KCal.value(), elapsed.value()));
|
||||
return activeKCal;
|
||||
} else {
|
||||
return KCal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metric bluetoothdevice::totalCalories() {
|
||||
QSettings settings;
|
||||
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
|
||||
|
||||
if (fromHR && Heart.value() > 0) {
|
||||
return hrKCal; // Return HR-based total calories
|
||||
} else {
|
||||
return KCal; // Return power-based total calories
|
||||
}
|
||||
}
|
||||
|
||||
metric bluetoothdevice::activeCalories() {
|
||||
return activeKCal;
|
||||
}
|
||||
|
||||
metric bluetoothdevice::hrCalories() {
|
||||
return hrKCal;
|
||||
}
|
||||
|
||||
metric bluetoothdevice::jouls() { return m_jouls; }
|
||||
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
|
||||
bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
|
||||
@@ -254,7 +299,17 @@ void bluetoothdevice::update_hr_from_external() {
|
||||
#ifndef IO_UNDER_QT
|
||||
lockscreen h;
|
||||
long appleWatchHeartRate = h.heartRate();
|
||||
h.setKcal(KCal.value());
|
||||
QSettings settings;
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
|
||||
if (activeOnly) {
|
||||
// When active calories setting is enabled, send both total and active calories
|
||||
h.setKcal(calories().value()); // This will be active calories
|
||||
h.setTotalKcal(totalCalories().value()); // This will be total calories
|
||||
} else {
|
||||
// When disabled, send total calories as before
|
||||
h.setKcal(calories().value()); // This will be total calories
|
||||
}
|
||||
h.setDistance(Distance.value());
|
||||
h.setSpeed(Speed.value());
|
||||
h.setPower(m_watt.value());
|
||||
@@ -271,6 +326,15 @@ void bluetoothdevice::update_hr_from_external() {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
lockscreen h;
|
||||
double kcal = calories().value();
|
||||
if(kcal < 0)
|
||||
kcal = 0;
|
||||
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value());
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void bluetoothdevice::clearStats() {
|
||||
@@ -279,6 +343,8 @@ void bluetoothdevice::clearStats() {
|
||||
moving.clear(true);
|
||||
Speed.clear(false);
|
||||
KCal.clear(true);
|
||||
hrKCal.clear(true);
|
||||
activeKCal.clear(true);
|
||||
Distance.clear(true);
|
||||
Distance1s.clear(true);
|
||||
Heart.clear(false);
|
||||
@@ -304,6 +370,8 @@ void bluetoothdevice::setPaused(bool p) {
|
||||
elapsed.setPaused(p);
|
||||
Speed.setPaused(p);
|
||||
KCal.setPaused(p);
|
||||
hrKCal.setPaused(p);
|
||||
activeKCal.setPaused(p);
|
||||
Distance.setPaused(p);
|
||||
Distance1s.setPaused(p);
|
||||
Heart.setPaused(p);
|
||||
@@ -327,6 +395,8 @@ void bluetoothdevice::setLap() {
|
||||
elapsed.setLap(true);
|
||||
Speed.setLap(false);
|
||||
KCal.setLap(true);
|
||||
hrKCal.setLap(true);
|
||||
activeKCal.setLap(true);
|
||||
Distance.setLap(true);
|
||||
Distance1s.setLap(true);
|
||||
Heart.setLap(false);
|
||||
|
||||
@@ -108,11 +108,19 @@ class bluetoothdevice : public QObject {
|
||||
|
||||
/**
|
||||
* @brief calories Gets a metric object to get and set the amount of energy expended.
|
||||
* Default implementation returns the protected KCal property. Units: kcal
|
||||
* Default implementation returns the protected KCal property, potentially adjusted for active calories. Units: kcal
|
||||
* Other implementations could have different units.
|
||||
* @return
|
||||
*/
|
||||
virtual metric calories();
|
||||
virtual metric activeCalories();
|
||||
virtual metric hrCalories();
|
||||
|
||||
/**
|
||||
* @brief totalCalories Gets total calories (including BMR) regardless of active calories setting.
|
||||
* @return Total calories metric
|
||||
*/
|
||||
virtual metric totalCalories();
|
||||
|
||||
/**
|
||||
* @brief jouls Gets a metric object to get and set the number of joules expended. Units: joules
|
||||
@@ -548,6 +556,8 @@ class bluetoothdevice : public QObject {
|
||||
* @brief KCal The number of kilocalories expended in the session. Units: kcal
|
||||
*/
|
||||
metric KCal;
|
||||
metric activeKCal;
|
||||
metric hrKCal;
|
||||
|
||||
/**
|
||||
* @brief Speed The simulated speed of the device. Units: km/h
|
||||
|
||||
@@ -285,9 +285,9 @@ double bowflext216treadmill::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
double data = (double)convertedData / 100.0f;
|
||||
return data * 1.60934;
|
||||
} else if(bowflex_T128) {
|
||||
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(12)) + ((uint16_t)((uint8_t)packet.at(13)) << 8);
|
||||
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(9)) + ((uint16_t)((uint8_t)packet.at(10)) << 8);
|
||||
double data = (double)convertedData / 100.0f;
|
||||
return data;
|
||||
return data * 1.60934;
|
||||
} else if (bowflex_t6 == false) {
|
||||
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(6)) + (((uint16_t)((uint8_t)packet.at(7)) << 8) & 0xFF00);
|
||||
double data = (double)convertedData / 100.0f;
|
||||
|
||||
@@ -284,7 +284,7 @@ void csafeelliptical::update() {
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -72,7 +72,14 @@ void cscbike::update() {
|
||||
}
|
||||
}
|
||||
|
||||
m_watt = wattFromHR(false);
|
||||
bool rogue_echo_bike = settings.value(QZSettings::rogue_echo_bike, QZSettings::default_rogue_echo_bike).toBool();
|
||||
|
||||
if (rogue_echo_bike) {
|
||||
double rpm = currentCadence().value();
|
||||
m_watt = 0.000602337 * pow(rpm, 3.11762) + 32.6404;
|
||||
} else {
|
||||
m_watt = wattFromHR(false);
|
||||
}
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
|
||||
@@ -738,7 +738,7 @@ void cycleopsphantombike::characteristicChanged(const QLowEnergyCharacteristic &
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)currentHeart().value());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -75,6 +75,38 @@ void deerruntreadmill::writeCharacteristic(const QLowEnergyCharacteristic charac
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
connect(unlock_service, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
|
||||
if (unlock_service->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit debug(QStringLiteral("writeUnlockCharacteristic error because the connection is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
unlock_service->writeCharacteristic(unlock_characteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> unlock ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) {
|
||||
uint8_t result = 0;
|
||||
|
||||
@@ -183,21 +215,26 @@ void deerruntreadmill::update() {
|
||||
lastSpeed = 0.5;
|
||||
}
|
||||
|
||||
// should be:
|
||||
// 0x49 = inited
|
||||
// 0x8a = tape stopped after a pause
|
||||
/*if (lastState == 0x49)*/ {
|
||||
uint8_t initData2[] = {0x4d, 0x00, 0x0c, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0x2a, 0x43};
|
||||
initData2[2] = pollCounter;
|
||||
if (pitpat) {
|
||||
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
|
||||
} else {
|
||||
// should be:
|
||||
// 0x49 = inited
|
||||
// 0x8a = tape stopped after a pause
|
||||
/*if (lastState == 0x49)*/ {
|
||||
uint8_t initData2[] = {0x4d, 0x00, 0x0c, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0x2a, 0x43};
|
||||
initData2[2] = pollCounter;
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("start"),
|
||||
false, true);
|
||||
} /*else {
|
||||
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("start"),
|
||||
false, true);
|
||||
} /*else {
|
||||
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
|
||||
true);
|
||||
}*/
|
||||
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
|
||||
true);
|
||||
}*/
|
||||
}
|
||||
|
||||
requestStart = -1;
|
||||
emit tapeStarted();
|
||||
@@ -219,11 +256,16 @@ void deerruntreadmill::update() {
|
||||
|
||||
requestStop = -1;
|
||||
} else {
|
||||
uint8_t poll[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
poll[2] = pollCounter;
|
||||
if (pitpat) {
|
||||
uint8_t poll[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("pitpat poll"), false, true);
|
||||
} else {
|
||||
uint8_t poll[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
poll[2] = pollCounter;
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("poll"), false,
|
||||
true);
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("poll"), false,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
pollCounter++;
|
||||
@@ -265,13 +307,16 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
emit debug(QStringLiteral(" << ") + QString::number(value.length()) + QStringLiteral(" ") + value.toHex(' '));
|
||||
emit packetReceived();
|
||||
|
||||
if (newValue.length() < 51)
|
||||
if ((newValue.length() < 51 && !pitpat) || (newValue.length() < 50 && pitpat))
|
||||
return;
|
||||
|
||||
lastPacket = value;
|
||||
// lastState = value.at(0);
|
||||
|
||||
double speed = ((double)(((value[9] << 8) & 0xff) + value[10]) / 100.0);
|
||||
if(pitpat) {
|
||||
speed = ((double)((value[3] << 8) | ((uint8_t)value[4])) / 1000.0);
|
||||
}
|
||||
double incline = 0.0;
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
@@ -343,6 +388,20 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
void deerruntreadmill::btinit(bool startTape) {
|
||||
if (pitpat) {
|
||||
// PitPat treadmill initialization sequence
|
||||
uint8_t initData1[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("pitpat init 1"), false, true);
|
||||
|
||||
uint8_t unlockData[] = {0x6b, 0x05, 0x9d, 0x98, 0x43};
|
||||
writeUnlockCharacteristic(unlockData, sizeof(unlockData), QStringLiteral("pitpat unlock"), false);
|
||||
|
||||
uint8_t initData2[] = {0x6a, 0x05, 0xd7, 0xd2, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("pitpat init 2"), false, true);
|
||||
|
||||
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
|
||||
}
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
@@ -352,11 +411,30 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
|
||||
QBluetoothUuid _gattWriteCharacteristicId((quint16)0xfff1);
|
||||
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xfff2);
|
||||
QBluetoothUuid _pitpatWriteCharacteristicId((quint16)0xfba1);
|
||||
QBluetoothUuid _pitpatNotifyCharacteristicId((quint16)0xfba2);
|
||||
QBluetoothUuid _unlockCharacteristicId((quint16)0x2b2a);
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
|
||||
QLowEnergyService* service = qobject_cast<QLowEnergyService*>(sender());
|
||||
if (service == unlock_service && pitpat) {
|
||||
// Handle unlock service characteristics
|
||||
auto characteristics_list = unlock_service->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("unlock char uuid") << c.uuid() << QStringLiteral("handle") << c.handle()
|
||||
<< c.properties();
|
||||
}
|
||||
|
||||
unlock_characteristic = unlock_service->characteristic(_unlockCharacteristicId);
|
||||
if (unlock_characteristic.isValid()) {
|
||||
emit debug(QStringLiteral("unlock characteristic found"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// qDebug() << gattCommunicationChannelService->characteristics();
|
||||
auto characteristics_list = gattCommunicationChannelService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
@@ -364,8 +442,14 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
<< c.properties();
|
||||
}
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
|
||||
if (pitpat) {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_pitpatWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_pitpatNotifyCharacteristicId);
|
||||
} else {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
|
||||
}
|
||||
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotifyCharacteristic.isValid());
|
||||
|
||||
@@ -403,6 +487,8 @@ void deerruntreadmill::characteristicWritten(const QLowEnergyCharacteristic &cha
|
||||
|
||||
void deerruntreadmill::serviceScanDone(void) {
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0);
|
||||
QBluetoothUuid _pitpatServiceId((quint16)0xfba0);
|
||||
QBluetoothUuid _unlockServiceId((quint16)0x1801);
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
@@ -411,7 +497,17 @@ void deerruntreadmill::serviceScanDone(void) {
|
||||
emit debug(s.toString());
|
||||
}
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
// Check if this is a pitpat treadmill by looking for the 0xfba0 service
|
||||
if (services_list.contains(_pitpatServiceId)) {
|
||||
pitpat = true;
|
||||
emit debug(QStringLiteral("Detected pitpat treadmill variant"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_pitpatServiceId);
|
||||
unlock_service = m_control->createServiceObject(_unlockServiceId);
|
||||
} else {
|
||||
pitpat = false;
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService) {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
|
||||
&deerruntreadmill::stateChanged);
|
||||
@@ -419,6 +515,12 @@ void deerruntreadmill::serviceScanDone(void) {
|
||||
} else {
|
||||
emit debug(QStringLiteral("error on find Service"));
|
||||
}
|
||||
|
||||
if (pitpat && unlock_service) {
|
||||
connect(unlock_service, &QLowEnergyService::stateChanged, this,
|
||||
&deerruntreadmill::stateChanged);
|
||||
unlock_service->discoverDetails();
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::errorService(QLowEnergyService::ServiceError err) {
|
||||
|
||||
@@ -48,6 +48,7 @@ class deerruntreadmill : public treadmill {
|
||||
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);
|
||||
void writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false);
|
||||
void startDiscover();
|
||||
uint8_t calculateXOR(uint8_t arr[], size_t size);
|
||||
bool noConsole = false;
|
||||
@@ -66,6 +67,11 @@ class deerruntreadmill : public treadmill {
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
|
||||
QLowEnergyService *unlock_service = nullptr;
|
||||
QLowEnergyCharacteristic unlock_characteristic;
|
||||
|
||||
bool pitpat = false;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
@@ -150,6 +150,11 @@ void echelonconnectsport::update() {
|
||||
initDone) {
|
||||
update_metrics(true, watts());
|
||||
|
||||
// Continuous ERG mode support - recalculate resistance as cadence changes when using power zone tiles
|
||||
if (RequestedPower.value() > 0) {
|
||||
changePower(RequestedPower.value());
|
||||
}
|
||||
|
||||
// sending poll every 2 seconds
|
||||
if (sec1Update++ >= (2000 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
|
||||
@@ -106,8 +106,12 @@ void eslinkertreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, con
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
} else {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
}
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
@@ -704,6 +708,8 @@ void eslinkertreadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotifyCharacteristic.isValid());
|
||||
|
||||
qDebug() << (gattWriteCharacteristic.properties() & QLowEnergyService::WriteWithoutResponse);
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&eslinkertreadmill::characteristicChanged);
|
||||
|
||||
@@ -65,6 +65,14 @@ void fakebike::update() {
|
||||
speedLimit());
|
||||
}
|
||||
|
||||
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
if (watts())
|
||||
KCal +=
|
||||
((((0.048 * ((double)watts()) + 1.19) * weight * 3.5) / 200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
|
||||
QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
@@ -171,28 +179,7 @@ uint16_t fakebike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t fakebike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
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();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1141,20 +1141,5 @@ uint16_t fitplusbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t fitplusbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; 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 max_resistance;
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ void fitshowtreadmill::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
qDebug() << "adding" << gatt.toString() << "as the default service";
|
||||
serviceId = gatt; // NOTE: clazy-rule-of-tow
|
||||
}
|
||||
if(gatt == QBluetoothUuid((quint16)0x1826) && !fs_connected) {
|
||||
if(gatt == QBluetoothUuid((quint16)0x1826) && !fs_connected && !tunturi_t80_connected) {
|
||||
QSettings settings;
|
||||
settings.setValue(QZSettings::ftms_treadmill, bluetoothDevice.name());
|
||||
qDebug() << "forcing FTMS treadmill since it has FTMS";
|
||||
@@ -845,6 +845,9 @@ void fitshowtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << "NOBLEPRO FIX!";
|
||||
minStepInclinationValue = 0.5;
|
||||
noblepro_connected = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-"))) {
|
||||
qDebug() << "TUNTURI T80 detected - ignoring FTMS forcing";
|
||||
tunturi_t80_connected = true;
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -153,6 +153,7 @@ class fitshowtreadmill : public treadmill {
|
||||
double minStepInclinationValue = 1.0;
|
||||
bool noblepro_connected = false;
|
||||
bool fs_connected = false;
|
||||
bool tunturi_t80_connected = false;
|
||||
|
||||
metric rawInclination;
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ void ftmsbike::init() {
|
||||
if (initDone)
|
||||
return;
|
||||
|
||||
if(ICSE) {
|
||||
if(ICSE || HAMMER) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
bool ret = writeCharacteristic(write, sizeof(write), "requestControl", false, true);
|
||||
write[0] = {FTMS_RESET};
|
||||
@@ -194,7 +194,7 @@ void ftmsbike::zwiftPlayInit() {
|
||||
}
|
||||
|
||||
void ftmsbike::forcePower(int16_t requestPower) {
|
||||
if(resistance_lvl_mode || TITAN_7000) {
|
||||
if((resistance_lvl_mode || TITAN_7000) && !MAGNUS) {
|
||||
forceResistance(resistanceFromPowerRequest(requestPower));
|
||||
} else {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00};
|
||||
@@ -216,23 +216,9 @@ uint16_t ftmsbike::wattsFromResistance(double resistance) {
|
||||
return _ergTable.estimateWattage(Cadence.value(), resistance);
|
||||
}
|
||||
|
||||
|
||||
resistance_t ftmsbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; 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 max_resistance;
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
@@ -246,6 +232,10 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
|
||||
double fr = (((double)requestResistance) * bikeResistanceGain) + ((double)bikeResistanceOffset);
|
||||
if(ergModeNotSupported) {
|
||||
if(requestResistance < 0) {
|
||||
qDebug() << "Negative resistance detected:" << requestResistance << "using fallback value 1";
|
||||
requestResistance = 1;
|
||||
}
|
||||
requestResistance = _inclinationResistanceTable.estimateInclination(requestResistance) * 10.0;
|
||||
qDebug() << "ergMode Not Supported so the resistance will be" << requestResistance;
|
||||
} else {
|
||||
@@ -411,8 +401,15 @@ void ftmsbike::update() {
|
||||
|
||||
lastGearValue = gears();
|
||||
|
||||
// if a classic request of power from zwift or any other platform is coming, will be transfereed on the ftmsCharacteristicChanged applying the gear mod too
|
||||
if (requestPower != -1 && (!virtualBike || !virtualBike->ftmsDeviceConnected() || (zwiftPlayService != nullptr && gears_zwift_ratio))) {
|
||||
// Power request routing logic:
|
||||
// 1. No virtualBike: route directly to bike
|
||||
// 2. VirtualBike not connected to FTMS: route directly to bike
|
||||
// 3. ZwiftPlay with gear ratio: route directly to bike
|
||||
// 4. ErgMode supported + power sensor: use delta power system (bypass FTMS routing)
|
||||
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"));
|
||||
if (requestPower != -1 && (!virtualBike || !virtualBike->ftmsDeviceConnected() || (zwiftPlayService != nullptr && gears_zwift_ratio) || (ergModeSupported && power_sensor))) {
|
||||
qDebug() << QStringLiteral("writing power") << requestPower;
|
||||
init();
|
||||
forcePower(requestPower);
|
||||
@@ -666,14 +663,22 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
if(DU30_bike) {
|
||||
m_watt = wattsFromResistance(Resistance.value());
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else if (MRK_S26C) {
|
||||
m_watt = Cadence.value() * (Resistance.value() * 1.16);
|
||||
emit debug(QStringLiteral("Current Watt (MRK-S26C formula): ") + QString::number(m_watt.value()));
|
||||
} else if (LYDSTO && watt_ignore_builtin) {
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
} else {
|
||||
double ftms_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
}
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else if(DOMYOS) {
|
||||
@@ -742,6 +747,182 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged2AD2 = now;
|
||||
ftmsFrameReceived = true;
|
||||
} else if (characteristic.uuid() == QBluetoothUuid::CyclingPowerMeasurement && !ftmsFrameReceived) {
|
||||
uint16_t flags = (((uint16_t)((uint8_t)newValue.at(1)) << 8) | (uint16_t)((uint8_t)newValue.at(0)));
|
||||
bool cadence_present = false;
|
||||
bool wheel_revs = false;
|
||||
bool crank_rev_present = false;
|
||||
uint16_t time_division = 1024;
|
||||
uint8_t index = 4;
|
||||
|
||||
if (newValue.length() > 3) {
|
||||
double ftms_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
emit powerChanged(m_watt.value());
|
||||
}
|
||||
emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
if(THINK_X) {
|
||||
|
||||
if ((flags & 0x1) == 0x01) // Pedal Power Balance Present
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
if ((flags & 0x2) == 0x02) // Pedal Power Balance Reference
|
||||
{
|
||||
}
|
||||
if ((flags & 0x4) == 0x04) // Accumulated Torque Present
|
||||
{
|
||||
index += 2;
|
||||
}
|
||||
if ((flags & 0x8) == 0x08) // Accumulated Torque Source
|
||||
{
|
||||
}
|
||||
|
||||
if ((flags & 0x10) == 0x10) // Wheel Revolution Data Present
|
||||
{
|
||||
cadence_present = true;
|
||||
wheel_revs = true;
|
||||
}
|
||||
|
||||
if ((flags & 0x20) == 0x20) // Crank Revolution Data Present
|
||||
{
|
||||
cadence_present = true;
|
||||
crank_rev_present = true;
|
||||
}
|
||||
|
||||
if (cadence_present) {
|
||||
if (wheel_revs && !crank_rev_present) {
|
||||
time_division = 2048;
|
||||
CrankRevs =
|
||||
(((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) |
|
||||
((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)));
|
||||
index += 4;
|
||||
|
||||
LastCrankEventTime =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
|
||||
index += 2; // wheel event time
|
||||
|
||||
} else if (wheel_revs && crank_rev_present) {
|
||||
index += 4; // wheel revs
|
||||
index += 2; // wheel event time
|
||||
}
|
||||
|
||||
if (crank_rev_present) {
|
||||
CrankRevs =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
index += 2;
|
||||
|
||||
LastCrankEventTime =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
index += 2;
|
||||
}
|
||||
|
||||
int16_t deltaT = LastCrankEventTime - oldLastCrankEventTime;
|
||||
if (deltaT < 0) {
|
||||
deltaT = LastCrankEventTime + time_division - oldLastCrankEventTime;
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
if (CrankRevs != oldCrankRevs && deltaT) {
|
||||
double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * time_division * 60;
|
||||
if (!crank_rev_present)
|
||||
cadence =
|
||||
cadence /
|
||||
2; // I really don't like this, there is no relationship between wheel rev and crank rev
|
||||
if (cadence >= 0) {
|
||||
Cadence = cadence;
|
||||
}
|
||||
lastGoodCadence = now;
|
||||
} else if (lastGoodCadence.msecsTo(now) > 2000) {
|
||||
Cadence = 0;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Current Cadence: ") << Cadence.value() << CrankRevs << oldCrankRevs << deltaT
|
||||
<< time_division << LastCrankEventTime << oldLastCrankEventTime;
|
||||
|
||||
oldLastCrankEventTime = LastCrankEventTime;
|
||||
oldCrankRevs = CrankRevs;
|
||||
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = Cadence.value() * settings
|
||||
.value(QZSettings::cadence_sensor_speed_ratio,
|
||||
QZSettings::default_cadence_sensor_speed_ratio)
|
||||
.toDouble();
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
|
||||
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChangedPower.msecsTo(now)));
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
|
||||
// if we change this, also change the wattsFromResistance function. We can create a standard function in
|
||||
// order to have all the costants in one place (I WANT MORE TIME!!!)
|
||||
double ac = 0.01243107769;
|
||||
double bc = 1.145964912;
|
||||
double cc = -23.50977444;
|
||||
|
||||
double ar = 0.1469553975;
|
||||
double br = -5.841344538;
|
||||
double cr = 97.62165482;
|
||||
|
||||
double res =
|
||||
(((sqrt(pow(br, 2.0) - 4.0 * ar *
|
||||
(cr - (m_watt.value() * 132.0 /
|
||||
(ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) -
|
||||
br) /
|
||||
(2.0 * ar)) *
|
||||
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
|
||||
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
|
||||
|
||||
if (isnan(res)) {
|
||||
if (Cadence.value() > 0) {
|
||||
// let's keep the last good value
|
||||
} else {
|
||||
m_pelotonResistance = 0;
|
||||
}
|
||||
} else {
|
||||
m_pelotonResistance = res;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Current Peloton Resistance: ") + QString::number(m_pelotonResistance.value());
|
||||
|
||||
if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance)
|
||||
.toBool())
|
||||
Resistance = pelotonToBikeResistance(m_pelotonResistance.value());
|
||||
else
|
||||
Resistance = m_pelotonResistance;
|
||||
emit resistanceRead(Resistance.value());
|
||||
qDebug() << QStringLiteral("Current Resistance Calculated: ") + QString::number(Resistance.value());
|
||||
|
||||
if (watts())
|
||||
KCal +=
|
||||
((((0.048 * ((double)watts()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChangedPower.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()));
|
||||
|
||||
lastRefreshCharacteristicChangedPower = now;
|
||||
}
|
||||
}
|
||||
} else if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACE)) {
|
||||
union flags {
|
||||
struct {
|
||||
@@ -802,8 +983,11 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
1000.0;
|
||||
index += 3;
|
||||
} else {
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged2ACE.msecsTo(now)));
|
||||
// Only calculate distance if 2AD2 hasn't already done it recently (within 2000ms)
|
||||
if (lastRefreshCharacteristicChanged2AD2.msecsTo(now) > 2000) {
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged2ACE.msecsTo(now)));
|
||||
}
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
@@ -868,11 +1052,14 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
}
|
||||
|
||||
if (Flags.instantPower) {
|
||||
double ftms_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
index += 2;
|
||||
}
|
||||
@@ -951,8 +1138,14 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
update_hr_from_external();
|
||||
}
|
||||
|
||||
if(resistance_received && requestPower == -1)
|
||||
_inclinationResistanceTable.collectData(Inclination.value(), Resistance.value(), m_watt.value());
|
||||
if(resistance_received && requestPower == -1) {
|
||||
// Apply the same gears modification as in ftmsCharacteristicChanged
|
||||
double gears_modified_inclination = Inclination.value();
|
||||
if (gears() != 0) {
|
||||
gears_modified_inclination += (gears() * GEARS_SLOPE_MULTIPLIER / 100.0);
|
||||
}
|
||||
_inclinationResistanceTable.collectData(gears_modified_inclination, Resistance.value(), m_watt.value());
|
||||
}
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
@@ -960,7 +1153,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -1092,6 +1285,10 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
settings.setValue(QZSettings::domyosbike_notfmts, true);
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Domyos bike presents itself like a FTMS but it's not. Restart QZ to apply the fix, thanks.");
|
||||
} else if(gattFTMSService == nullptr && PM5) {
|
||||
settings.setValue(QZSettings::ftms_rower, bluetoothDevice.name());
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("PM5 rower found. Restart QZ to apply the fix, thanks.");
|
||||
}
|
||||
|
||||
if (gattFTMSService && gattWriteCharControlPointId.isValid() &&
|
||||
@@ -1139,15 +1336,27 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
|
||||
void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
|
||||
QSettings settings;
|
||||
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"));
|
||||
|
||||
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
|
||||
if (!autoResistance() || resistance_lvl_mode || ergModeNotSupported) {
|
||||
bool isPowerCommand = (newValue.length() > 0 && (uint8_t)newValue.at(0) == FTMS_SET_TARGET_POWER);
|
||||
|
||||
// FTMS routing filter logic:
|
||||
// - Block simulation commands (0x11) when resistance_lvl_mode=true
|
||||
// - Allow power commands (0x05) only when no external power sensor (delta power system handles external sensors)
|
||||
bool allowPowerRouting = (!power_sensor && ergModeSupported && isPowerCommand);
|
||||
|
||||
if (!autoResistance() || (resistance_lvl_mode && !allowPowerRouting) || ergModeNotSupported) {
|
||||
qDebug() << "ignoring routing FTMS packet to the bike from virtualbike because of auto resistance OFF or resistance lvl mode is on or ergModeNotSupported"
|
||||
<< characteristic.uuid() << newValue.toHex(' ') << ergModeNotSupported << resistance_lvl_mode;
|
||||
<< characteristic.uuid() << newValue.toHex(' ') << "ergModeNotSupported:" << ergModeNotSupported
|
||||
<< "resistance_lvl_mode:" << resistance_lvl_mode << "power_sensor:" << power_sensor << "isPowerCommand:" << isPowerCommand;
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray b = newValue;
|
||||
QSettings settings;
|
||||
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
|
||||
|
||||
if (gattWriteCharControlPointId.isValid()) {
|
||||
@@ -1168,7 +1377,7 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8));
|
||||
|
||||
if (gears() != 0) {
|
||||
slope += (gears() * 50);
|
||||
slope += (gears() * GEARS_SLOPE_MULTIPLIER);
|
||||
}
|
||||
|
||||
if(min_inclination > (((double)slope) / 100.0)) {
|
||||
@@ -1322,7 +1531,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
max_resistance = 16;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("MAGNUS "))) {
|
||||
qDebug() << QStringLiteral("MAGNUS found");
|
||||
resistance_lvl_mode = true;
|
||||
MAGNUS = true;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("DU30-"))) {
|
||||
qDebug() << QStringLiteral("DU30 found");
|
||||
max_resistance = 32;
|
||||
@@ -1333,6 +1544,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("DOMYOS"))) {
|
||||
qDebug() << QStringLiteral("DOMYOS found");
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
DOMYOS = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("3G Cardio RB"))) {
|
||||
qDebug() << QStringLiteral("_3G_Cardio_RB found");
|
||||
@@ -1366,6 +1578,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("SPAX-BK-"))) {
|
||||
qDebug() << QStringLiteral("SPAX-BK found");
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("LYDSTO"))) {
|
||||
qDebug() << QStringLiteral("LYDSTO found");
|
||||
LYDSTO = true;
|
||||
@@ -1374,11 +1587,13 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
SL010 = true;
|
||||
max_resistance = 25;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("REEBOK"))) {
|
||||
qDebug() << QStringLiteral("REEBOK found");
|
||||
REEBOK = true;
|
||||
max_resistance = 32;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("TITAN 7000"))) {
|
||||
qDebug() << QStringLiteral("Titan 7000 found");
|
||||
TITAN_7000 = true;
|
||||
@@ -1397,7 +1612,29 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("YS_G1MPLUS found");
|
||||
YS_G1MPLUS = true;
|
||||
max_resistance = 100;
|
||||
} else if (bluetoothDevice.name().toUpper().startsWith(QStringLiteral("PM5"))) {
|
||||
PM5 = true;
|
||||
qDebug() << QStringLiteral("PM5 found");
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("THINK X")) || device.name().toUpper().startsWith(QStringLiteral("THINK-"))) {
|
||||
THINK_X = true;
|
||||
qDebug() << "THINK X workaround enabled!";
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("WLT8828"))) {
|
||||
qDebug() << QStringLiteral("WLT8828 found");
|
||||
WLT8828 = true;
|
||||
max_resistance = 32;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false; // this bike doesn't have ERG mode natively
|
||||
} else if(device.name().toUpper().startsWith("VANRYSEL-HT")) {
|
||||
qDebug() << QStringLiteral("VANRYSEL-HT found");
|
||||
VANRYSEL_HT = true;
|
||||
} else if(device.name().toUpper().startsWith("MRK-S26C-")) {
|
||||
qDebug() << QStringLiteral("MRK-S26C found");
|
||||
MRK_S26C = true;
|
||||
} else if(device.name().toUpper().startsWith("HAMMER")) {
|
||||
qDebug() << QStringLiteral("HAMMER found");
|
||||
HAMMER = true;
|
||||
}
|
||||
|
||||
|
||||
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {
|
||||
resistance_lvl_mode = true;
|
||||
@@ -1455,7 +1692,7 @@ void ftmsbike::setWheelDiameter(double diameter) {
|
||||
}
|
||||
|
||||
uint16_t ftmsbike::watts() {
|
||||
if (currentCadence().value() == 0) {
|
||||
if (currentCadence().value() == 0 && !VANRYSEL_HT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ class ftmsbike : public bike {
|
||||
double maxGears() override;
|
||||
double minGears() override;
|
||||
|
||||
// true because or the bike supports it by hardware or because QZ is emulating this in this module
|
||||
bool ergModeSupportedAvailableBySoftware() override { return true; }
|
||||
|
||||
private:
|
||||
bool writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
|
||||
bool wait_for_response = false);
|
||||
@@ -94,6 +97,9 @@ class ftmsbike : public bike {
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
// Gear modification constants
|
||||
static constexpr int GEARS_SLOPE_MULTIPLIER = 50;
|
||||
|
||||
QList<QLowEnergyService *> gattCommunicationChannelService;
|
||||
QLowEnergyCharacteristic gattWriteCharControlPointId;
|
||||
@@ -105,8 +111,10 @@ class ftmsbike : public bike {
|
||||
uint8_t sec1Update = 0;
|
||||
QByteArray lastPacket;
|
||||
QByteArray lastPacketFromFTMS;
|
||||
QDateTime lastRefreshCharacteristicChangedPower = QDateTime::currentDateTime();
|
||||
QDateTime lastRefreshCharacteristicChanged2AD2 = QDateTime::currentDateTime();
|
||||
QDateTime lastRefreshCharacteristicChanged2ACE = QDateTime::currentDateTime();
|
||||
bool ftmsFrameReceived = false;
|
||||
uint8_t firstStateChanged = 0;
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
double bikeResistanceGain = 1.0;
|
||||
@@ -146,11 +154,22 @@ class ftmsbike : public bike {
|
||||
bool FIT_BK = false;
|
||||
bool YS_G1MPLUS = false;
|
||||
bool EXPERT_SX9 = false;
|
||||
bool PM5 = false;
|
||||
bool THINK_X = false;
|
||||
bool WLT8828 = false;
|
||||
bool VANRYSEL_HT = false;
|
||||
bool MAGNUS = false;
|
||||
bool MRK_S26C = false;
|
||||
bool HAMMER = false;
|
||||
|
||||
int16_t T2_lastGear = 0;
|
||||
|
||||
uint8_t battery_level = 0;
|
||||
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
QDateTime lastGoodCadence = QDateTime::currentDateTime();
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
@@ -62,12 +62,11 @@ void ftmsrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
|
||||
|
||||
void ftmsrower::forceResistance(resistance_t requestResistance) {
|
||||
|
||||
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
write[3] = ((uint16_t)requestResistance * 100) & 0xFF;
|
||||
write[4] = ((uint16_t)requestResistance * 100) >> 8;
|
||||
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00};
|
||||
write[1] = ((uint8_t)(requestResistance * 10));
|
||||
writeCharacteristic(write, sizeof(write), QStringLiteral("forceResistance ") + QString::number(requestResistance));
|
||||
if(NORDLYS)
|
||||
Resistance = requestResistance; // Nordlys does not report back the resistance so we set it here
|
||||
}
|
||||
|
||||
void ftmsrower::update() {
|
||||
@@ -143,6 +142,123 @@ void ftmsrower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QSettings settings;
|
||||
|
||||
QString charUuid = characteristic.uuid().toString();
|
||||
|
||||
if (charUuid == QStringLiteral("{ce060031-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060031 - Based on go-row implementation
|
||||
if (newValue.length() >= 10) {
|
||||
// Extract RowState from byte 9 - this indicates if user is actively rowing
|
||||
pm5RowState = (uint8_t)newValue.at(9);
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060031 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060032-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060032 - Based on go-row implementation
|
||||
if (newValue.length() >= 7) {
|
||||
// Extract cadence (SPM) from byte 5
|
||||
uint8_t spm = (uint8_t)newValue.at(5);
|
||||
if (spm > 0) {
|
||||
Cadence = spm;
|
||||
lastStroke = now;
|
||||
}
|
||||
|
||||
// Extract speed from bytes 3-4 (little endian) in 0.001m/s
|
||||
uint16_t speedRaw = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
|
||||
if (speedRaw > 0) {
|
||||
Speed = (speedRaw * 0.001) * 3.6; // Convert m/s to km/h
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060032 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" Cadence: ") + QString::number(Cadence.value()) +
|
||||
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060033-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060033 - Additional data
|
||||
if (newValue.length() >= 20) {
|
||||
emit debug(QStringLiteral("PM5 CE060033 RAW: ") + newValue.toHex(' '));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060036-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060036 - Power and stroke count (based on go-row implementation)
|
||||
if (newValue.length() >= 9) {
|
||||
// Extract stroke count from bytes 7-8 (little endian)
|
||||
uint16_t strokeCount = ((uint8_t)newValue.at(8) << 8) | (uint8_t)newValue.at(7);
|
||||
if (strokeCount != StrokesCount.value()) {
|
||||
StrokesCount = strokeCount;
|
||||
lastStroke = now;
|
||||
}
|
||||
|
||||
// Extract power from bytes 3-4 (little endian)
|
||||
uint16_t power = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
|
||||
if (power > 0) {
|
||||
m_watt = power;
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060036 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
|
||||
QStringLiteral(" Stroke Count: ") + QString::number(StrokesCount.value()) +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060035-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060035 - Stroke data including drive length (stroke length)
|
||||
if (newValue.length() >= 7) {
|
||||
// Extract drive length (stroke length) from byte 6 - 0.01 meters LSB, max 2.55m
|
||||
uint8_t driveLengthRaw = (uint8_t)newValue.at(6);
|
||||
if (driveLengthRaw > 0) {
|
||||
// Convert from 0.01m units to meters
|
||||
double strokeLengthMeters = driveLengthRaw * 0.01;
|
||||
StrokesLength = strokeLengthMeters;
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060035 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" Stroke Length: ") + QString::number(StrokesLength.value()) +
|
||||
QStringLiteral("m RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
|
||||
// Update calories based on power if available
|
||||
if (m_watt.value() > 0) {
|
||||
KCal += ((((0.048 * ((double)m_watt.value()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
|
||||
// Update crank revolutions for virtual device compatibility
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged = now;
|
||||
|
||||
// Apply RowState logic after all characteristics processing
|
||||
if (PM5 && pm5RowState == 0) {
|
||||
m_watt = 0;
|
||||
Cadence = 0;
|
||||
Speed = 0;
|
||||
}
|
||||
|
||||
// Update metrics for virtual device
|
||||
update_metrics(false, m_watt.value());
|
||||
|
||||
emit debug(QStringLiteral("PM5 Metrics - Cadence: ") + QString::number(Cadence.value()) +
|
||||
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
|
||||
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
|
||||
QStringLiteral(" Distance: ") + QString::number(Distance.value()) +
|
||||
QStringLiteral(" Calories: ") + QString::number(KCal.value()) +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
|
||||
void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
@@ -156,6 +272,17 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
|
||||
|
||||
qDebug() << QStringLiteral(" << ") << characteristic.uuid() << " " << newValue.toHex(' ');
|
||||
|
||||
// Handle Concept2 PM5 characteristics as fallback when FTMS is not available
|
||||
if (PM5 && (characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060031-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060032-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060033-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060035-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060036-43e5-11e4-916c-0800200c9a66")))) {
|
||||
|
||||
parseConcept2Data(characteristic, newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (characteristic.uuid() != QBluetoothUuid((quint16)0x2AD1)) {
|
||||
return;
|
||||
}
|
||||
@@ -569,6 +696,36 @@ void ftmsrower::serviceScanDone(void) {
|
||||
#endif
|
||||
|
||||
auto services_list = m_control->services();
|
||||
bool hasFTMSService = false;
|
||||
bool hasConcept2Services = false;
|
||||
|
||||
// Check if FTMS service (0x1826) is available
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
if (s == ftmsService) {
|
||||
hasFTMSService = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no FTMS service, check for Concept2 PM5 services
|
||||
if (!hasFTMSService && PM5) {
|
||||
QBluetoothUuid concept2InfoService(QStringLiteral("ce060010-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2ControlService(QStringLiteral("ce060020-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2RowingService(QStringLiteral("ce060030-43e5-11e4-916c-0800200c9a66"));
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
if (s == concept2InfoService || s == concept2ControlService || s == concept2RowingService) {
|
||||
hasConcept2Services = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConcept2Services) {
|
||||
emit debug(QStringLiteral("PM5 without FTMS service detected, using Concept2 protocol"));
|
||||
}
|
||||
}
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
@@ -619,6 +776,9 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) {
|
||||
PM5 = true;
|
||||
qDebug() << "PM5 found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("NORDLYS"))) {
|
||||
NORDLYS = true;
|
||||
qDebug() << "NORDLYS found!";
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
|
||||
@@ -46,6 +46,7 @@ class ftmsrower : public rower {
|
||||
void startDiscover();
|
||||
uint16_t watts() override;
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void parseConcept2Data(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
@@ -69,6 +70,7 @@ class ftmsrower : public rower {
|
||||
bool WHIPR = false;
|
||||
bool KINGSMITH = false;
|
||||
bool PM5 = false;
|
||||
bool NORDLYS = false;
|
||||
|
||||
bool WATER_ROWER = false;
|
||||
bool DFIT_L_R = false;
|
||||
@@ -76,6 +78,9 @@ class ftmsrower : public rower {
|
||||
bool ROWER = false;
|
||||
QDateTime lastStroke = QDateTime::currentDateTime();
|
||||
double lastStrokesCount = 0;
|
||||
|
||||
// PM5 specific variables
|
||||
uint8_t pm5RowState = 0;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
@@ -972,7 +972,7 @@ void horizontreadmill::update() {
|
||||
forceIncline(requestInclination);
|
||||
|
||||
// this treadmill doesn't send the incline, so i'm forcing it manually
|
||||
if(SW_TREADMILL) {
|
||||
if(SW_TREADMILL || mobvoi_treadmill) {
|
||||
Inclination = requestInclination;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,15 +77,13 @@ void kineticinroadbike::writeCharacteristic(uint8_t *data, uint8_t data_len, con
|
||||
}
|
||||
|
||||
void kineticinroadbike::forceResistance(resistance_t requestResistance) {
|
||||
/*uint8_t noOpData[] = {0xf0, 0xb1, 0x01, 0x00, 0x00};
|
||||
|
||||
noOpData[3] = requestResistance;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
|
||||
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
|
||||
}
|
||||
|
||||
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("force resistance"), false, true);*/
|
||||
if (noWriteResistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use Smart Control brake mode for direct resistance control
|
||||
smart_control_set_mode_brake_data cmd = smart_control_set_mode_brake_command(requestResistance);
|
||||
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set brake resistance"), false, true);
|
||||
}
|
||||
|
||||
void kineticinroadbike::update() {
|
||||
@@ -108,6 +106,23 @@ void kineticinroadbike::update() {
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
if (requestInclination != -100) {
|
||||
qDebug() << QStringLiteral("writing inclination ") + QString::number(requestInclination);
|
||||
forceInclination(requestInclination);
|
||||
if(!VirtualBike() || (VirtualBike() && !VirtualBike()->ftmsDeviceConnected())) {
|
||||
Inclination = requestInclination;
|
||||
}
|
||||
requestInclination = -100;
|
||||
requestResistance = -1; // Clear resistance request when handling inclination
|
||||
}
|
||||
|
||||
if (requestPower != -1) {
|
||||
qDebug() << QStringLiteral("writing power ") + QString::number(requestPower);
|
||||
changePower(requestPower);
|
||||
requestPower = -1;
|
||||
requestResistance = -1; // Clear resistance request when handling power
|
||||
}
|
||||
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance > max_resistance)
|
||||
requestResistance = max_resistance;
|
||||
@@ -120,6 +135,7 @@ void kineticinroadbike::update() {
|
||||
}
|
||||
requestResistance = -1;
|
||||
}
|
||||
|
||||
if (requestStart != -1) {
|
||||
qDebug() << QStringLiteral("starting...");
|
||||
|
||||
@@ -136,6 +152,42 @@ void kineticinroadbike::update() {
|
||||
}
|
||||
}
|
||||
|
||||
void kineticinroadbike::changePower(int32_t power) {
|
||||
if (noWriteResistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
RequestedPower = power;
|
||||
|
||||
if (power < 0)
|
||||
power = 0;
|
||||
|
||||
// Use Smart Control ERG mode for power-based training
|
||||
smart_control_set_mode_erg_data cmd = smart_control_set_mode_erg_command((uint16_t)power);
|
||||
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set ERG power"), false, true);
|
||||
}
|
||||
|
||||
void kineticinroadbike::forceInclination(double inclination) {
|
||||
if (noWriteResistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update resistance to match inclination (for UI consistency)
|
||||
Resistance = inclination;
|
||||
|
||||
// Smart Control simulation parameters
|
||||
QSettings settings;
|
||||
float weightKG = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
float rollingCoeff = 0.004f; // Standard rolling resistance for asphalt
|
||||
float windCoeff = 0.6f; // Standard wind resistance coefficient
|
||||
float windSpeedMPS = 0.0f; // No headwind/tailwind
|
||||
|
||||
// Use Smart Control simulation mode for inclination control
|
||||
smart_control_set_mode_simulation_data cmd = smart_control_set_mode_simulation_command(
|
||||
weightKG, rollingCoeff, windCoeff, (float)inclination, windSpeedMPS);
|
||||
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set simulation inclination"), false, true);
|
||||
}
|
||||
|
||||
void kineticinroadbike::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
|
||||
}
|
||||
@@ -264,7 +316,7 @@ void kineticinroadbike::characteristicChanged(const QLowEnergyCharacteristic &ch
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -44,10 +44,11 @@ class kineticinroadbike : public bike {
|
||||
resistance_t pelotonToBikeResistance(int pelotonResistance) override;
|
||||
resistance_t maxResistance() override { return max_resistance; }
|
||||
resistance_t resistanceFromPowerRequest(uint16_t power) override;
|
||||
void changePower(int32_t power) override;
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
const resistance_t max_resistance = 32;
|
||||
const resistance_t max_resistance = 999;
|
||||
double bikeResistanceToPeloton(double resistance);
|
||||
double GetDistanceFromPacket(const QByteArray &packet);
|
||||
QTime GetElapsedFromPacket(const QByteArray &packet);
|
||||
@@ -56,6 +57,7 @@ class kineticinroadbike : public bike {
|
||||
bool wait_for_response = false);
|
||||
void startDiscover();
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void forceInclination(double inclination);
|
||||
uint16_t watts() override;
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
@@ -146,6 +146,8 @@ void kingsmithr2treadmill::update() {
|
||||
debug("creating virtual treadmill interface...");
|
||||
auto virtualTreadMill = new virtualtreadmill(this, noHeartService);
|
||||
connect(virtualTreadMill, &virtualtreadmill::debug, this, &kingsmithr2treadmill::debug);
|
||||
connect(virtualTreadMill, &virtualtreadmill::changeInclination, this,
|
||||
&kingsmithr2treadmill::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
debug("creating virtual bike interface...");
|
||||
@@ -446,7 +448,7 @@ void kingsmithr2treadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId((quint16)0xFED7);
|
||||
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xFED8);
|
||||
|
||||
if (KS_NACH_X21C || KS_NGCH_G1C_2) {
|
||||
if (KS_NACH_X21C || KS_NGCH_G1C_2 || KS_HDSY_X21C_2) {
|
||||
_gattWriteCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED7-0000-1000-8000-00805f9b34fb"));
|
||||
_gattNotifyCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED8-0000-1000-8000-00805f9b34fb"));
|
||||
} else if (KS_NGCH_G1C || KS_NACH_MXG || KS_NACH_X21C_2) {
|
||||
@@ -515,6 +517,12 @@ void kingsmithr2treadmill::serviceScanDone(void) {
|
||||
qDebug() << "KS_NACH_X21C default service id not found";
|
||||
_gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("00011234-0000-1000-8000-00805f9b34fb"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
} else if(gattCommunicationChannelService == nullptr && KS_HDSY_X21C) {
|
||||
KS_HDSY_X21C_2 = true;
|
||||
KS_HDSY_X21C = false;
|
||||
qDebug() << "KS_HDSY_X21C default service id not found";
|
||||
_gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("00021234-0000-1000-8000-00805f9b34fb"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
} else if(gattCommunicationChannelService == nullptr && KS_NGCH_G1C) {
|
||||
KS_NGCH_G1C_2 = true;
|
||||
KS_NGCH_G1C = false;
|
||||
@@ -548,6 +556,9 @@ void kingsmithr2treadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
|
||||
if (device.name().toUpper().startsWith(QStringLiteral("KS-NACH-X21C"))) {
|
||||
qDebug() << "KS-NACH-X21C workaround!";
|
||||
KS_NACH_X21C = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("KS-HDSY-X21C"))) {
|
||||
qDebug() << "KS-HDSY-X21C workaround!";
|
||||
KS_HDSY_X21C = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("KS-NGCH-G1C"))) {
|
||||
qDebug() << "KS-NGCH-G1C workaround!";
|
||||
KS_NGCH_G1C = true;
|
||||
|
||||
@@ -98,6 +98,8 @@ class kingsmithr2treadmill : public treadmill {
|
||||
|
||||
bool KS_NACH_X21C = false;
|
||||
bool KS_NACH_X21C_2 = false;
|
||||
bool KS_HDSY_X21C = false;
|
||||
bool KS_HDSY_X21C_2 = false;
|
||||
bool KS_NGCH_G1C = false;
|
||||
bool KS_NGCH_G1C_2 = false;
|
||||
bool KS_NACH_MXG = false;
|
||||
|
||||
@@ -300,6 +300,7 @@ void nordictrackifitadbbike::processPendingDatagrams() {
|
||||
settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote)
|
||||
.toBool();
|
||||
double inclination_delay_seconds = settings.value(QZSettings::inclination_delay_seconds, QZSettings::default_inclination_delay_seconds).toDouble();
|
||||
bool proform_tdf_10_0 = settings.value(QZSettings::proform_tdf_10_0, QZSettings::default_proform_tdf_10_0).toBool();
|
||||
|
||||
// only resistance
|
||||
if(proform_studio_NTEX71021 || nordictrackadbbike_resistance) {
|
||||
@@ -310,14 +311,21 @@ void nordictrackifitadbbike::processPendingDatagrams() {
|
||||
int y2 = (int)(493 - (13.57 * (requestResistance - 1)));
|
||||
int y1Resistance = (int)(493 - (13.57 * currentResistance().value()));
|
||||
|
||||
if(!proform_studio_NTEX71021) { // s22i default
|
||||
if(proform_tdf_10_0) {
|
||||
x1 = 1175;
|
||||
y2 = (int)(590 - (15.91 * requestResistance));
|
||||
y1Resistance = (int)(590 - (15.91 * currentResistance().value()));
|
||||
Resistance = requestResistance;
|
||||
emit resistanceRead(Resistance.value());
|
||||
}
|
||||
else if(!proform_studio_NTEX71021) { // s22i default
|
||||
x1 = 1920 - 75;
|
||||
y2 = (int)(803 - (23.777 * requestResistance));
|
||||
y1Resistance = (int)(803 - (23.777 * currentResistance().value()));
|
||||
Resistance = requestResistance;
|
||||
emit resistanceRead(Resistance.value());
|
||||
}
|
||||
|
||||
|
||||
lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " +
|
||||
QString::number(x1) + " " + QString::number(y2) + " 200";
|
||||
qDebug() << " >> " + lastCommand;
|
||||
@@ -358,7 +366,6 @@ void nordictrackifitadbbike::processPendingDatagrams() {
|
||||
double inc = qRound(requestInclination / 0.5) * 0.5;
|
||||
if (inc != currentInclination().value()) {
|
||||
bool proform_studio = settings.value(QZSettings::proform_studio, QZSettings::default_proform_studio).toBool();
|
||||
bool proform_tdf_10_0 = settings.value(QZSettings::proform_tdf_10_0, QZSettings::default_proform_tdf_10_0).toBool();
|
||||
int x1 = 75;
|
||||
int y2 = (int)(616.18 - (17.223 * (inc + gears())));
|
||||
int y1Resistance = (int)(616.18 - (17.223 * currentInclination().value()));
|
||||
|
||||
@@ -470,7 +470,7 @@ void nordictrackifitadbelliptical::processPendingDatagrams() {
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadencep && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
503
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp
Normal file
503
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp
Normal file
@@ -0,0 +1,503 @@
|
||||
#include "nordictrackifitadbrower.h"
|
||||
#include "qzsettings.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
#include <chrono>
|
||||
#include <math.h>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
nordictrackifitadbrowerLogcatAdbThread::nordictrackifitadbrowerLogcatAdbThread(QString s) { Q_UNUSED(s) }
|
||||
|
||||
void nordictrackifitadbrowerLogcatAdbThread::run() {
|
||||
QSettings settings;
|
||||
QString ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
runAdbCommand("connect " + ip);
|
||||
|
||||
while (1) {
|
||||
runAdbTailCommand("logcat");
|
||||
if(adbCommandPending.length() != 0) {
|
||||
runAdbCommand(adbCommandPending);
|
||||
adbCommandPending = "";
|
||||
}
|
||||
msleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
QString nordictrackifitadbrowerLogcatAdbThread::runAdbCommand(QString command) {
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QProcess process;
|
||||
emit debug("adb >> " + command);
|
||||
process.start("adb/adb.exe", QStringList(command.split(' ')));
|
||||
process.waitForFinished(-1); // will wait forever until finished
|
||||
|
||||
QString out = process.readAllStandardOutput();
|
||||
QString err = process.readAllStandardError();
|
||||
|
||||
emit debug("adb << OUT " + out);
|
||||
emit debug("adb << ERR" + err);
|
||||
#else
|
||||
QString out;
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
bool nordictrackifitadbrowerLogcatAdbThread::runCommand(QString command) {
|
||||
if(adbCommandPending.length() == 0) {
|
||||
adbCommandPending = command;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void nordictrackifitadbrowerLogcatAdbThread::runAdbTailCommand(QString command) {
|
||||
#ifdef Q_OS_WINDOWS
|
||||
auto process = new QProcess;
|
||||
QObject::connect(process, &QProcess::readyReadStandardOutput, [process, this]() {
|
||||
QString output = process->readAllStandardOutput();
|
||||
// qDebug() << "adbLogCat STDOUT << " << output;
|
||||
QStringList lines = output.split('\n', Qt::SplitBehaviorFlags::SkipEmptyParts);
|
||||
bool wattFound = false;
|
||||
bool hrmFound = false;
|
||||
bool cadenceFound = false;
|
||||
bool resistanceFound = false;
|
||||
foreach (QString line, lines) {
|
||||
if (line.contains("Changed KPH") || line.contains("Changed Actual KPH")) {
|
||||
emit debug(line);
|
||||
speed = line.split(' ').last().toDouble();
|
||||
} else if (line.contains("Changed Resistance")) {
|
||||
emit debug(line);
|
||||
resistance = line.split(' ').last().toDouble();
|
||||
resistanceFound = true;
|
||||
} else if (line.contains("Changed RPM")) {
|
||||
emit debug(line);
|
||||
cadence = line.split(' ').last().toDouble();
|
||||
cadenceFound = true;
|
||||
} else if (line.contains("Changed Watts")) {
|
||||
emit debug(line);
|
||||
watt = line.split(' ').last().toDouble();
|
||||
wattFound = true;
|
||||
} else if (line.contains("HeartRateDataUpdate")) {
|
||||
emit debug(line);
|
||||
QStringList splitted = line.split(' ', Qt::SkipEmptyParts);
|
||||
if (splitted.length() > 14) {
|
||||
hrm = splitted[14].toInt();
|
||||
hrmFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
emit onSpeedResistance(speed, resistance);
|
||||
if (cadenceFound)
|
||||
emit onCadence(cadence);
|
||||
if (wattFound)
|
||||
emit onWatt(watt);
|
||||
if (hrmFound)
|
||||
emit onHRM(hrm);
|
||||
#ifdef Q_OS_WINDOWS
|
||||
if(adbCommandPending.length() != 0) {
|
||||
runAdbCommand(adbCommandPending);
|
||||
adbCommandPending = "";
|
||||
}
|
||||
#endif
|
||||
});
|
||||
QObject::connect(process, &QProcess::readyReadStandardError, [process, this]() {
|
||||
auto output = process->readAllStandardError();
|
||||
emit debug("adbLogCat ERROR << " + output);
|
||||
});
|
||||
emit debug("adbLogCat >> " + command);
|
||||
process->start("adb/adb.exe", QStringList(command.split(' ')));
|
||||
process->waitForFinished(-1);
|
||||
#endif
|
||||
}
|
||||
|
||||
nordictrackifitadbrower::nordictrackifitadbrower(bool noWriteResistance, bool noHeartService,
|
||||
int8_t bikeResistanceOffset, double bikeResistanceGain) {
|
||||
QSettings settings;
|
||||
bool nordictrack_ifit_adb_remote =
|
||||
settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote)
|
||||
.toBool();
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
initDone = false;
|
||||
connect(refresh, &QTimer::timeout, this, &nordictrackifitadbrower::update);
|
||||
ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
refresh->start(200ms);
|
||||
|
||||
socket = new QUdpSocket(this);
|
||||
bool result = socket->bind(QHostAddress::AnyIPv4, 8002);
|
||||
qDebug() << result;
|
||||
processPendingDatagrams();
|
||||
connect(socket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams()));
|
||||
|
||||
initRequest = true;
|
||||
|
||||
// ******************************************* virtual device init *************************************
|
||||
if (!firstStateChanged && !this->hasVirtualDevice()) {
|
||||
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();
|
||||
bool virtual_device_force_bike =
|
||||
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool();
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && !virtual_device_rower) {
|
||||
qDebug() << "ios_peloton_workaround activated!";
|
||||
h = new lockscreen();
|
||||
h->virtualbike_ios();
|
||||
} else
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
if (virtual_device_rower) {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
// connect(virtualRower,&virtualrower::debug ,this,&nordictrackifitadbrower::debug);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else if (virtual_device_force_bike) {
|
||||
qDebug() << QStringLiteral("creating virtual bike interface...");
|
||||
auto virtualBike =
|
||||
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&nordictrackifitadbrower::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
// connect(virtualRower,&virtualrower::debug ,this,&nordictrackifitadbrower::debug);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
if (nordictrack_ifit_adb_remote) {
|
||||
#ifdef Q_OS_ANDROID
|
||||
QAndroidJniObject IP = QAndroidJniObject::fromString(ip).object<jstring>();
|
||||
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/QZAdbRemote", "createConnection",
|
||||
"(Ljava/lang/String;Landroid/content/Context;)V",
|
||||
IP.object<jstring>(), QtAndroid::androidContext().object());
|
||||
#elif defined Q_OS_WIN
|
||||
logcatAdbThread = new nordictrackifitadbrowerLogcatAdbThread("logcatAdbThread");
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onCadence, this,
|
||||
&nordictrackifitadbrower::onCadence);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onSpeedResistance, this,
|
||||
&nordictrackifitadbrower::onSpeedResistance);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onWatt, this,
|
||||
&nordictrackifitadbrower::onWatt);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onHRM, this, &nordictrackifitadbrower::onHRM);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::debug, this, &nordictrackifitadbrower::debug);
|
||||
logcatAdbThread->start();
|
||||
#elif defined Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
h->adb_connect(ip.toStdString().c_str());
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::onSpeedResistance(double speed, double resistance) {
|
||||
if(speed > 0)
|
||||
speedReadFromTM = true;
|
||||
Speed = speed;
|
||||
Resistance = resistance;
|
||||
resistanceReadFromTM = true;
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::onWatt(double watt) {
|
||||
m_watt = watt;
|
||||
wattReadFromTM = true;
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::onCadence(double cadence) {
|
||||
Cadence = cadence;
|
||||
cadenceReadFromTM = true;
|
||||
}
|
||||
|
||||
double nordictrackifitadbrower::getDouble(QString v) {
|
||||
QChar d = QLocale().decimalPoint();
|
||||
if (d == ',') {
|
||||
v = v.replace('.', ',');
|
||||
}
|
||||
return QLocale().toDouble(v);
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::processPendingDatagrams() {
|
||||
qDebug() << "in !";
|
||||
QHostAddress sender;
|
||||
QSettings settings;
|
||||
uint16_t port;
|
||||
while (socket->hasPendingDatagrams()) {
|
||||
QByteArray datagram;
|
||||
datagram.resize(socket->pendingDatagramSize());
|
||||
socket->readDatagram(datagram.data(), datagram.size(), &sender, &port);
|
||||
lastSender = sender;
|
||||
qDebug() << "Message From :: " << sender.toString();
|
||||
qDebug() << "Port From :: " << port;
|
||||
qDebug() << "Message :: " << datagram;
|
||||
|
||||
QString ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
|
||||
double speed = 0;
|
||||
double cadence = 0;
|
||||
double resistance = 0;
|
||||
double gear = 0;
|
||||
double watt = 0;
|
||||
QStringList lines = QString::fromLocal8Bit(datagram.data()).split("\n");
|
||||
foreach (QString line, lines) {
|
||||
qDebug() << line;
|
||||
|
||||
if (line.contains(QStringLiteral("Changed KPH")) && !settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
QStringList aValues = line.split(" ");
|
||||
if (aValues.length()) {
|
||||
speedReadFromTM = true;
|
||||
speed = getDouble(aValues.last());
|
||||
Speed = speed;
|
||||
}
|
||||
} else if (line.contains(QStringLiteral("Changed RPM"))) {
|
||||
QStringList aValues = line.split(" ");
|
||||
if (aValues.length()) {
|
||||
cadence = getDouble(aValues.last());
|
||||
Cadence = cadence;
|
||||
cadenceReadFromTM = true;
|
||||
if(!speedReadFromTM) {
|
||||
Speed = Cadence.value() *
|
||||
settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio)
|
||||
.toDouble();
|
||||
}
|
||||
}
|
||||
} else if (line.contains(QStringLiteral("Changed CurrentGear"))) {
|
||||
QStringList aValues = line.split(" ");
|
||||
if (aValues.length()) {
|
||||
gear = getDouble(aValues.last());
|
||||
Resistance = gear;
|
||||
gearsAvailable = true;
|
||||
}
|
||||
} else if (line.contains(QStringLiteral("Changed Resistance"))) {
|
||||
QStringList aValues = line.split(" ");
|
||||
if (aValues.length()) {
|
||||
resistance = getDouble(aValues.last());
|
||||
m_pelotonResistance = (100 / 32) * resistance; // adjusted for rower resistance range
|
||||
qDebug() << QStringLiteral("Current Peloton Resistance: ") << m_pelotonResistance.value()
|
||||
<< resistance;
|
||||
if(!gearsAvailable) {
|
||||
Resistance = resistance;
|
||||
resistanceReadFromTM = true;
|
||||
}
|
||||
}
|
||||
} else if (line.contains(QStringLiteral("Changed Watts"))) {
|
||||
QStringList aValues = line.split(" ");
|
||||
if (aValues.length()) {
|
||||
watt = getDouble(aValues.last());
|
||||
m_watt = watt;
|
||||
wattReadFromTM = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), 0, Speed.value(), // no inclination for rower
|
||||
fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), 20);
|
||||
}
|
||||
|
||||
bool nordictrack_ifit_adb_remote =
|
||||
settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote)
|
||||
.toBool();
|
||||
|
||||
// resistance
|
||||
if (nordictrack_ifit_adb_remote) {
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance != currentResistance().value()) {
|
||||
int x1 = 1205; // Estimated x-coordinate of the resistance slider (right side)
|
||||
int y2 = (int)(590 - (15.65 * requestResistance));
|
||||
int y1Resistance = (int)(590 - (15.65 * currentResistance().value()));
|
||||
|
||||
lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " +
|
||||
QString::number(x1) + " " + QString::number(y2) + " 200";
|
||||
qDebug() << " >> " + lastCommand;
|
||||
#ifdef Q_OS_ANDROID
|
||||
QAndroidJniObject command = QAndroidJniObject::fromString(lastCommand).object<jstring>();
|
||||
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/QZAdbRemote",
|
||||
"sendCommand", "(Ljava/lang/String;)V",
|
||||
command.object<jstring>());
|
||||
#elif defined(Q_OS_WIN)
|
||||
if (logcatAdbThread)
|
||||
logcatAdbThread->runCommand("shell " + lastCommand);
|
||||
#elif defined Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
h->adb_sendcommand(lastCommand.toStdString().c_str());
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
requestResistance = -1;
|
||||
} else {
|
||||
QByteArray message = (QString::number(requestResistance).toLocal8Bit()) + ";";
|
||||
requestResistance = -1;
|
||||
int ret = socket->writeDatagram(message, message.size(), sender, 8003);
|
||||
qDebug() << QString::number(ret) + " >> " + message;
|
||||
}
|
||||
|
||||
if (watts())
|
||||
KCal +=
|
||||
((((0.048 * ((double)watts()) + 1.19) * weight * 3.5) / 200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
|
||||
QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
|
||||
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
|
||||
#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"))) {
|
||||
update_hr_from_external();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadencep =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadencep && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value()));
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
|
||||
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
|
||||
emit debug(QStringLiteral("Current Gear: ") + QString::number(gear));
|
||||
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
|
||||
emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value()));
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::onHRM(int hrm) {
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
bool disable_hr_frommachinery =
|
||||
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
|
||||
|
||||
if (
|
||||
#ifdef Q_OS_ANDROID
|
||||
(!settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) &&
|
||||
#endif
|
||||
heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery) {
|
||||
|
||||
Heart = hrm;
|
||||
emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value()));
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::forceResistance(double resistance) {}
|
||||
|
||||
void nordictrackifitadbrower::update() {
|
||||
|
||||
QSettings settings;
|
||||
update_metrics(false, 0);
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
// updating the rower console every second
|
||||
if (sec1Update++ == (500 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
if (requestStart != -1) {
|
||||
emit debug(QStringLiteral("starting..."));
|
||||
|
||||
// btinit();
|
||||
|
||||
requestStart = -1;
|
||||
}
|
||||
if (requestStop != -1) {
|
||||
emit debug(QStringLiteral("stopping..."));
|
||||
// writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
|
||||
requestStop = -1;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t nordictrackifitadbrower::watts() {
|
||||
// If we have watts from the machine, use them
|
||||
if (wattReadFromTM && m_watt.value() > 0) {
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
// Otherwise calculate watts from resistance and cadence
|
||||
return wattsFromResistance(currentResistance().value(), currentCadence().value());
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::changeResistanceRequested(double resistance) {
|
||||
if (resistance < 0)
|
||||
resistance = 0;
|
||||
changeResistance(resistance);
|
||||
}
|
||||
|
||||
bool nordictrackifitadbrower::connected() { return true; }
|
||||
|
||||
uint16_t nordictrackifitadbrower::wattsFromResistance(double resistance, double cadence) {
|
||||
// Rower power estimation based on resistance and cadence
|
||||
// This formula is based on general rowing power curves
|
||||
// Power increases with both resistance and cadence (stroke rate)
|
||||
|
||||
if (cadence <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Basic power formula for rowing: power increases exponentially with stroke rate
|
||||
// and linearly with resistance level
|
||||
double basePower = resistance * 8; // Base power per resistance level
|
||||
double cadenceFactor = 1.0 + (cadence - 20.0) * 0.05; // Cadence multiplier
|
||||
|
||||
// Additional exponential component for higher stroke rates (similar to real rowing)
|
||||
double exponentialFactor = exp(cadence * 0.015);
|
||||
|
||||
double power = basePower * cadenceFactor * (exponentialFactor / 10.0);
|
||||
|
||||
// Ensure minimum and maximum bounds
|
||||
if (power < 10) power = 10;
|
||||
if (power > 500) power = 500; // Reasonable max for most users
|
||||
|
||||
return (uint16_t)power;
|
||||
}
|
||||
126
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h
Normal file
126
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h
Normal file
@@ -0,0 +1,126 @@
|
||||
#ifndef NORDICTRACKIFITADBROWER_H
|
||||
#define NORDICTRACKIFITADBROWER_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 <QThread>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include "devices/rower.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class nordictrackifitadbrowerLogcatAdbThread : public QThread {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit nordictrackifitadbrowerLogcatAdbThread(QString s);
|
||||
bool runCommand(QString command);
|
||||
|
||||
void run() override;
|
||||
|
||||
signals:
|
||||
void onSpeedResistance(double speed, double resistance);
|
||||
void debug(QString message);
|
||||
void onWatt(double watt);
|
||||
void onHRM(int hrm);
|
||||
void onCadence(double cadence);
|
||||
|
||||
private:
|
||||
QString adbCommandPending = "";
|
||||
QString runAdbCommand(QString command);
|
||||
double speed = 0;
|
||||
double resistance = 0;
|
||||
double cadence = 0;
|
||||
double watt = 0;
|
||||
int hrm = 0;
|
||||
QString name;
|
||||
struct adbfile {
|
||||
QDateTime date;
|
||||
QString name;
|
||||
};
|
||||
|
||||
void runAdbTailCommand(QString command);
|
||||
};
|
||||
|
||||
class nordictrackifitadbrower : public rower {
|
||||
Q_OBJECT
|
||||
public:
|
||||
nordictrackifitadbrower(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
const resistance_t max_resistance = 32; // max resistance for rower
|
||||
void forceResistance(double resistance);
|
||||
uint16_t watts() override;
|
||||
double getDouble(QString v);
|
||||
uint16_t wattsFromResistance(double resistance, double cadence);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
uint8_t sec1Update = 0;
|
||||
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
QDateTime lastResistanceChanged = QDateTime::currentDateTime();
|
||||
uint8_t firstStateChanged = 0;
|
||||
uint16_t m_watts = 0;
|
||||
bool cadenceReadFromTM = false;
|
||||
bool resistanceReadFromTM = false;
|
||||
bool wattReadFromTM = false;
|
||||
bool speedReadFromTM = false;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
|
||||
bool gearsAvailable = false;
|
||||
|
||||
QUdpSocket *socket = nullptr;
|
||||
QHostAddress lastSender;
|
||||
|
||||
nordictrackifitadbrowerLogcatAdbThread *logcatAdbThread = nullptr;
|
||||
|
||||
QString lastCommand;
|
||||
|
||||
QString ip;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
|
||||
private slots:
|
||||
|
||||
void processPendingDatagrams();
|
||||
void changeResistanceRequested(double resistance);
|
||||
void onHRM(int hrm);
|
||||
void onWatt(double watt);
|
||||
void onCadence(double cadence);
|
||||
void onSpeedResistance(double speed, double resistance);
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
#endif // NORDICTRACKIFITADBROWER_H
|
||||
@@ -196,6 +196,19 @@ void paferstreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
|
||||
|
||||
emit packetReceived();
|
||||
|
||||
if (newValue.length() == 4 && value.at(0) == 0x55 && value.at(1) == 0x09 && value.at(2) == 0x01 &&
|
||||
value.at(3) == 0x01) {
|
||||
qDebug() << "Paferstreadmill: pressing start button";
|
||||
emit tapeStarted();
|
||||
requestStart = 1;
|
||||
return;
|
||||
} else if (newValue.length() == 4 && value.at(0) == 0x55 && value.at(1) == 0x09 && value.at(2) == 0x01 &&
|
||||
value.at(3) == 0x00) {
|
||||
qDebug() << "Paferstreadmill: pressing stop button";
|
||||
requestStop = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((newValue.length() != 13))
|
||||
return;
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ void pitpatbike::characteristicChanged(const QLowEnergyCharacteristic &character
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -56,26 +56,41 @@ void proformbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt
|
||||
}
|
||||
|
||||
resistance_t proformbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
QSettings settings;
|
||||
|
||||
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();
|
||||
|
||||
if (proform_225_csx_PFEX32925_INT_0) {
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
resistance_t best_resistance_match = 1;
|
||||
int min_watt_difference = 1000;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; i++) {
|
||||
if (((wattsFromResistance(i) * watt_gain) + watt_offset) <= power &&
|
||||
((wattsFromResistance(i + 1) * watt_gain) + watt_offset) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest")
|
||||
<< ((wattsFromResistance(i) * watt_gain) + watt_offset)
|
||||
<< ((wattsFromResistance(i + 1) * watt_gain) + watt_offset) << power;
|
||||
uint16_t current_watts = (wattsFromResistance(i) * watt_gain) + watt_offset;
|
||||
uint16_t next_watts = (wattsFromResistance(i + 1) * watt_gain) + watt_offset;
|
||||
|
||||
if (current_watts <= power && next_watts >= power) {
|
||||
qDebug() << current_watts << next_watts << power;
|
||||
return i;
|
||||
}
|
||||
|
||||
int diff = abs(current_watts - power);
|
||||
if (diff < min_watt_difference) {
|
||||
min_watt_difference = diff;
|
||||
best_resistance_match = i;
|
||||
qDebug() << QStringLiteral("best match") << best_resistance_match << "with watts" << current_watts << "diff" << diff;
|
||||
}
|
||||
}
|
||||
if (power < ((wattsFromResistance(1) * watt_gain) + watt_offset))
|
||||
return 1;
|
||||
else
|
||||
return max_resistance;
|
||||
|
||||
qDebug() << "Bracketing not found, best match:" << best_resistance_match;
|
||||
return best_resistance_match;
|
||||
}
|
||||
|
||||
uint16_t proformbike::wattsFromResistance(resistance_t resistance) {
|
||||
@@ -865,7 +880,7 @@ void proformbike::update() {
|
||||
uint8_t noOpData5_proform_bike_PFEVEX71316_0[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x08, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x81, 0x0e, 0x41, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80};
|
||||
|
||||
uint8_t noOpData6_proform_bike_PFEVEX71316_0[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x80, 0x3a, 0x00, 0x00, 0x00,
|
||||
uint8_t noOpData6_proform_bike_PFEVEX71316_0[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xaa, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
uint8_t noOpData2_proform_bike_325_csx[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, 0x0d, 0x3c, 0x9e, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80};
|
||||
@@ -1185,7 +1200,9 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
(newValue.at(0) != 0x00 && newValue.at(0) != 0x01) || newValue.at(1) != 0x12 ||
|
||||
(newValue.at(0) == 0x00 &&
|
||||
(newValue.at(2) != 0x01 || newValue.at(3) != 0x04 || newValue.at(4) != 0x02 || (proform_bike_PFEVEX71316_0 ? newValue.at(5) != 0x30 : newValue.at(5) != 0x2c))) ||
|
||||
(proform_bike_PFEVEX71316_0 && (uint8_t)newValue.at(2) == 0xFF && (uint8_t)newValue.at(3) == 0xFF)) {
|
||||
(proform_bike_PFEVEX71316_0 && (uint8_t)newValue.at(2) == 0xFF && (uint8_t)newValue.at(3) == 0xFF) ||
|
||||
(proform_bike_PFEVEX71316_0 && (uint8_t)newValue.at(14) == 0xFF && (uint8_t)newValue.at(15) == 0xFF &&
|
||||
(uint8_t)newValue.at(16) == 0xFF && (uint8_t)newValue.at(17) == 0xFF)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -397,20 +397,5 @@ uint16_t proformtelnetbike::wattsFromResistance(resistance_t resistance) {
|
||||
}
|
||||
|
||||
resistance_t proformtelnetbike::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();
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ void proformtreadmill::forceIncline(double incline) {
|
||||
proform_treadmill_sport_8_5 || proform_treadmill_505_cst || proform_505_cst_80_44 || proform_carbon_tl || proform_proshox2 || nordictrack_s20i_treadmill || proform_595i_proshox2 ||
|
||||
proform_treadmill_8_7 || proform_carbon_tl_PFTL59720 || proform_treadmill_sport_70 || proform_treadmill_575i || proform_performance_300i || proform_performance_400i || proform_treadmill_c700 ||
|
||||
proform_treadmill_c960i || nordictrack_tseries5_treadmill || proform_carbon_tl_PFTL59722c || proform_treadmill_1500_pro || proform_trainer_8_0 || proform_treadmill_705_cst_V80_44 ||
|
||||
nordictrack_treadmill_ultra_le || proform_treadmill_carbon_tls
|
||||
nordictrack_treadmill_ultra_le || proform_treadmill_carbon_tls || proform_treadmill_995i
|
||||
) {
|
||||
write[14] = write[11] + write[12] + 0x12;
|
||||
} else if (!nordictrack_t65s_treadmill && !nordictrack_elite_800 && !nordictrack_t65s_treadmill_81_miles && !nordictrack_s30_treadmill && !nordictrack_s20_treadmill && !nordictrack_t65s_83_treadmill) {
|
||||
@@ -108,7 +108,7 @@ void proformtreadmill::forceSpeed(double speed) {
|
||||
proform_treadmill_sport_8_5 || proform_treadmill_505_cst || proform_505_cst_80_44 || proform_treadmill_705_cst || proform_treadmill_705_cst_V78_239 || proform_carbon_tl || proform_proshox2 || nordictrack_s20i_treadmill || proform_595i_proshox2 ||
|
||||
proform_treadmill_8_7 || proform_carbon_tl_PFTL59720 || proform_treadmill_sport_70 || proform_treadmill_575i || proform_performance_300i || proform_performance_400i || proform_treadmill_c700 ||
|
||||
proform_treadmill_c960i || nordictrack_tseries5_treadmill || proform_carbon_tl_PFTL59722c || proform_treadmill_1500_pro || proform_trainer_8_0 || proform_treadmill_705_cst_V80_44 ||
|
||||
nordictrack_treadmill_ultra_le || proform_treadmill_carbon_tls) {
|
||||
nordictrack_treadmill_ultra_le || proform_treadmill_carbon_tls || proform_treadmill_995i) {
|
||||
write[14] = write[11] + write[12] + 0x11;
|
||||
} else if (!nordictrack_t65s_treadmill && !nordictrack_elite_800 && !nordictrack_t65s_treadmill_81_miles && !nordictrack_s30_treadmill && !nordictrack_s20_treadmill && !nordictrack_t65s_83_treadmill) {
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
@@ -137,45 +137,6 @@ void proformtreadmill::update() {
|
||||
QSettings settings;
|
||||
update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
/*if (proform_treadmill_995i) {
|
||||
uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x80, 0x0a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData3[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x84, 0x74, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
switch (counterPoll) {
|
||||
case 0:
|
||||
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 1:
|
||||
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 2:
|
||||
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"));
|
||||
if (requestInclination != -100) {
|
||||
if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
|
||||
requestInclination <= 15) {
|
||||
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
|
||||
forceIncline(requestInclination);
|
||||
}
|
||||
requestInclination = -100;
|
||||
}
|
||||
if (requestSpeed != -1) {
|
||||
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
|
||||
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
|
||||
forceSpeed(requestSpeed);
|
||||
}
|
||||
requestSpeed = -1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
counterPoll++;
|
||||
if (counterPoll > 2) {
|
||||
counterPoll = 0;
|
||||
}
|
||||
} else*/
|
||||
if (proform_treadmill_9_0 || proform_treadmill_z1300i) {
|
||||
uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00,
|
||||
@@ -689,6 +650,70 @@ void proformtreadmill::update() {
|
||||
if (counterPoll > 5) {
|
||||
counterPoll = 0;
|
||||
}
|
||||
} else if (proform_treadmill_995i) {
|
||||
uint8_t noOpData1[] = {0xfe, 0x02, 0x14, 0x03};
|
||||
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x10, 0x04, 0x10, 0x02, 0x00,
|
||||
0x0a, 0x1b, 0x94, 0x30, 0x00, 0x00, 0x40, 0x50, 0x00, 0x80};
|
||||
uint8_t noOpData3[] = {0xff, 0x02, 0x18, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData4[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00,
|
||||
0x0f, 0x80, 0x0a, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData6[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x85, 0x00, 0x10, 0x8a, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
switch (counterPoll) {
|
||||
case 0:
|
||||
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 1:
|
||||
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 2:
|
||||
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"), false, true);
|
||||
if (requestInclination != -100) {
|
||||
if (requestInclination < 0)
|
||||
requestInclination = 0;
|
||||
if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
|
||||
requestInclination <= 15) {
|
||||
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
|
||||
forceIncline(requestInclination);
|
||||
}
|
||||
requestInclination = -100;
|
||||
}
|
||||
if (requestSpeed != -1) {
|
||||
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
|
||||
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
|
||||
forceSpeed(requestSpeed);
|
||||
}
|
||||
requestSpeed = -1;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 4:
|
||||
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 5:
|
||||
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp"));
|
||||
if (requestStart != -1) {
|
||||
emit debug(QStringLiteral("starting..."));
|
||||
requestStart = -1;
|
||||
emit tapeStarted();
|
||||
}
|
||||
if (requestStop != -1 || requestPause != -1) {
|
||||
forceSpeed(0);
|
||||
emit debug(QStringLiteral("stopping..."));
|
||||
requestStop = -1;
|
||||
requestPause = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
counterPoll++;
|
||||
if (counterPoll > 5) {
|
||||
counterPoll = 0;
|
||||
}
|
||||
} else if (nordictrackt70 || proform_treadmill_sport_8_5) {
|
||||
uint8_t noOpData1[] = {0xff, 0x05, 0x18, 0x00, 0x00, 0x01, 0x2f, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
@@ -3137,11 +3162,11 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
((nordictrack_t65s_treadmill || nordictrack_t65s_treadmill_81_miles || proform_pro_1000_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill ||
|
||||
nordictrack_s20_treadmill || proform_treadmill_se || proform_cadence_lt || proform_8_5_treadmill || nordictrack_treadmill_exp_5i || proform_carbon_tl ||
|
||||
nordictrack_s20i_treadmill || proform_treadmill_8_7 || proform_carbon_tl_PFTL59720 || proform_treadmill_575i || nordictrack_tseries5_treadmill ||
|
||||
proform_carbon_tl_PFTL59722c || proform_treadmill_1500_pro) &&
|
||||
proform_carbon_tl_PFTL59722c || proform_treadmill_1500_pro || proform_treadmill_995i) &&
|
||||
(newValue.at(4) != 0x02 || newValue.at(5) != 0x2e)) ||
|
||||
|
||||
((nordictrack_elite_800) &&
|
||||
(newValue.at(4) != 0x02 || newValue.at(5) != 0x2f)) ||
|
||||
((uint8_t)newValue.at(18)) == 0xFF && ((uint8_t)newValue.at(19)) == 0xFF) ||
|
||||
|
||||
(((uint8_t)newValue.at(12)) == 0xFF && ((uint8_t)newValue.at(13)) == 0xFF &&
|
||||
((uint8_t)newValue.at(14)) == 0xFF && ((uint8_t)newValue.at(15)) == 0xFF &&
|
||||
@@ -3276,89 +3301,119 @@ void proformtreadmill::btinit() {
|
||||
nordictrack_elite_800 = settings.value(QZSettings::nordictrack_elite_800, QZSettings::default_nordictrack_elite_800).toBool();
|
||||
nordictrack_treadmill_ultra_le = settings.value(QZSettings::nordictrack_treadmill_ultra_le, QZSettings::default_nordictrack_treadmill_ultra_le).toBool();
|
||||
proform_treadmill_carbon_tls = settings.value(QZSettings::proform_treadmill_carbon_tls, QZSettings::default_proform_treadmill_carbon_tls).toBool();
|
||||
proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i, QZSettings::default_proform_treadmill_995i).toBool();
|
||||
|
||||
if (proform_treadmill_995i) {
|
||||
// ProForm 995i initialization frames from pkt4658 to pkt4756 (all 25 frames)
|
||||
uint8_t initData1[4] = {0xfe, 0x02, 0x08, 0x02}; // pkt4658
|
||||
uint8_t initData2[20] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04,
|
||||
0x81, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4661
|
||||
uint8_t initData3[4] = {0xfe, 0x02, 0x08, 0x02}; // pkt4667
|
||||
uint8_t initData4[20] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04,
|
||||
0x80, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4670
|
||||
uint8_t initData5[4] = {0xfe, 0x02, 0x08, 0x02}; // pkt4676
|
||||
uint8_t initData6[20] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04,
|
||||
0x88, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4679
|
||||
uint8_t initData7[4] = {0xfe, 0x02, 0x0a, 0x02}; // pkt4684
|
||||
uint8_t initData8[20] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06,
|
||||
0x82, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4687
|
||||
uint8_t initData9[4] = {0xfe, 0x02, 0x0a, 0x02}; // pkt4694
|
||||
uint8_t initData10[20] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06,
|
||||
0x84, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4697
|
||||
uint8_t initData11[4] = {0xfe, 0x02, 0x08, 0x02}; // pkt4703
|
||||
uint8_t initData12[20] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04,
|
||||
0x95, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4706
|
||||
uint8_t initData13[4] = {0xfe, 0x02, 0x2c, 0x04}; // pkt4712
|
||||
uint8_t initData14[20] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28,
|
||||
0x90, 0x07, 0x01, 0xc8, 0x6c, 0x02, 0xae, 0x4c,
|
||||
0xf8, 0x9e, 0x4a, 0xe0}; // pkt4715
|
||||
uint8_t initData15[20] = {0x01, 0x12, 0x94, 0x4a, 0xf6, 0xa4, 0x70, 0x06,
|
||||
0xc2, 0x98, 0x5c, 0x12, 0xde, 0x9c, 0x48, 0x0e,
|
||||
0xfa, 0xb0, 0x64, 0x5a}; // pkt4718
|
||||
uint8_t initData16[20] = {0xff, 0x08, 0x06, 0xf4, 0xe0, 0x98, 0x02, 0x00,
|
||||
0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4721
|
||||
uint8_t initData17[4] = {0xfe, 0x02, 0x19, 0x03}; // pkt4726
|
||||
uint8_t initData18[20] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15,
|
||||
0x02, 0x00, 0x0f, 0x00, 0x10, 0x00, 0xd8, 0x1c,
|
||||
0x48, 0x00, 0x00, 0xe0}; // pkt4729
|
||||
uint8_t initData19[20] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x08,
|
||||
0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4732
|
||||
uint8_t initData20[4] = {0xfe, 0x02, 0x17, 0x03}; // pkt4739
|
||||
uint8_t initData21[20] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13,
|
||||
0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4742
|
||||
uint8_t initData22[20] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4745
|
||||
uint8_t initData23[4] = {0xfe, 0x02, 0x19, 0x03}; // pkt4750
|
||||
uint8_t initData24[20] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15,
|
||||
0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4753
|
||||
uint8_t initData25[20] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00,
|
||||
0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}; // pkt4756
|
||||
|
||||
// bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i,
|
||||
// QZSettings::default_proform_treadmill_995i).toBool();
|
||||
int sleepms = 400;
|
||||
|
||||
/*if (proform_treadmill_995i) {
|
||||
uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02};
|
||||
uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x80, 0x88,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x88, 0x90,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData5[] = {0xfe, 0x02, 0x0a, 0x02};
|
||||
uint8_t initData6[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x82, 0x00,
|
||||
0x00, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData7[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00,
|
||||
0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04};
|
||||
uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28, 0x90, 0x04,
|
||||
0x00, 0xc1, 0x58, 0xfd, 0x90, 0x31, 0xd0, 0x75, 0x28, 0xc1};
|
||||
uint8_t initData11[] = {0x01, 0x12, 0x78, 0x2d, 0xc0, 0x71, 0x20, 0xf5, 0x88, 0x41,
|
||||
0x18, 0xdd, 0x90, 0x51, 0x10, 0xd5, 0x88, 0x41, 0x38, 0xed};
|
||||
uint8_t initData12[] = {0xff, 0x08, 0xa0, 0x91, 0x40, 0x80, 0x02, 0x00, 0x00, 0x15,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x48, 0x00, 0x00, 0xe0};
|
||||
uint8_t noOpData3[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x62, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x0c,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData6[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init pkt4658"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init pkt4661"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init pkt4667"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init pkt4670"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init pkt4676"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init pkt4679"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init pkt4684"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init pkt4687"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init pkt4694"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init pkt4697"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init pkt4703"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init pkt4706"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData13, sizeof(initData13), QStringLiteral("init pkt4712"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData14, sizeof(initData14), QStringLiteral("init pkt4715"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData15, sizeof(initData15), QStringLiteral("init pkt4718"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData16, sizeof(initData16), QStringLiteral("init pkt4721"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init pkt4726"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData18, sizeof(initData18), QStringLiteral("init pkt4729"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData19, sizeof(initData19), QStringLiteral("init pkt4732"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData20, sizeof(initData20), QStringLiteral("init pkt4739"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData21, sizeof(initData21), QStringLiteral("init pkt4742"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false);
|
||||
writeCharacteristic(initData22, sizeof(initData22), QStringLiteral("init pkt4745"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
} else*/
|
||||
if (nordictrack10) {
|
||||
writeCharacteristic(initData23, sizeof(initData23), QStringLiteral("init pkt4750"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData24, sizeof(initData24), QStringLiteral("init pkt4753"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData25, sizeof(initData25), QStringLiteral("init pkt4756"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
} else if (nordictrack10) {
|
||||
uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02};
|
||||
uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
@@ -6158,12 +6213,12 @@ void proformtreadmill::btinit() {
|
||||
0x0f, 0x80, 0x08, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData31[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x80, 0x00, 0x10, 0x82, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData32[] = {0xfe, 0x02, 0x0d, 0x02};
|
||||
/*uint8_t initData32[] = {0xfe, 0x02, 0x0d, 0x02};
|
||||
uint8_t initData33[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x04, 0x09, 0x02, 0x02,
|
||||
0x00, 0x10, 0x04, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData34[] = {0xfe, 0x02, 0x0f, 0x02};
|
||||
uint8_t initData35[] = {0xff, 0x0f, 0x02, 0x04, 0x02, 0x0b, 0x04, 0x0b, 0x02, 0x02,
|
||||
0x02, 0x10, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 0x00, 0x00};
|
||||
0x02, 0x10, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 0x00, 0x00};*/
|
||||
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
@@ -6227,14 +6282,14 @@ void proformtreadmill::btinit() {
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData31, sizeof(initData31), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData32, sizeof(initData32), QStringLiteral("init"), false, false);
|
||||
/*writeCharacteristic(initData32, sizeof(initData32), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData33, sizeof(initData33), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData34, sizeof(initData34), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(initData35, sizeof(initData35), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(sleepms);
|
||||
QThread::msleep(sleepms); */
|
||||
} else if (nordictrack_t65s_treadmill_81_miles) {
|
||||
uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02};
|
||||
uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
@@ -115,6 +115,7 @@ class proformtreadmill : public treadmill {
|
||||
bool proform_treadmill_705_cst_V80_44 = false;
|
||||
bool nordictrack_t65s_treadmill_81_miles = false;
|
||||
bool nordictrack_elite_800 = false;
|
||||
bool proform_treadmill_995i = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
@@ -272,8 +272,7 @@ void schwinnic4bike::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
if (Flags.heartRate) {
|
||||
|
||||
heart = ((double)(((uint8_t)newValue.at(index))));
|
||||
// index += 1; //NOTE: clang-analyzer-deadcode..DeadStores
|
||||
emit debug(QStringLiteral("Current Heart: ") + QString::number(heart));
|
||||
// index += 1; //NOTE: clang-analyzer-deadcode..DeadStores
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,12 +368,13 @@ void schwinnic4bike::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value()));
|
||||
emit debug(QStringLiteral("Current Peloton Resistance: ") + QString::number(m_pelotonResistance.value()));
|
||||
emit debug(QStringLiteral("Current Calculated Resistance: ") + QString::number(Resistance.value()));
|
||||
emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs));
|
||||
|
||||
@@ -249,6 +249,11 @@ void solebike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
return;
|
||||
}
|
||||
|
||||
if (((unsigned char)newValue.at(1)) != 0x13) {
|
||||
qDebug() << QStringLiteral("not a valid packet");
|
||||
return;
|
||||
}
|
||||
|
||||
double distance = GetDistanceFromPacket(newValue);
|
||||
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
@@ -335,7 +340,14 @@ QTime solebike::GetElapsedFromPacket(const QByteArray &packet) {
|
||||
double solebike::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(7) << 8) | packet.at(8);
|
||||
double data = ((double)convertedData) / 100.0f;
|
||||
return data;
|
||||
|
||||
// the bike send the distance in miles always
|
||||
QSettings settings;
|
||||
double miles = 1;
|
||||
if (settings.value(QZSettings::sole_treadmill_miles, QZSettings::default_sole_treadmill_miles).toBool())
|
||||
miles = 1.60934;
|
||||
|
||||
return data * miles;
|
||||
}
|
||||
|
||||
double solebike::GetWattFromPacket(const QByteArray &packet) {
|
||||
@@ -347,7 +359,14 @@ double solebike::GetWattFromPacket(const QByteArray &packet) {
|
||||
double solebike::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(11) << 8) | packet.at(12);
|
||||
double data = ((double)convertedData) / 100.0f;
|
||||
return data;
|
||||
|
||||
// the bike send the speed in miles always
|
||||
QSettings settings;
|
||||
double miles = 1;
|
||||
if (settings.value(QZSettings::sole_treadmill_miles, QZSettings::default_sole_treadmill_miles).toBool())
|
||||
miles = 1.60934;
|
||||
|
||||
return data * miles;
|
||||
}
|
||||
|
||||
void solebike::btinit() {
|
||||
|
||||
@@ -102,6 +102,22 @@ void stagesbike::update() {
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
if (requestInclination != -100 || ((VirtualBike() && VirtualBike()->ftmsDeviceConnected()) && lastGearValue != gears() && lastRawRequestedInclinationValue != -100)) {
|
||||
qDebug() << QStringLiteral("writing inclination ") << requestInclination << lastRawRequestedInclinationValue << gears();
|
||||
|
||||
if(eliteService != nullptr) {
|
||||
QByteArray a = setSimulationMode(
|
||||
lastRawRequestedInclinationValue + gears(), 0.005, 0.5, 0.0, 1.0); // since this bike doesn't have the concept of resistance,
|
||||
// i'm using the gears in the inclination
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(eliteService, eliteWriteCharacteristic, b, a.length(), "forceInclination", false, false);
|
||||
|
||||
requestInclination = -100; // reset the requestInclination to -100 so that it doesn't get written again
|
||||
requestResistance = -1; // reset the requestResistance so that it doesn't get written again
|
||||
}
|
||||
}
|
||||
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance > 100) {
|
||||
requestResistance = 100;
|
||||
@@ -551,6 +567,10 @@ void stagesbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
// ********************************************************************************************************
|
||||
}
|
||||
|
||||
void stagesbike::inclinationChanged(double grade, double percentage) {
|
||||
changeInclination(grade, percentage);
|
||||
}
|
||||
|
||||
void stagesbike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' '));
|
||||
|
||||
|
||||
@@ -104,6 +104,8 @@ class stagesbike : public bike {
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
void inclinationChanged(double grade, double percentage);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
|
||||
@@ -345,14 +345,15 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
uint16_t time_division = 1024;
|
||||
uint8_t index = 4;
|
||||
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
if (newValue.length() > 3) {
|
||||
m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
if (newValue.length() > 3) {
|
||||
double tacx_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
m_rawWatt = tacx_watt; // Always update rawWatt from TACX bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = tacx_watt; // Only update watt if no external power sensor
|
||||
emit powerChanged(m_watt.value());
|
||||
}
|
||||
|
||||
emit powerChanged(m_watt.value());
|
||||
emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
@@ -635,11 +636,14 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
|
||||
if (Flags.instantPower) {
|
||||
// power table from an user
|
||||
double tacx_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
m_rawWatt = tacx_watt; // Always update rawWatt from TACX bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = tacx_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
@@ -104,22 +104,7 @@ uint16_t technogymbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t technogymbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; 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 max_resistance;
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
void technogymbike::forceResistance(resistance_t requestResistance) {
|
||||
@@ -424,7 +409,7 @@ void technogymbike::characteristicChanged(const QLowEnergyCharacteristic &charac
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -33,9 +33,16 @@ void toorxtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this,
|
||||
&toorxtreadmill::serviceDiscovered);
|
||||
|
||||
// Start a discovery
|
||||
qDebug() << QStringLiteral("toorxtreadmill::deviceDiscovered");
|
||||
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
|
||||
// Start a discovery - use FullDiscovery only if not done before
|
||||
QSettings settings;
|
||||
bool discoveryCompleted = settings.value(QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed).toBool();
|
||||
qDebug() << QStringLiteral("toorxtreadmill::deviceDiscovered - discoveryCompleted:") << discoveryCompleted;
|
||||
|
||||
if (discoveryCompleted) {
|
||||
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::MinimalDiscovery);
|
||||
} else {
|
||||
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -382,6 +389,14 @@ void toorxtreadmill::rfCommConnected() {
|
||||
qDebug() << QStringLiteral(" init1 write");
|
||||
socket->write((char *)init2, sizeof(init2));
|
||||
qDebug() << QStringLiteral(" init2 write");
|
||||
|
||||
// Mark discovery as completed for future connections
|
||||
QSettings settings;
|
||||
if (!settings.value(QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed).toBool()) {
|
||||
settings.setValue(QZSettings::toorxtreadmill_discovery_completed, true);
|
||||
qDebug() << QStringLiteral("toorxtreadmill discovery marked as completed");
|
||||
}
|
||||
|
||||
initDone = true;
|
||||
// requestStart = 1;
|
||||
emit connectedAndDiscovered();
|
||||
@@ -426,8 +441,8 @@ uint16_t toorxtreadmill::GetCaloriesFromPacket(const QByteArray &packet) {
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
uint16_t toorxtreadmill::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(9) << 8) | packet.at(10);
|
||||
double toorxtreadmill::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
double convertedData = (double)((packet.at(9) << 8) | packet.at(10)) / 100.0;
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class toorxtreadmill : public treadmill {
|
||||
bool MASTERT409 = false;
|
||||
|
||||
uint16_t GetElapsedTimeFromPacket(const QByteArray &packet);
|
||||
uint16_t GetDistanceFromPacket(const QByteArray &packet);
|
||||
double GetDistanceFromPacket(const QByteArray &packet);
|
||||
uint16_t GetCaloriesFromPacket(const QByteArray &packet);
|
||||
double GetSpeedFromPacket(const QByteArray &packet);
|
||||
uint8_t GetInclinationFromPacket(const QByteArray &packet);
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
treadmill::treadmill() {}
|
||||
|
||||
void treadmill::changeSpeed(double speed) {
|
||||
// Reset target watts only if called from external source
|
||||
if (!callingFromFollowPower) {
|
||||
targetWatts = -1;
|
||||
qDebug() << "External speed change - resetting power following mode";
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
bool stryd_speed_instead_treadmill = settings.value(QZSettings::stryd_speed_instead_treadmill, QZSettings::default_stryd_speed_instead_treadmill).toBool();
|
||||
m_lastRawSpeedRequested = speed;
|
||||
@@ -31,7 +37,7 @@ void treadmill::changeSpeed(double speed) {
|
||||
if (autoResistanceEnable)
|
||||
requestSpeed = (speed * m_difficult) + m_difficult_offset;
|
||||
}
|
||||
void treadmill::changeInclination(double grade, double inclination) {
|
||||
void treadmill::changeInclination(double grade, double inclination) {
|
||||
QSettings settings;
|
||||
double treadmill_incline_min = settings.value(QZSettings::treadmill_incline_min, QZSettings::default_treadmill_incline_min).toDouble();
|
||||
double treadmill_incline_max = settings.value(QZSettings::treadmill_incline_max, QZSettings::default_treadmill_incline_max).toDouble();
|
||||
@@ -587,25 +593,34 @@ bool treadmill::followPowerBySpeed() {
|
||||
if (treadmill_follow_wattage) {
|
||||
|
||||
if (currentInclination().value() != lastInclination && wattsMetric().value() != 0) {
|
||||
|
||||
// If not following power mode, calculate new target from current values
|
||||
if (targetWatts == -1) {
|
||||
targetWatts = wattsCalc(w, currentSpeed().value(), lastInclination);
|
||||
qDebug() << "Starting power following mode with target watts:" << targetWatts;
|
||||
}
|
||||
|
||||
// Find speed to maintain targetWatts with current inclination
|
||||
double newspeed = 0;
|
||||
double bestSpeed = 0.1;
|
||||
|
||||
// don't read the wattage directly from the m_watt because if you were using a power sensor, the power calcuated in the for will not match it
|
||||
double previousWatt = wattsCalc(w, currentSpeed().value(), lastInclination);
|
||||
|
||||
double bestDifference = fabs(wattsCalc(w, bestSpeed, currentInclination().value()) - previousWatt);
|
||||
double bestDifference = fabs(wattsCalc(w, bestSpeed, currentInclination().value()) - targetWatts);
|
||||
|
||||
for (int speed = 1; speed <= 300; speed++) {
|
||||
double s = ((double)speed) / 10.0;
|
||||
double thisDifference = fabs(wattsCalc(w, s, currentInclination().value()) - previousWatt);
|
||||
double thisDifference = fabs(wattsCalc(w, s, currentInclination().value()) - targetWatts);
|
||||
if (thisDifference < bestDifference) {
|
||||
bestDifference = thisDifference;
|
||||
bestSpeed = s;
|
||||
}
|
||||
}
|
||||
// Now bestSpeed is the speed closest to the desired wattage
|
||||
|
||||
newspeed = bestSpeed;
|
||||
qDebug() << QStringLiteral("changing speed to") << newspeed << "due to inclination changed" << currentInclination().value() << lastInclination;
|
||||
qDebug() << "Following power: changing speed to" << newspeed << "to maintain" << targetWatts << "watts (inclination changed" << currentInclination().value() << lastInclination << ")";
|
||||
|
||||
callingFromFollowPower = true; // Set flag before calling
|
||||
changeSpeedAndInclination(newspeed, currentInclination().value());
|
||||
callingFromFollowPower = false; // Reset flag after calling
|
||||
|
||||
r = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@ class treadmill : public bluetoothdevice {
|
||||
double m_lastRawInclinationRequested = -100;
|
||||
bool instantaneousStrideLengthCMAvailableFromDevice = false;
|
||||
treadmillErgTable _ergTable;
|
||||
|
||||
// Power following logic
|
||||
bool callingFromFollowPower = false; // Flag to track if change comes from followPowerBySpeed
|
||||
double targetWatts = -1; // Target watts to maintain during power following
|
||||
|
||||
void parseSpeed(double speed);
|
||||
void parseInclination(double speed);
|
||||
|
||||
@@ -431,6 +431,10 @@ double trxappgateusbbike::GetWattFromPacket(const QByteArray &packet) {
|
||||
|
||||
double trxappgateusbbike::GetCadenceFromPacket(const QByteArray &packet) {
|
||||
|
||||
QSettings settings;
|
||||
double cadence_gain = settings.value(QZSettings::cadence_gain, QZSettings::default_cadence_gain).toDouble();
|
||||
double cadence_offset = settings.value(QZSettings::cadence_offset, QZSettings::default_cadence_offset).toDouble();
|
||||
|
||||
uint16_t convertedData;
|
||||
if (bike_type != JLL_IC400 && bike_type != ASVIVA && bike_type != FYTTER_RI08 && bike_type != HAMMER_SPEED_BIKE_S) {
|
||||
convertedData = (packet.at(9) - 1) + ((packet.at(8) - 1) * 100);
|
||||
@@ -441,7 +445,7 @@ double trxappgateusbbike::GetCadenceFromPacket(const QByteArray &packet) {
|
||||
if (data < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
return (data * cadence_gain) + cadence_offset ;
|
||||
}
|
||||
|
||||
double trxappgateusbbike::GetResistanceFromPacket(const QByteArray &packet) {
|
||||
@@ -1276,28 +1280,7 @@ uint16_t trxappgateusbbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t trxappgateusbbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
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();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
void trxappgateusbbike::resistanceFromFTMSAccessory(resistance_t res) {
|
||||
|
||||
@@ -56,7 +56,7 @@ void trxappgateusbelliptical::writeCharacteristic(uint8_t *data, uint8_t data_le
|
||||
}
|
||||
|
||||
void trxappgateusbelliptical::forceResistance(resistance_t requestResistance) {
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
|
||||
uint8_t noOpData1[] = {0xf0, 0xa6, 0x01, 0x01, 0x03, 0x9b};
|
||||
noOpData1[4] = requestResistance + 1;
|
||||
noOpData1[5] = noOpData1[4] + 0x98;
|
||||
@@ -95,7 +95,7 @@ void trxappgateusbelliptical::update() {
|
||||
}
|
||||
requestResistance = -1;
|
||||
} else {
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
|
||||
uint8_t noOpData1[] = {0xf0, 0xa2, 0x01, 0x01, 0x94};
|
||||
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp"));
|
||||
} else {
|
||||
@@ -139,22 +139,41 @@ void trxappgateusbelliptical::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
|
||||
double trxappgateusbelliptical::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = (packet.at(7) - 1) + ((packet.at(6) - 1) * 100);
|
||||
double data = (double)(convertedData) / 10.0f;
|
||||
return data;
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// JTX Fitness doesn't send speed via bluetooth, calculate from cadence using settings ratio
|
||||
QSettings settings;
|
||||
double cadence_speed_ratio = settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio).toDouble();
|
||||
double cadence = GetCadenceFromPacket(packet);
|
||||
return cadence * cadence_speed_ratio;
|
||||
} else {
|
||||
uint16_t convertedData = (packet.at(7) - 1) + ((packet.at(6) - 1) * 100);
|
||||
double data = (double)(convertedData) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
double trxappgateusbelliptical::GetCadenceFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = ((uint16_t)packet.at(9)) + ((uint16_t)packet.at(8) * 100);
|
||||
uint16_t convertedData;
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// JTX Fitness uses only byte 5 for cadence
|
||||
convertedData = packet.at(5);
|
||||
} else {
|
||||
convertedData = ((uint16_t)packet.at(9)) + ((uint16_t)packet.at(8) * 100);
|
||||
}
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
double trxappgateusbelliptical::GetWattFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = ((packet.at(16) - 1) * 100) + (packet.at(17) - 1);
|
||||
double data = ((double)(convertedData)) / 10.0f;
|
||||
return data;
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// JTX Fitness doesn't send watts via bluetooth, use classic elliptical calculation
|
||||
return 0; // Will be calculated in characteristicChanged using wattsFromResistance
|
||||
} else {
|
||||
uint16_t convertedData = ((packet.at(16) - 1) * 100) + (packet.at(17) - 1);
|
||||
double data = ((double)(convertedData)) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
|
||||
@@ -216,7 +235,7 @@ void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacterist
|
||||
|
||||
void trxappgateusbelliptical::btinit() {
|
||||
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
|
||||
uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x00, 0x91};
|
||||
uint8_t initData2[] = {0xf0, 0xa0, 0x01, 0x01, 0x92};
|
||||
uint8_t initData3[] = {0xf0, 0xa1, 0x01, 0x01, 0x93};
|
||||
@@ -277,11 +296,12 @@ void trxappgateusbelliptical::stateChanged(QLowEnergyService::ServiceState state
|
||||
QString uuidNotify1 = QStringLiteral("0000fff1-0000-1000-8000-00805f9b34fb");
|
||||
QString uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e");
|
||||
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS) {
|
||||
uuidWrite = QStringLiteral("49535343-8841-43f4-a8d4-ecbe34729bb3");
|
||||
uuidNotify1 = QStringLiteral("49535343-1E4D-4BD9-BA61-23C647249616");
|
||||
uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e");
|
||||
}
|
||||
// TAURUS_FX99 uses standard 0000fff0 characteristics
|
||||
|
||||
QBluetoothUuid _gattWriteCharacteristicId(uuidWrite);
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(uuidNotify1);
|
||||
@@ -364,6 +384,41 @@ void trxappgateusbelliptical::serviceScanDone(void) {
|
||||
uuid = uuid2;
|
||||
}
|
||||
|
||||
// Fallback logic: try to find the service in discovered services
|
||||
bool found = false;
|
||||
foreach (QBluetoothUuid s, m_control->services()) {
|
||||
if (s == QBluetoothUuid::fromString(uuid)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If primary service not found, try fallback service
|
||||
if (!found) {
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
// I-CONSOLE+ device but DCT2000I service not found, try 0000fff0 service (Taurus FX9.9)
|
||||
bool found_fff0 = false;
|
||||
foreach (QBluetoothUuid s, m_control->services()) {
|
||||
if (s == QBluetoothUuid::fromString(uuid3)) {
|
||||
found_fff0 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found_fff0) {
|
||||
uuid = uuid3;
|
||||
elliptical_type = TYPE::TAURUS_FX99;
|
||||
qDebug() << QStringLiteral("I-CONSOLE+ device detected as Taurus FX9.9 with 0000fff0 service");
|
||||
} else {
|
||||
qDebug() << QStringLiteral("DCT2000I service not found");
|
||||
}
|
||||
} else {
|
||||
// Try DCT2000I/JTX Fitness service as fallback
|
||||
uuid = uuid2;
|
||||
elliptical_type = TYPE::JTX_FITNESS;
|
||||
qDebug() << QStringLiteral("Standard service not found, trying JTX Fitness service as fallback");
|
||||
}
|
||||
}
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(uuid);
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
@@ -448,7 +503,13 @@ void trxappgateusbelliptical::controllerStateChanged(QLowEnergyController::Contr
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t trxappgateusbelliptical::watts() { return m_watt.value(); }
|
||||
uint16_t trxappgateusbelliptical::watts() {
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// For JTX Fitness, always use the elliptical class generic calculation
|
||||
return elliptical::watts();
|
||||
}
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
|
||||
void trxappgateusbelliptical::searchingStop() { searchStopped = true; }
|
||||
|
||||
@@ -78,6 +78,8 @@ class trxappgateusbelliptical : public elliptical {
|
||||
typedef enum TYPE {
|
||||
ELLIPTICAL_GENERIC = 0,
|
||||
DCT2000I = 1,
|
||||
JTX_FITNESS = 2,
|
||||
TAURUS_FX99 = 3,
|
||||
} TYPE;
|
||||
TYPE elliptical_type = ELLIPTICAL_GENERIC;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ wahookickrsnapbike::wahookickrsnapbike(bool noWriteResistance, bool noHeartServi
|
||||
ergModeSupported = true; // IMPORTANT, only for this bike
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_rawWatt.setType(metric::METRIC_WATT);
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -35,6 +34,17 @@ wahookickrsnapbike::wahookickrsnapbike(bool noWriteResistance, bool noHeartServi
|
||||
refresh->start(settings.value(QZSettings::poll_device_time, QZSettings::default_poll_device_time).toInt());
|
||||
wheelCircumference::GearTable g;
|
||||
g.printTable();
|
||||
|
||||
// Setup op timeout handler for serialized commands
|
||||
_opTimeout.setSingleShot(true);
|
||||
connect(&_opTimeout, &QTimer::timeout, this, [this]() {
|
||||
// Timeout waiting for ack; clear and try next pending to avoid stall
|
||||
if (_currentOp != WahooOp::None) {
|
||||
emit debug(QStringLiteral("Ack timeout; releasing op and continuing queue"));
|
||||
}
|
||||
_currentOp = WahooOp::None;
|
||||
processNextPending();
|
||||
});
|
||||
}
|
||||
|
||||
void wahookickrsnapbike::restoreDefaultWheelDiameter() {
|
||||
@@ -200,9 +210,11 @@ void wahookickrsnapbike::update() {
|
||||
}
|
||||
#endif
|
||||
|
||||
QSettings settings;
|
||||
bool wahooWithoutWheelDiameter = settings.value(QZSettings::wahoo_without_wheel_diameter, QZSettings::default_wahoo_without_wheel_diameter).toBool();
|
||||
|
||||
if (initRequest) {
|
||||
lastCommandErgMode = false;
|
||||
QSettings settings;
|
||||
QByteArray a = unlockCommand();
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
@@ -219,16 +231,17 @@ void wahookickrsnapbike::update() {
|
||||
}
|
||||
QThread::msleep(700);
|
||||
|
||||
QByteArray d = setWheelCircumference(wheelCircumference::gearsToWheelDiameter(gears()));
|
||||
uint8_t e[20];
|
||||
setGears(settings.value(QZSettings::gears_current_value, QZSettings::default_gears_current_value).toDouble());
|
||||
memcpy(e, d.constData(), d.length());
|
||||
writeCharacteristic(e, d.length(), "setWheelCircumference", false, true);
|
||||
if (!wahooWithoutWheelDiameter) {
|
||||
QByteArray d = setWheelCircumference(wheelCircumference::gearsToWheelDiameter(gears()));
|
||||
uint8_t e[20];
|
||||
memcpy(e, d.constData(), d.length());
|
||||
writeCharacteristic(e, d.length(), "setWheelCircumference", false, true);
|
||||
}
|
||||
|
||||
// required to the SS2K only one time
|
||||
Resistance = 0;
|
||||
emit resistanceRead(Resistance.value());
|
||||
initRequest = false;
|
||||
initRequest = false;
|
||||
} else if (
|
||||
#ifndef Q_OS_IOS
|
||||
bluetoothDevice.isValid() &&
|
||||
@@ -265,54 +278,91 @@ void wahookickrsnapbike::update() {
|
||||
requestResistance = -1;
|
||||
}
|
||||
|
||||
if (KICKR_BIKE) {
|
||||
if(requestInclination != -100) {
|
||||
debug("writing inclination request " + QString::number(requestInclination));
|
||||
inclinationChanged(requestInclination, requestInclination);
|
||||
Inclination = requestInclination; // the bike is not sending back the inclination?
|
||||
requestInclination = -100;
|
||||
}
|
||||
} else if (requestResistance != -1 && KICKR_BIKE == false) {
|
||||
if (requestResistance > 100) {
|
||||
requestResistance = 100;
|
||||
} else if (requestResistance == 0) {
|
||||
requestResistance = 1;
|
||||
if (!wahooWithoutWheelDiameter) {
|
||||
if (KICKR_BIKE) {
|
||||
if(requestInclination != -100) {
|
||||
debug("writing inclination request " + QString::number(requestInclination));
|
||||
inclinationChanged(requestInclination, requestInclination);
|
||||
Inclination = requestInclination; // the bike is not sending back the inclination?
|
||||
requestInclination = -100;
|
||||
}
|
||||
} else if (requestResistance != -1 && KICKR_BIKE == false) {
|
||||
if (requestResistance > 100) {
|
||||
requestResistance = 100;
|
||||
} else if (requestResistance == 0) {
|
||||
requestResistance = 1;
|
||||
}
|
||||
|
||||
auto virtualBike = this->VirtualBike();
|
||||
if (requestResistance != currentResistance().value() &&
|
||||
((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
lastForcedResistance = requestResistance;
|
||||
QByteArray a = setResistanceMode(((double)requestResistance) / 100.0);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
} else if (requestResistance != currentResistance().value() && ((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(lastForcedResistance));
|
||||
QByteArray a = setResistanceMode(((double)lastForcedResistance) / 100.0);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
}
|
||||
requestResistance = -1;
|
||||
}
|
||||
|
||||
auto virtualBike = this->VirtualBike();
|
||||
if (requestResistance != currentResistance().value() &&
|
||||
((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
lastForcedResistance = requestResistance;
|
||||
QByteArray a = setResistanceMode(((double)requestResistance) / 100.0);
|
||||
if (lastGearValue != gears()) {
|
||||
if(KICKR_SNAP) {
|
||||
inclinationChanged(lastGrade, lastGrade);
|
||||
} else {
|
||||
// Queue wheel circumference change (higher priority)
|
||||
sendWheelCircumferenceNow(wheelCircumference::gearsToWheelDiameter(gears()));
|
||||
lastGrade = 999; // to force a change
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (KICKR_BIKE) {
|
||||
if(requestInclination != -100) {
|
||||
debug("writing inclination request " + QString::number(requestInclination));
|
||||
inclinationChanged(requestInclination, requestInclination);
|
||||
Inclination = requestInclination; // the bike is not sending back the inclination?
|
||||
requestInclination = -100;
|
||||
} else if (lastGearValue != gears()) {
|
||||
inclinationChanged(lastGrade, lastGrade);
|
||||
}
|
||||
} else if ((requestResistance != -1 || lastGearValue != gears()) && KICKR_BIKE == false) {
|
||||
if (requestResistance > 100) {
|
||||
requestResistance = 100;
|
||||
} else if (requestResistance == 0) {
|
||||
requestResistance = 1;
|
||||
}
|
||||
|
||||
auto virtualBike = this->VirtualBike();
|
||||
if (requestResistance != currentResistance().value() &&
|
||||
((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
lastForcedResistance = requestResistance;
|
||||
QByteArray a = setResistanceMode(((double)requestResistance) / 100.0);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
} else if (requestResistance != currentResistance().value() &&
|
||||
((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && lastGearValue != gears()) {
|
||||
emit debug(QStringLiteral("writing resistance due to gears changed ") + QString::number(lastForcedResistance));
|
||||
QByteArray a = setResistanceMode(((double)lastForcedResistance + (gears() - lastGearValue)) / 100.0);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
} else if (requestResistance != currentResistance().value() &&
|
||||
((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(lastForcedResistance));
|
||||
QByteArray a = setResistanceMode(((double)lastForcedResistance) / 100.0);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
}
|
||||
requestResistance = -1;
|
||||
}
|
||||
|
||||
if (lastGearValue != gears()) {
|
||||
if(KICKR_SNAP) {
|
||||
inclinationChanged(lastGrade, lastGrade);
|
||||
} else {
|
||||
QByteArray a = setWheelCircumference(wheelCircumference::gearsToWheelDiameter(gears()));
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setWheelCircumference", false, false);
|
||||
lastGrade = 999; // to force a change
|
||||
} else if (virtualBike && virtualBike->ftmsDeviceConnected() && lastGearValue != gears()) {
|
||||
inclinationChanged(lastGrade, lastGrade);
|
||||
}
|
||||
requestResistance = -1;
|
||||
}
|
||||
}
|
||||
|
||||
lastGearValue = gears();
|
||||
|
||||
if (requestStart != -1) {
|
||||
emit debug(QStringLiteral("starting..."));
|
||||
|
||||
@@ -401,6 +451,26 @@ void wahookickrsnapbike::handleCharacteristicValueChanged(const QBluetoothUuid &
|
||||
|
||||
qDebug() << QStringLiteral(" << ") << newValue.toHex(' ') << uuid;
|
||||
|
||||
// Detect acks for serialized commands (format: 0x01 <cmdId> 0x01 0x00 ...)
|
||||
if (newValue.size() >= 3 && (uint8_t)newValue.at(0) == 0x01) {
|
||||
uint8_t cmd = (uint8_t)newValue.at(1);
|
||||
uint8_t status = (uint8_t)newValue.at(2);
|
||||
if ((cmd == _setSimGrade || cmd == _setWheelCircumference) && status == 0x01) {
|
||||
|
||||
if(cmd == _setWheelCircumference) {
|
||||
homeform::singleton()->setToastRequested("Gear accepted from the trainer");
|
||||
}
|
||||
|
||||
// Ack received for our tracked op; release and continue
|
||||
if ((_currentOp == WahooOp::SimGrade && cmd == _setSimGrade) ||
|
||||
(_currentOp == WahooOp::WheelCircumference && cmd == _setWheelCircumference)) {
|
||||
_opTimeout.stop();
|
||||
_currentOp = WahooOp::None;
|
||||
processNextPending();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uuid == QBluetoothUuid::CyclingPowerMeasurement) {
|
||||
lastPacket = newValue;
|
||||
|
||||
@@ -803,7 +873,7 @@ void wahookickrsnapbike::serviceScanDone(void) {
|
||||
QSettings settings;
|
||||
settings.setValue(QZSettings::ftms_bike, bluetoothDevice.name());
|
||||
settings.sync();
|
||||
if(homeform::singleton())
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Zwift Hub device found, please restart the app to enjoy virtual gearing!");
|
||||
return;
|
||||
}
|
||||
@@ -824,13 +894,13 @@ void wahookickrsnapbike::error(QLowEnergyController::Error err) {
|
||||
void wahookickrsnapbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
|
||||
if (device.name().toUpper().startsWith("WAHOO KICKR")) {
|
||||
WAHOO_KICKR = true;
|
||||
qDebug() << "WAHOO KICKR workaround activated";
|
||||
} else if(device.name().toUpper().startsWith("KICKR BIKE")) {
|
||||
KICKR_BIKE = true;
|
||||
qDebug() << "KICKR BIKE workaround activated";
|
||||
|
||||
if (device.name().toUpper().startsWith("WAHOO KICKR")) {
|
||||
WAHOO_KICKR = true;
|
||||
qDebug() << "WAHOO KICKR workaround activated";
|
||||
} else if(device.name().toUpper().startsWith("KICKR BIKE")) {
|
||||
KICKR_BIKE = true;
|
||||
qDebug() << "KICKR BIKE workaround activated";
|
||||
} else if(device.name().toUpper().startsWith("KICKR SNAP")) {
|
||||
KICKR_SNAP = true;
|
||||
qDebug() << "KICKR SNAP workaround activated";
|
||||
@@ -916,30 +986,84 @@ void wahookickrsnapbike::controllerStateChanged(QLowEnergyController::Controller
|
||||
|
||||
void wahookickrsnapbike::inclinationChanged(double grade, double percentage) {
|
||||
Q_UNUSED(percentage);
|
||||
if(lastCommandErgMode) {
|
||||
lastGrade = grade + 1; // to force a refresh
|
||||
initRequest = true;
|
||||
qDebug() << "avoid sending this command, since I have first to restore the setSimGrade";
|
||||
return;
|
||||
}
|
||||
if(lastGrade == grade) {
|
||||
qDebug() << "grade is already set to " << grade << "skipping";
|
||||
return;
|
||||
}
|
||||
lastGrade = grade;
|
||||
Inclination = grade;
|
||||
emit debug(QStringLiteral("writing inclination ") + QString::number(grade));
|
||||
QSettings settings;
|
||||
double g = grade;
|
||||
if(KICKR_SNAP) {
|
||||
g += gears() * 0.5;
|
||||
qDebug() << "adding gear offset so " << g;
|
||||
|
||||
if (settings.value(QZSettings::wahoo_without_wheel_diameter, QZSettings::default_wahoo_without_wheel_diameter).toBool()) {
|
||||
lastGrade = grade;
|
||||
emit debug(QStringLiteral("writing inclination ") + QString::number(grade));
|
||||
double g = grade;
|
||||
g += gears();
|
||||
sendSimGradeNow(g);
|
||||
} else {
|
||||
if(lastCommandErgMode) {
|
||||
lastGrade = grade + 1; // to force a refresh
|
||||
initRequest = true;
|
||||
qDebug() << "avoid sending this command, since I have first to restore the setSimGrade";
|
||||
return;
|
||||
}
|
||||
if(lastGrade == grade) {
|
||||
qDebug() << "grade is already set to " << grade << "skipping";
|
||||
return;
|
||||
}
|
||||
lastGrade = grade;
|
||||
Inclination = grade;
|
||||
emit debug(QStringLiteral("writing inclination ") + QString::number(grade));
|
||||
double g = grade;
|
||||
if(KICKR_SNAP) {
|
||||
g += gears() * 0.5;
|
||||
qDebug() << "adding gear offset so " << g;
|
||||
}
|
||||
sendSimGradeNow(g);
|
||||
lastCommandErgMode = false;
|
||||
}
|
||||
QByteArray a = setSimGrade(g);
|
||||
}
|
||||
|
||||
// Send or enqueue: SimGrade
|
||||
void wahookickrsnapbike::sendSimGradeNow(double grade) {
|
||||
// If an operation is in flight, store latest grade and return
|
||||
if (_currentOp != WahooOp::None) {
|
||||
_pendingSimGrade = true;
|
||||
_pendingSimGradeValue = grade;
|
||||
return;
|
||||
}
|
||||
QByteArray a = setSimGrade(grade);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
_currentOp = WahooOp::SimGrade;
|
||||
// Send without blocking; wait for explicit ack in handleCharacteristicValueChanged
|
||||
writeCharacteristic(b, a.length(), "setSimGrade", false, false);
|
||||
lastCommandErgMode = false;
|
||||
_opTimeout.start(1000);
|
||||
}
|
||||
|
||||
// Send or enqueue: Wheel Circumference
|
||||
void wahookickrsnapbike::sendWheelCircumferenceNow(double mm) {
|
||||
// If an operation is in flight, prefer to hold latest wheel circ (priority for next send)
|
||||
if (_currentOp != WahooOp::None) {
|
||||
_pendingWheelCirc = true;
|
||||
_pendingWheelCircValue = mm;
|
||||
return;
|
||||
}
|
||||
QByteArray a = setWheelCircumference(mm);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
_currentOp = WahooOp::WheelCircumference;
|
||||
writeCharacteristic(b, a.length(), "setWheelCircumference", false, false);
|
||||
_opTimeout.start(1000);
|
||||
}
|
||||
|
||||
// Process next pending item, wheel circumference has priority
|
||||
void wahookickrsnapbike::processNextPending() {
|
||||
if (_currentOp != WahooOp::None) return;
|
||||
if (_pendingWheelCirc) {
|
||||
_pendingWheelCirc = false;
|
||||
sendWheelCircumferenceNow(_pendingWheelCircValue);
|
||||
return;
|
||||
}
|
||||
if (_pendingSimGrade) {
|
||||
_pendingSimGrade = false;
|
||||
sendSimGradeNow(_pendingSimGradeValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool wahookickrsnapbike::inclinationAvailableByHardware() {
|
||||
@@ -947,10 +1071,18 @@ bool wahookickrsnapbike::inclinationAvailableByHardware() {
|
||||
}
|
||||
|
||||
double wahookickrsnapbike::maxGears() {
|
||||
QSettings settings;
|
||||
if (settings.value(QZSettings::wahoo_without_wheel_diameter, QZSettings::default_wahoo_without_wheel_diameter).toBool()) {
|
||||
return bike::maxGears(); // Use base class behavior
|
||||
}
|
||||
wheelCircumference::GearTable g;
|
||||
return g.maxGears;
|
||||
return g.maxGears; // Use gear table when wheel diameter mode is disabled
|
||||
}
|
||||
|
||||
double wahookickrsnapbike::minGears() {
|
||||
return 1;
|
||||
QSettings settings;
|
||||
if (settings.value(QZSettings::wahoo_without_wheel_diameter, QZSettings::default_wahoo_without_wheel_diameter).toBool()) {
|
||||
return bike::minGears(); // Use base class behavior
|
||||
}
|
||||
return 1; // Use gear minimum when wheel diameter mode is disabled
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ class wahookickrsnapbike : public bike {
|
||||
double maxGears() override;
|
||||
double minGears() override;
|
||||
|
||||
|
||||
enum OperationCode : uint8_t {
|
||||
_unlock = 32,
|
||||
_setResistanceMode = 64,
|
||||
@@ -62,7 +63,7 @@ class wahookickrsnapbike : public bike {
|
||||
// Variabili per iOS (pubbliche per permettere all'implementazione iOS di impostarle)
|
||||
bool zwift_found = false;
|
||||
bool wahoo_found = false;
|
||||
|
||||
|
||||
// Wrapper per characteristicChanged che accetta direttamente QBluetoothUuid
|
||||
void handleCharacteristicValueChanged(const QBluetoothUuid &uuid, const QByteArray &newValue);
|
||||
|
||||
@@ -80,6 +81,18 @@ class wahookickrsnapbike : public bike {
|
||||
|
||||
bool writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false,
|
||||
bool wait_for_response = false);
|
||||
|
||||
// Serialized command handling for setSimGrade and setWheelCircumference
|
||||
enum class WahooOp { None, SimGrade, WheelCircumference };
|
||||
WahooOp _currentOp = WahooOp::None;
|
||||
bool _pendingSimGrade = false;
|
||||
double _pendingSimGradeValue = 0.0;
|
||||
bool _pendingWheelCirc = false;
|
||||
double _pendingWheelCircValue = 0.0;
|
||||
QTimer _opTimeout;
|
||||
void processNextPending();
|
||||
void sendSimGradeNow(double grade);
|
||||
void sendWheelCircumferenceNow(double mm);
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
metric ResistanceFromFTMSAccessory;
|
||||
void startDiscover();
|
||||
|
||||
@@ -79,7 +79,7 @@ void ypooelliptical::forceInclination(double inclination) {
|
||||
|
||||
void ypooelliptical::forceResistance(resistance_t requestResistance) {
|
||||
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS) {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00};
|
||||
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
|
||||
writeCharacteristic(&gattFTMSWriteCharControlPointId, gattFTMSService, write, sizeof(write),
|
||||
@@ -108,7 +108,7 @@ void ypooelliptical::update() {
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(&gattFTMSWriteCharControlPointId, gattFTMSService, write, sizeof(write), "requestControl", false, true);
|
||||
} else {
|
||||
@@ -250,7 +250,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
|
||||
if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACE) && !iconsole_elliptical) {
|
||||
|
||||
if(E35 == false && SCH_590E == false && KETTLER == false && CARDIOPOWER_EEGO == false && MYELLIPTICAL == false && SKANDIKA == false && DOMYOS == false) {
|
||||
if(E35 == false && SCH_590E == false && KETTLER == false && CARDIOPOWER_EEGO == false && MYELLIPTICAL == false && SKANDIKA == false && DOMYOS == false && FEIER == false && MX_AS == false && FTMS == false) {
|
||||
if (newvalue.length() == 18) {
|
||||
qDebug() << QStringLiteral("let's wait for the next piece of frame");
|
||||
lastPacket = newvalue;
|
||||
@@ -270,7 +270,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
index += 3;
|
||||
|
||||
if (!Flags.moreData) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS) {
|
||||
Speed = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index)))) /
|
||||
100.0;
|
||||
@@ -282,7 +282,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
// this particular device, seems to send the actual speed here
|
||||
if (Flags.avgSpeed) {
|
||||
// double avgSpeed;
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS) {
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS && !FEIER && !MX_AS && !FTMS) {
|
||||
Speed = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index)))) /
|
||||
100.0;
|
||||
@@ -292,7 +292,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
}
|
||||
|
||||
if (Flags.totDistance) {
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS) {
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS && !FEIER && !MX_AS && !FTMS) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)lastPacket.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)lastPacket.at(index)))) /
|
||||
@@ -314,7 +314,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
double divisor = 1.0;
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS)
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS)
|
||||
divisor = 2.0;
|
||||
Cadence = (((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index))))) / divisor;
|
||||
@@ -382,7 +382,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
double divisor = 100.0; // i added this because this device seems to send it multiplied by 100
|
||||
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS)
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS)
|
||||
divisor = 1.0;
|
||||
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
@@ -396,7 +396,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
if (Flags.avgPower && lastPacket.length() > index + 1 && !E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS) { // E35 has a bug about this
|
||||
if (Flags.avgPower && lastPacket.length() > index + 1 && !E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS && !FEIER && !MX_AS && !FTMS) { // E35 has a bug about this
|
||||
double avgPower;
|
||||
avgPower = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index))));
|
||||
@@ -541,7 +541,7 @@ void ypooelliptical::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
qDebug() << "skipping service" << s->serviceUuid();
|
||||
continue;
|
||||
}
|
||||
else if(s->serviceUuid() != _gattFTMSService && (SCH_590E || MYELLIPTICAL || SKANDIKA || DOMYOS)) {
|
||||
else if(s->serviceUuid() != _gattFTMSService && (SCH_590E || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS)) {
|
||||
qDebug() << "skipping service" << s->serviceUuid();
|
||||
continue;
|
||||
}
|
||||
@@ -768,6 +768,19 @@ void ypooelliptical::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("DOMYOS-EL"))) {
|
||||
DOMYOS = true;
|
||||
qDebug() << "DOMYOS workaround ON!";
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("FEIER-EM-"))) {
|
||||
FEIER = true;
|
||||
qDebug() << "FEIER workaround ON!";
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("MX-AS "))) {
|
||||
MX_AS = true;
|
||||
qDebug() << "MX_AS workaround ON!";
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
QString ftms_elliptical_setting = settings.value(QZSettings::ftms_elliptical, QZSettings::default_ftms_elliptical).toString();
|
||||
if(ftms_elliptical_setting != QStringLiteral("Disabled") && device.name().toUpper() == ftms_elliptical_setting.toUpper()) {
|
||||
FTMS = true;
|
||||
qDebug() << "FTMS Elliptical workaround ON!";
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
|
||||
@@ -83,6 +83,9 @@ class ypooelliptical : public elliptical {
|
||||
bool MYELLIPTICAL = false;
|
||||
bool SKANDIKA = false;
|
||||
bool DOMYOS = false;
|
||||
bool FEIER = false;
|
||||
bool MX_AS = false;
|
||||
bool FTMS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
@@ -184,6 +184,36 @@ class ergTable : public QObject {
|
||||
return sameResPoints.first().wattage;
|
||||
}
|
||||
|
||||
uint16_t resistanceFromPowerRequest(uint16_t power, uint16_t cadence, uint16_t maxResistance) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << cadence;
|
||||
|
||||
if (cadence == 0)
|
||||
return 1;
|
||||
|
||||
uint16_t best_resistance_match = 1;
|
||||
int min_watt_difference = 1000;
|
||||
|
||||
for (uint16_t i = 1; i < maxResistance; i++) {
|
||||
uint16_t current_watts = estimateWattage(cadence, i);
|
||||
uint16_t next_watts = estimateWattage(cadence, i + 1);
|
||||
|
||||
if (current_watts <= power && next_watts >= power) {
|
||||
qDebug() << current_watts << next_watts << power;
|
||||
return i;
|
||||
}
|
||||
|
||||
int diff = abs(current_watts - power);
|
||||
if (diff < min_watt_difference) {
|
||||
min_watt_difference = diff;
|
||||
best_resistance_match = i;
|
||||
qDebug() << QStringLiteral("best match") << best_resistance_match << "with watts" << current_watts << "diff" << diff;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Bracketing not found, best match:" << best_resistance_match;
|
||||
return best_resistance_match;
|
||||
}
|
||||
|
||||
QList<ergDataPoint> getConsolidatedData() const {
|
||||
return consolidatedData;
|
||||
}
|
||||
@@ -192,6 +222,18 @@ class ergTable : public QObject {
|
||||
return wattageData;
|
||||
}
|
||||
|
||||
uint16_t getMaxResistance() const {
|
||||
if (consolidatedData.isEmpty()) return 0;
|
||||
|
||||
uint16_t maxRes = 0;
|
||||
for (const auto& point : consolidatedData) {
|
||||
if (point.resistance > maxRes) {
|
||||
maxRes = point.resistance;
|
||||
}
|
||||
}
|
||||
return maxRes;
|
||||
}
|
||||
|
||||
private:
|
||||
QMap<CadenceResistancePair, WattageStats> wattageData;
|
||||
QList<ergDataPoint> consolidatedData;
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// The following FIT Protocol software provided may be used with FIT protocol
|
||||
// devices only and remains the copyrighted property of Garmin International, Inc.
|
||||
// The software is being provided on an "as-is" basis and as an accommodation,
|
||||
// and therefore all warranties, representations, or guarantees of any kind
|
||||
// (whether express, implied or statutory) including, without limitation,
|
||||
// warranties of merchantability, non-infringement, or fitness for a particular
|
||||
// purpose, are specifically disclaimed.
|
||||
//
|
||||
// Copyright 2022 Garmin International, Inc.
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2025 Garmin International, Inc.
|
||||
// Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
|
||||
// may not use this file except in compliance with the Flexible and Interoperable Data
|
||||
// Transfer (FIT) Protocol License.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ****WARNING**** This file is auto-generated! Do NOT edit this file.
|
||||
// Profile Version = 21.84Release
|
||||
// Tag = production/akw/21.84.00-0-g894a113c
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Profile Version = 21.171.0Release
|
||||
// Tag = production/release/21.171.0-0-g57fed75
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#include "fit.hpp"
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// The following FIT Protocol software provided may be used with FIT protocol
|
||||
// devices only and remains the copyrighted property of Garmin International, Inc.
|
||||
// The software is being provided on an "as-is" basis and as an accommodation,
|
||||
// and therefore all warranties, representations, or guarantees of any kind
|
||||
// (whether express, implied or statutory) including, without limitation,
|
||||
// warranties of merchantability, non-infringement, or fitness for a particular
|
||||
// purpose, are specifically disclaimed.
|
||||
//
|
||||
// Copyright 2022 Garmin International, Inc.
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2025 Garmin International, Inc.
|
||||
// Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
|
||||
// may not use this file except in compliance with the Flexible and Interoperable Data
|
||||
// Transfer (FIT) Protocol License.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ****WARNING**** This file is auto-generated! Do NOT edit this file.
|
||||
// Profile Version = 21.84Release
|
||||
// Tag = production/akw/21.84.00-0-g894a113c
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Profile Version = 21.171.0Release
|
||||
// Tag = production/release/21.171.0-0-g57fed75
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
@@ -85,9 +80,9 @@ namespace fit
|
||||
#define FIT_PROTOCOL_VERSION ::fit::versionMap.at( ::fit::ProtocolVersion::V20 ).GetVersionByte()
|
||||
|
||||
#define FIT_PROFILE_VERSION_MAJOR 21
|
||||
#define FIT_PROFILE_VERSION_MINOR 84
|
||||
#define FIT_PROFILE_VERSION_SCALE 100
|
||||
#define FIT_PROFILE_VERSION ((FIT_UINT16) (FIT_PROFILE_VERSION_MAJOR * 100 + FIT_PROFILE_VERSION_MINOR))
|
||||
#define FIT_PROFILE_VERSION_MINOR 171
|
||||
#define FIT_PROFILE_VERSION_SCALE 1000
|
||||
#define FIT_PROFILE_VERSION ((FIT_UINT16) (FIT_PROFILE_VERSION_MAJOR * FIT_PROFILE_VERSION_SCALE + FIT_PROFILE_VERSION_MINOR))
|
||||
|
||||
#define FIT_HEADER_SIZE_NO_CRC 12
|
||||
#define FIT_HEADER_SIZE_WITH_CRC 14
|
||||
|
||||
248
src/fit-sdk/fit_aad_accel_features_mesg.hpp
Normal file
248
src/fit-sdk/fit_aad_accel_features_mesg.hpp
Normal file
@@ -0,0 +1,248 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2025 Garmin International, Inc.
|
||||
// Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
|
||||
// may not use this file except in compliance with the Flexible and Interoperable Data
|
||||
// Transfer (FIT) Protocol License.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ****WARNING**** This file is auto-generated! Do NOT edit this file.
|
||||
// Profile Version = 21.171.0Release
|
||||
// Tag = production/release/21.171.0-0-g57fed75
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#if !defined(FIT_AAD_ACCEL_FEATURES_MESG_HPP)
|
||||
#define FIT_AAD_ACCEL_FEATURES_MESG_HPP
|
||||
|
||||
#include "fit_mesg.hpp"
|
||||
|
||||
namespace fit
|
||||
{
|
||||
|
||||
class AadAccelFeaturesMesg : public Mesg
|
||||
{
|
||||
public:
|
||||
class FieldDefNum final
|
||||
{
|
||||
public:
|
||||
static const FIT_UINT8 Timestamp = 253;
|
||||
static const FIT_UINT8 Time = 0;
|
||||
static const FIT_UINT8 EnergyTotal = 1;
|
||||
static const FIT_UINT8 ZeroCrossCnt = 2;
|
||||
static const FIT_UINT8 Instance = 3;
|
||||
static const FIT_UINT8 TimeAboveThreshold = 4;
|
||||
static const FIT_UINT8 Invalid = FIT_FIELD_NUM_INVALID;
|
||||
};
|
||||
|
||||
AadAccelFeaturesMesg(void) : Mesg(Profile::MESG_AAD_ACCEL_FEATURES)
|
||||
{
|
||||
}
|
||||
|
||||
AadAccelFeaturesMesg(const Mesg &mesg) : Mesg(mesg)
|
||||
{
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Checks the validity of timestamp field
|
||||
// Returns FIT_TRUE if field is valid
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_BOOL IsTimestampValid() const
|
||||
{
|
||||
const Field* field = GetField(253);
|
||||
if( FIT_NULL == field )
|
||||
{
|
||||
return FIT_FALSE;
|
||||
}
|
||||
|
||||
return field->IsValueValid();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Returns timestamp field
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_DATE_TIME GetTimestamp(void) const
|
||||
{
|
||||
return GetFieldUINT32Value(253, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Set timestamp field
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
void SetTimestamp(FIT_DATE_TIME timestamp)
|
||||
{
|
||||
SetFieldUINT32Value(253, timestamp, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Checks the validity of time field
|
||||
// Returns FIT_TRUE if field is valid
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_BOOL IsTimeValid() const
|
||||
{
|
||||
const Field* field = GetField(0);
|
||||
if( FIT_NULL == field )
|
||||
{
|
||||
return FIT_FALSE;
|
||||
}
|
||||
|
||||
return field->IsValueValid();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Returns time field
|
||||
// Units: s
|
||||
// Comment: Time interval length in seconds
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_UINT16 GetTime(void) const
|
||||
{
|
||||
return GetFieldUINT16Value(0, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Set time field
|
||||
// Units: s
|
||||
// Comment: Time interval length in seconds
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
void SetTime(FIT_UINT16 time)
|
||||
{
|
||||
SetFieldUINT16Value(0, time, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Checks the validity of energy_total field
|
||||
// Returns FIT_TRUE if field is valid
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_BOOL IsEnergyTotalValid() const
|
||||
{
|
||||
const Field* field = GetField(1);
|
||||
if( FIT_NULL == field )
|
||||
{
|
||||
return FIT_FALSE;
|
||||
}
|
||||
|
||||
return field->IsValueValid();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Returns energy_total field
|
||||
// Comment: Total accelerometer energy in the interval
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_UINT32 GetEnergyTotal(void) const
|
||||
{
|
||||
return GetFieldUINT32Value(1, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Set energy_total field
|
||||
// Comment: Total accelerometer energy in the interval
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
void SetEnergyTotal(FIT_UINT32 energyTotal)
|
||||
{
|
||||
SetFieldUINT32Value(1, energyTotal, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Checks the validity of zero_cross_cnt field
|
||||
// Returns FIT_TRUE if field is valid
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_BOOL IsZeroCrossCntValid() const
|
||||
{
|
||||
const Field* field = GetField(2);
|
||||
if( FIT_NULL == field )
|
||||
{
|
||||
return FIT_FALSE;
|
||||
}
|
||||
|
||||
return field->IsValueValid();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Returns zero_cross_cnt field
|
||||
// Comment: Count of zero crossings
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_UINT16 GetZeroCrossCnt(void) const
|
||||
{
|
||||
return GetFieldUINT16Value(2, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Set zero_cross_cnt field
|
||||
// Comment: Count of zero crossings
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
void SetZeroCrossCnt(FIT_UINT16 zeroCrossCnt)
|
||||
{
|
||||
SetFieldUINT16Value(2, zeroCrossCnt, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Checks the validity of instance field
|
||||
// Returns FIT_TRUE if field is valid
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_BOOL IsInstanceValid() const
|
||||
{
|
||||
const Field* field = GetField(3);
|
||||
if( FIT_NULL == field )
|
||||
{
|
||||
return FIT_FALSE;
|
||||
}
|
||||
|
||||
return field->IsValueValid();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Returns instance field
|
||||
// Comment: Instance ID of zero crossing algorithm
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_UINT8 GetInstance(void) const
|
||||
{
|
||||
return GetFieldUINT8Value(3, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Set instance field
|
||||
// Comment: Instance ID of zero crossing algorithm
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
void SetInstance(FIT_UINT8 instance)
|
||||
{
|
||||
SetFieldUINT8Value(3, instance, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Checks the validity of time_above_threshold field
|
||||
// Returns FIT_TRUE if field is valid
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_BOOL IsTimeAboveThresholdValid() const
|
||||
{
|
||||
const Field* field = GetField(4);
|
||||
if( FIT_NULL == field )
|
||||
{
|
||||
return FIT_FALSE;
|
||||
}
|
||||
|
||||
return field->IsValueValid();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Returns time_above_threshold field
|
||||
// Units: s
|
||||
// Comment: Total accelerometer time above threshold in the interval
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_FLOAT32 GetTimeAboveThreshold(void) const
|
||||
{
|
||||
return GetFieldFLOAT32Value(4, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Set time_above_threshold field
|
||||
// Units: s
|
||||
// Comment: Total accelerometer time above threshold in the interval
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
void SetTimeAboveThreshold(FIT_FLOAT32 timeAboveThreshold)
|
||||
{
|
||||
SetFieldFLOAT32Value(4, timeAboveThreshold, 0, FIT_SUBFIELD_INDEX_MAIN_FIELD);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace fit
|
||||
|
||||
#endif // !defined(FIT_AAD_ACCEL_FEATURES_MESG_HPP)
|
||||
30
src/fit-sdk/fit_aad_accel_features_mesg_listener.hpp
Normal file
30
src/fit-sdk/fit_aad_accel_features_mesg_listener.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2025 Garmin International, Inc.
|
||||
// Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
|
||||
// may not use this file except in compliance with the Flexible and Interoperable Data
|
||||
// Transfer (FIT) Protocol License.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ****WARNING**** This file is auto-generated! Do NOT edit this file.
|
||||
// Profile Version = 21.171.0Release
|
||||
// Tag = production/release/21.171.0-0-g57fed75
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#if !defined(FIT_AAD_ACCEL_FEATURES_MESG_LISTENER_HPP)
|
||||
#define FIT_AAD_ACCEL_FEATURES_MESG_LISTENER_HPP
|
||||
|
||||
#include "fit_aad_accel_features_mesg.hpp"
|
||||
|
||||
namespace fit
|
||||
{
|
||||
|
||||
class AadAccelFeaturesMesgListener
|
||||
{
|
||||
public:
|
||||
virtual ~AadAccelFeaturesMesgListener() {}
|
||||
virtual void OnMesg(AadAccelFeaturesMesg& mesg) = 0;
|
||||
};
|
||||
|
||||
} // namespace fit
|
||||
|
||||
#endif // !defined(FIT_AAD_ACCEL_FEATURES_MESG_LISTENER_HPP)
|
||||
@@ -1,18 +1,13 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// The following FIT Protocol software provided may be used with FIT protocol
|
||||
// devices only and remains the copyrighted property of Garmin International, Inc.
|
||||
// The software is being provided on an "as-is" basis and as an accommodation,
|
||||
// and therefore all warranties, representations, or guarantees of any kind
|
||||
// (whether express, implied or statutory) including, without limitation,
|
||||
// warranties of merchantability, non-infringement, or fitness for a particular
|
||||
// purpose, are specifically disclaimed.
|
||||
//
|
||||
// Copyright 2022 Garmin International, Inc.
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2025 Garmin International, Inc.
|
||||
// Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you
|
||||
// may not use this file except in compliance with the Flexible and Interoperable Data
|
||||
// Transfer (FIT) Protocol License.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ****WARNING**** This file is auto-generated! Do NOT edit this file.
|
||||
// Profile Version = 21.84Release
|
||||
// Tag = production/akw/21.84.00-0-g894a113c
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Profile Version = 21.171.0Release
|
||||
// Tag = production/release/21.171.0-0-g57fed75
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#if !defined(FIT_ACCELEROMETER_DATA_MESG_HPP)
|
||||
@@ -148,7 +143,7 @@ public:
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Returns sample_time_offset field
|
||||
// Units: ms
|
||||
// Comment: Each time in the array describes the time at which the accelerometer sample with the corresponding index was taken. Limited to 30 samples in each message. The samples may span across seconds. Array size must match the number of samples in accel_x and accel_y and accel_z
|
||||
// Comment: Each time in the array describes the time at which the accelerometer sample with the corrosponding index was taken. Limited to 30 samples in each message. The samples may span across seconds. Array size must match the number of samples in accel_x and accel_y and accel_z
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
FIT_UINT16 GetSampleTimeOffset(FIT_UINT8 index) const
|
||||
{
|
||||
@@ -158,7 +153,7 @@ public:
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Set sample_time_offset field
|
||||
// Units: ms
|
||||
// Comment: Each time in the array describes the time at which the accelerometer sample with the corresponding index was taken. Limited to 30 samples in each message. The samples may span across seconds. Array size must match the number of samples in accel_x and accel_y and accel_z
|
||||
// Comment: Each time in the array describes the time at which the accelerometer sample with the corrosponding index was taken. Limited to 30 samples in each message. The samples may span across seconds. Array size must match the number of samples in accel_x and accel_y and accel_z
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
void SetSampleTimeOffset(FIT_UINT8 index, FIT_UINT16 sampleTimeOffset)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user