mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
299 Commits
version-2.
...
build-1179
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3158514af | ||
|
|
cf0dc2d00d | ||
|
|
164aa38cb1 | ||
|
|
14998d0f25 | ||
|
|
ca74fe7ccd | ||
|
|
facba11bae | ||
|
|
8b8302fb53 | ||
|
|
eaea4bf8b8 | ||
|
|
3d9c3e4103 | ||
|
|
e840d7b3e9 | ||
|
|
3a248ad2c5 | ||
|
|
5912d7df2d | ||
|
|
94842114e6 | ||
|
|
d83df0ba5a | ||
|
|
0764fb50b2 | ||
|
|
bb5de868ab | ||
|
|
2b8fe6c28d | ||
|
|
0153e09f0d | ||
|
|
dc44433d7c | ||
|
|
8bdefdb331 | ||
|
|
c7f5e320fc | ||
|
|
c1582cc763 | ||
|
|
f2f0f7a793 | ||
|
|
3d665e397e | ||
|
|
194f8686f3 | ||
|
|
fb79d0ddd6 | ||
|
|
d7e0a4e441 | ||
|
|
465123a156 | ||
|
|
88d01562b1 | ||
|
|
85421f41b8 | ||
|
|
a67cb10633 | ||
|
|
f00a161fc1 | ||
|
|
c071c56eb7 | ||
|
|
b39f769423 | ||
|
|
dde526c059 | ||
|
|
c223d6e81d | ||
|
|
d531a1d313 | ||
|
|
b0722cc827 | ||
|
|
2e534abfbb | ||
|
|
6d3ca9877a | ||
|
|
f477cb32ab | ||
|
|
51b79ed413 | ||
|
|
fa78f03f0a | ||
|
|
a40fec4082 | ||
|
|
f6a9d8ca4e | ||
|
|
dd2bfc4e1b | ||
|
|
06fd78378e | ||
|
|
f28574245c | ||
|
|
b964c523dd | ||
|
|
0721bc3ec5 | ||
|
|
3f783305b2 | ||
|
|
be29180e48 | ||
|
|
19c65d7d90 | ||
|
|
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 |
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
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -130,6 +130,7 @@
|
||||
87097D31275EA9AF0020EE6F /* moc_sportsplusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */; };
|
||||
870A5DB32CEF8FB100839641 /* moc_technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */; };
|
||||
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB42CEF8FD200839641 /* technogymbike.cpp */; };
|
||||
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870C72632E91565E00DC8A84 /* ios_liveactivity.mm */; };
|
||||
8710706C29C48AEA0094D0F3 /* handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706B29C48AEA0094D0F3 /* handleurl.cpp */; };
|
||||
8710706E29C48AF30094D0F3 /* moc_handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */; };
|
||||
8710707329C4A5E70094D0F3 /* GarminConnect.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710707229C4A5E70094D0F3 /* GarminConnect.swift */; };
|
||||
@@ -286,6 +287,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 +343,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 +393,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 +462,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,8 +590,20 @@
|
||||
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 */; };
|
||||
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87EFC5652E918D35005BB573 /* WidgetKit.framework */; };
|
||||
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */; };
|
||||
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */; };
|
||||
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
|
||||
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
|
||||
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
|
||||
87EFE45B27A51901006EA1C3 /* moc_nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */; };
|
||||
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
|
||||
@@ -689,6 +711,13 @@
|
||||
remoteGlobalIDString = 876E4E102594747F00BD5714;
|
||||
remoteInfo = watchkit;
|
||||
};
|
||||
87EFC5742E918D38005BB573 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 6DB9C3763D02B1415CD9D565 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 87EFC5632E918D35005BB573;
|
||||
remoteInfo = QZWidgetExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -714,6 +743,17 @@
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
99542592E9780B9225F24AA8 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -959,6 +999,8 @@
|
||||
87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusbike.cpp; sourceTree = "<group>"; };
|
||||
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymbike.cpp; sourceTree = "<group>"; };
|
||||
870A5DB42CEF8FD200839641 /* technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = technogymbike.cpp; path = ../src/devices/technogymbike/technogymbike.cpp; sourceTree = SOURCE_ROOT; };
|
||||
870C72622E91565E00DC8A84 /* ios_liveactivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_liveactivity.h; path = ../src/ios/ios_liveactivity.h; sourceTree = SOURCE_ROOT; };
|
||||
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_liveactivity.mm; path = ../src/ios/ios_liveactivity.mm; sourceTree = SOURCE_ROOT; };
|
||||
8710706A29C48AE90094D0F3 /* handleurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = handleurl.h; path = ../src/handleurl.h; sourceTree = "<group>"; };
|
||||
8710706B29C48AEA0094D0F3 /* handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = handleurl.cpp; path = ../src/handleurl.cpp; sourceTree = "<group>"; };
|
||||
8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_handleurl.cpp; sourceTree = "<group>"; };
|
||||
@@ -1196,6 +1238,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 +1327,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 +1413,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 +1523,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,9 +1708,23 @@
|
||||
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>"; };
|
||||
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QZWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
87EFC5652E918D35005BB573 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveActivityBridge.swift; path = ../src/ios/LiveActivityBridge.swift; sourceTree = SOURCE_ROOT; };
|
||||
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QZWidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QZWorkoutAttributes.swift; path = QZWidget/QZWorkoutAttributes.swift; sourceTree = "<group>"; };
|
||||
87EFE45727A518F5006EA1C3 /* nautiluselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nautiluselliptical.h; path = ../src/devices/nautiluselliptical/nautiluselliptical.h; sourceTree = "<group>"; };
|
||||
87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nautiluselliptical.cpp; path = ../src/devices/nautiluselliptical/nautiluselliptical.cpp; sourceTree = "<group>"; };
|
||||
87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nautiluselliptical.cpp; sourceTree = "<group>"; };
|
||||
@@ -1892,6 +1961,20 @@
|
||||
FF5BDAB0076F3391B219EA52 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = /System/Library/Frameworks/SystemConfiguration.framework; sourceTree = "<absolute>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
87EFC5682E918D35005BB573 /* QZWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = QZWidget; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
876E4E172594748000BD5714 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
@@ -1900,10 +1983,20 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC5612E918D35005BB573 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */,
|
||||
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D1C883685E82D5676953459A /* Link Binary With Libraries */ = {
|
||||
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 +2333,31 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */,
|
||||
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */,
|
||||
870C72622E91565E00DC8A84 /* ios_liveactivity.h */,
|
||||
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */,
|
||||
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 +3058,7 @@
|
||||
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */,
|
||||
8768C9262BBC12D10099DBE1 /* libcrypto.a */,
|
||||
87FA94682B6B8A5A00B6AB9A /* libSystem.B.tbd */,
|
||||
87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */,
|
||||
@@ -3071,6 +3190,7 @@
|
||||
4D765E1B1EA6C757220C63E7 /* CoreFoundation.framework */,
|
||||
FCC237CA5AD60B9BA4447615 /* Foundation.framework */,
|
||||
344F66310C19536DB4886D8F /* qtpcre2 */,
|
||||
87EFC5652E918D35005BB573 /* WidgetKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -3333,6 +3453,7 @@
|
||||
E8C543AB96796ECAA2E65C57 /* qdomyoszwift */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */,
|
||||
8768C8CE2BBC12170099DBE1 /* adb */,
|
||||
87BAC3BE2BA497160003E925 /* PrivacyInfo.xcprivacy */,
|
||||
8745B2752AFCB4A300991A39 /* android */,
|
||||
@@ -3343,6 +3464,7 @@
|
||||
74B182DB50CB5611B5C1C297 /* Supporting Files */,
|
||||
876E4E122594747F00BD5714 /* watchkit */,
|
||||
876E4E1E2594748000BD5714 /* watchkit Extension */,
|
||||
87EFC5682E918D35005BB573 /* QZWidget */,
|
||||
AF39DD055C3EF8226FBE929D /* Frameworks */,
|
||||
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
|
||||
FE0A091FDBFB3E9C31B7A1BD /* Products */,
|
||||
@@ -3357,6 +3479,7 @@
|
||||
040B10E2EF2CEF79F2205FE2 /* qdomyoszwift.app */,
|
||||
876E4E112594747F00BD5714 /* watchkit.app */,
|
||||
876E4E1A2594748000BD5714 /* watchkit Extension.appex */,
|
||||
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -3373,11 +3496,13 @@
|
||||
30414803F31797EB689AE508 /* Copy Bundle Resources */,
|
||||
99542592E9780B9225F24AA8 /* Embed Frameworks */,
|
||||
876E4E332594748100BD5714 /* Embed Watch Content */,
|
||||
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
876E4E312594748100BD5714 /* PBXTargetDependency */,
|
||||
87EFC5752E918D38005BB573 /* PBXTargetDependency */,
|
||||
);
|
||||
name = qdomyoszwift;
|
||||
packageProductDependencies = (
|
||||
@@ -3423,6 +3548,28 @@
|
||||
productReference = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */;
|
||||
productType = "com.apple.product-type.watchkit2-extension";
|
||||
};
|
||||
87EFC5632E918D35005BB573 /* QZWidgetExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */;
|
||||
buildPhases = (
|
||||
87EFC5602E918D35005BB573 /* Sources */,
|
||||
87EFC5612E918D35005BB573 /* Frameworks */,
|
||||
87EFC5622E918D35005BB573 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
87EFC5682E918D35005BB573 /* QZWidget */,
|
||||
);
|
||||
name = QZWidgetExtension;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = QZWidgetExtension;
|
||||
productReference = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -3430,7 +3577,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
DefaultBuildSystemTypeForWorkspace = Original;
|
||||
LastSwiftUpdateCheck = 1220;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
TargetAttributes = {
|
||||
799833E5566DEFFC37E4BF1E = {
|
||||
DevelopmentTeam = 6335M7T29D;
|
||||
@@ -3446,6 +3593,9 @@
|
||||
DevelopmentTeam = 6335M7T29D;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
87EFC5632E918D35005BB573 = {
|
||||
CreatedOnToolsVersion = 26.0.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */;
|
||||
@@ -3467,6 +3617,7 @@
|
||||
799833E5566DEFFC37E4BF1E /* qdomyoszwift */,
|
||||
876E4E102594747F00BD5714 /* watchkit */,
|
||||
876E4E192594748000BD5714 /* watchkit Extension */,
|
||||
87EFC5632E918D35005BB573 /* QZWidgetExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -3513,6 +3664,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC5622E918D35005BB573 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -3530,10 +3688,19 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC5602E918D35005BB573 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F7E50F631C51CD5B5DC0BC43 /* Compile Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */,
|
||||
8738249627E646E3004F1B46 /* characteristicnotifier2acd.cpp in Compile Sources */,
|
||||
8738249127E646E3004F1B46 /* dirconpacket.cpp in Compile Sources */,
|
||||
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */,
|
||||
@@ -3680,12 +3847,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 */,
|
||||
@@ -3778,6 +3949,7 @@
|
||||
87943AB429E0215D007575F2 /* localipaddress.cpp in Compile Sources */,
|
||||
87EB917627EE5FB3002535E1 /* nautilusbike.cpp in Compile Sources */,
|
||||
ACB47DC464A2BC9D39C544AD /* gpx.cpp in Compile Sources */,
|
||||
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */,
|
||||
6361329E515248BB41640C07 /* homeform.cpp in Compile Sources */,
|
||||
87A18F072660D5C1002D7C96 /* ftmsrower.cpp in Compile Sources */,
|
||||
87C5F0D026285E7E0067A1B5 /* moc_smtpclient.cpp in Compile Sources */,
|
||||
@@ -3805,6 +3977,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 +4042,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 +4060,9 @@
|
||||
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 */,
|
||||
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift 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 */,
|
||||
@@ -4058,6 +4241,11 @@
|
||||
target = 876E4E102594747F00BD5714 /* watchkit */;
|
||||
targetProxy = 876E4E302594748100BD5714 /* PBXContainerItemProxy */;
|
||||
};
|
||||
87EFC5752E918D38005BB573 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
|
||||
targetProxy = 87EFC5742E918D38005BB573 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -4381,7 +4569,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 = 1179;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
|
||||
@@ -4417,6 +4606,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 +4652,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",
|
||||
@@ -4556,7 +4747,11 @@
|
||||
QMAKE_PKGINFO_TYPEINFO = "????";
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = "";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
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 +4770,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 = 1179;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
@@ -4613,6 +4809,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 +4855,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",
|
||||
@@ -4753,7 +4951,11 @@
|
||||
QMAKE_PKGINFO_TYPEINFO = "????";
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = _debug;
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
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 +5007,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 = 1178;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4830,7 +5032,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 +5103,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 = 1178;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -4922,7 +5124,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 +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 = 1179;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5038,7 +5240,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 +5311,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 = 1179;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5150,7 +5352,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";
|
||||
@@ -5181,6 +5383,184 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
87EFC5792E918D38005BB573 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1179;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QZWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SwiftUI,
|
||||
"-framework",
|
||||
WidgetKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
87EFC57A2E918D38005BB573 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1179;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QZWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SwiftUI,
|
||||
"-framework",
|
||||
WidgetKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -5211,6 +5591,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
87EFC5792E918D38005BB573 /* Debug */,
|
||||
87EFC57A2E918D38005BB573 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -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 + ");
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class BluetoothHandler : public QObject
|
||||
void onKeyPressed(int keyCode)
|
||||
{
|
||||
qDebug() << "Key pressed:" << keyCode;
|
||||
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == BIKE) {
|
||||
if (keyCode == 115) // up
|
||||
((bike*)m_bluetooth->device())->setGears(((bike*)m_bluetooth->device())->gears() + 1);
|
||||
else if (keyCode == 114) // down
|
||||
|
||||
@@ -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
|
||||
|
||||
20
src/Home.qml
20
src/Home.qml
@@ -72,7 +72,19 @@ HomeForm {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("New lap started!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: stopConfirmationDialog
|
||||
text: qsTr("Stop Workout")
|
||||
informativeText: qsTr("Do you really want to stop the current workout?")
|
||||
buttons: (MessageDialog.Yes | MessageDialog.No)
|
||||
onYesClicked: {
|
||||
close();
|
||||
inner_stop();
|
||||
}
|
||||
onNoClicked: close()
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -141,7 +153,11 @@ HomeForm {
|
||||
|
||||
start.onClicked: { start_clicked(); }
|
||||
stop.onClicked: {
|
||||
inner_stop();
|
||||
if (rootItem.confirmStopEnabled()) {
|
||||
stopConfirmationDialog.open();
|
||||
} else {
|
||||
inner_stop();
|
||||
}
|
||||
}
|
||||
lap.onClicked: { lap_clicked(); popupLap.open(); popupLapAutoClose.running = true; }
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -845,7 +845,6 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 0.5;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
@@ -904,7 +903,6 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 1;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
|
||||
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.19.0" android:versionCode="1102" 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
|
||||
6
src/bluetoothdevicetype.h
Normal file
6
src/bluetoothdevicetype.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef BLUETOOTHDEVICETYPE_H
|
||||
#define BLUETOOTHDEVICETYPE_H
|
||||
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
|
||||
|
||||
#endif // BLUETOOTHDEVICETYPE_H
|
||||
@@ -5,7 +5,7 @@ CharacteristicNotifier2A53::CharacteristicNotifier2A53(bluetoothdevice *Bike, QO
|
||||
: CharacteristicNotifier(0x2a53, parent), Bike(Bike) {}
|
||||
|
||||
int CharacteristicNotifier2A53::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
value.append(0x02); // total distance
|
||||
uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256;
|
||||
uint32_t distance = Bike->odometer() * 10000.0;
|
||||
|
||||
@@ -8,7 +8,7 @@ int CharacteristicNotifier2A63::notify(QByteArray &value) {
|
||||
if (normalizeWattage < 0)
|
||||
normalizeWattage = 0;
|
||||
|
||||
if (Bike->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (Bike->deviceType() == BIKE) {
|
||||
/*
|
||||
// set measurement
|
||||
measurement[2] = power & 0xFF;
|
||||
|
||||
@@ -7,8 +7,8 @@ CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QO
|
||||
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
|
||||
|
||||
int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == TREADMILL || dt == ELLIPTICAL) {
|
||||
value.append(0x0C); // Inclination available and distance for peloton
|
||||
//value.append((char)0x01); // heart rate available
|
||||
value.append((char)0x05); // HeartRate(8) | ElapsedTime(10)
|
||||
@@ -46,7 +46,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
inclination /= gain;
|
||||
}
|
||||
|
||||
if (dt == bluetoothdevice::TREADMILL)
|
||||
if (dt == TREADMILL)
|
||||
normalizeIncline = (uint32_t)qRound(inclination * 10);
|
||||
a = (normalizeIncline >> 8) & 0XFF;
|
||||
b = normalizeIncline & 0XFF;
|
||||
@@ -54,7 +54,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
inclineBytes.append(b);
|
||||
inclineBytes.append(a);
|
||||
double ramp = 0;
|
||||
if (dt == bluetoothdevice::TREADMILL)
|
||||
if (dt == TREADMILL)
|
||||
ramp = qRadiansToDegrees(qAtan(inclination / 100));
|
||||
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
|
||||
a = (normalizeRamp >> 8) & 0XFF;
|
||||
|
||||
@@ -8,12 +8,12 @@ CharacteristicNotifier2AD2::CharacteristicNotifier2AD2(bluetoothdevice *Bike, QO
|
||||
: CharacteristicNotifier(0x2ad2, parent), Bike(Bike) {}
|
||||
|
||||
int CharacteristicNotifier2AD2::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
|
||||
QSettings settings;
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
bool rowerAsABike = !virtual_device_rower && dt == bluetoothdevice::ROWING;
|
||||
bool rowerAsABike = !virtual_device_rower && dt == ROWING;
|
||||
bool double_cadence = settings.value(QZSettings::powr_sensor_running_cadence_double, QZSettings::default_powr_sensor_running_cadence_double).toBool();
|
||||
double cadence_multiplier = 2.0;
|
||||
if (double_cadence)
|
||||
@@ -24,7 +24,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
|
||||
if (normalizeWattage < 0)
|
||||
normalizeWattage = 0;
|
||||
|
||||
if (dt == bluetoothdevice::BIKE || rowerAsABike) {
|
||||
if (dt == BIKE || rowerAsABike) {
|
||||
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
|
||||
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
|
||||
value.append((char)0x02); // heart rate
|
||||
@@ -44,7 +44,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
|
||||
value.append(char(Bike->currentHeart().value())); // Actual value.
|
||||
value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix
|
||||
return CN_OK;
|
||||
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL || dt == bluetoothdevice::ROWING) {
|
||||
} else if (dt == TREADMILL || dt == ELLIPTICAL || dt == ROWING) {
|
||||
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
|
||||
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
|
||||
value.append((char)0x02); // heart rate
|
||||
@@ -53,11 +53,11 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
|
||||
value.append((char)(normalizeSpeed >> 8) & 0xFF); // speed
|
||||
|
||||
uint16_t cadence = 0;
|
||||
if (dt == bluetoothdevice::ELLIPTICAL)
|
||||
if (dt == ELLIPTICAL)
|
||||
cadence = ((elliptical *)Bike)->currentCadence().value();
|
||||
else if (dt == bluetoothdevice::TREADMILL)
|
||||
else if (dt == TREADMILL)
|
||||
cadence = ((treadmill *)Bike)->currentCadence().value();
|
||||
else if (dt == bluetoothdevice::ROWING)
|
||||
else if (dt == ROWING)
|
||||
cadence = ((rower *)Bike)->currentCadence().value();
|
||||
|
||||
value.append((char)((uint16_t)(cadence * cadence_multiplier) & 0xFF)); // cadence
|
||||
|
||||
@@ -10,7 +10,7 @@ CharacteristicWriteProcessor::CharacteristicWriteProcessor(double bikeResistance
|
||||
void CharacteristicWriteProcessor::changePower(uint16_t power) { Bike->changePower(power); }
|
||||
|
||||
void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr, uint8_t cw) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
QSettings settings;
|
||||
bool force_resistance =
|
||||
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
|
||||
@@ -64,7 +64,7 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
|
||||
|
||||
qDebug() << "changeSlope CRR = " << fCRR << CRR_offset << "CW = " << fCW;
|
||||
|
||||
if (dt == bluetoothdevice::BIKE) {
|
||||
if (dt == BIKE) {
|
||||
|
||||
// if the bike doesn't have the inclination by hardware, i'm simulating inclination with the value received
|
||||
// from Zwift
|
||||
@@ -82,9 +82,9 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
|
||||
Bike->changeResistance((resistance_t)(round(resistance * bikeResistanceGain)) + bikeResistanceOffset + 1 +
|
||||
CRR_offset + CW_offset); // resistance start from 1
|
||||
}
|
||||
} else if (dt == bluetoothdevice::TREADMILL) {
|
||||
} else if (dt == TREADMILL) {
|
||||
emit changeInclination(grade, percentage);
|
||||
} else if (dt == bluetoothdevice::ELLIPTICAL) {
|
||||
} else if (dt == ELLIPTICAL) {
|
||||
bool inclinationAvailableByHardware = ((elliptical *)Bike)->inclinationAvailableByHardware();
|
||||
qDebug() << "inclinationAvailableByHardware" << inclinationAvailableByHardware << "erg_mode" << erg_mode;
|
||||
emit changeInclination(grade, percentage);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -13,8 +13,8 @@ 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) {
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == BIKE || dt == 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");
|
||||
|
||||
@@ -76,7 +82,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
|
||||
reply.append((quint8)cmd);
|
||||
reply.append((quint8)FTMS_NOT_SUPPORTED);
|
||||
}
|
||||
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
|
||||
char a, b;
|
||||
if ((char)data.at(0) == 0x02) {
|
||||
// Set Target Speed
|
||||
@@ -85,7 +91,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
|
||||
|
||||
uint16_t uspeed = a + (((uint16_t)b) << 8);
|
||||
double requestSpeed = (double)uspeed / 100.0;
|
||||
if (dt == bluetoothdevice::TREADMILL) {
|
||||
if (dt == TREADMILL) {
|
||||
((treadmill *)Bike)->changeSpeed(requestSpeed);
|
||||
}
|
||||
qDebug() << QStringLiteral("new requested speed ") + QString::number(requestSpeed);
|
||||
@@ -97,10 +103,10 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
|
||||
int16_t sincline = a + (((int16_t)b) << 8);
|
||||
double requestIncline = (double)sincline / 10.0;
|
||||
|
||||
if (dt == bluetoothdevice::TREADMILL)
|
||||
if (dt == TREADMILL)
|
||||
((treadmill *)Bike)->changeInclination(requestIncline, requestIncline);
|
||||
// Resistance as incline on Sole E95s Elliptical #419
|
||||
else if (dt == bluetoothdevice::ELLIPTICAL) {
|
||||
else if (dt == ELLIPTICAL) {
|
||||
if(((elliptical *)Bike)->inclinationAvailableByHardware())
|
||||
((elliptical *)Bike)->changeInclination(requestIncline, requestIncline);
|
||||
else
|
||||
|
||||
@@ -13,8 +13,8 @@ CharacteristicWriteProcessorE005::CharacteristicWriteProcessorE005(double bikeRe
|
||||
|
||||
int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
|
||||
if (data.size()) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::BIKE) {
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == BIKE) {
|
||||
char cmd = data.at(0);
|
||||
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(), data);
|
||||
if (cmd == wahookickrsnapbike::_setSimMode && data.count() >= 7) {
|
||||
@@ -35,7 +35,7 @@ int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArra
|
||||
qDebug() << "erg mode" << watts;
|
||||
changePower(watts);
|
||||
}
|
||||
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
|
||||
}
|
||||
reply.append((quint8)FTMS_RESPONSE_CODE);
|
||||
reply.append((quint8)data.at(0));
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
activiotreadmill::activiotreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
android_antbike::android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
antbike::antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
apexbike::apexbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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;
|
||||
|
||||
@@ -19,7 +19,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
bhfitnesselliptical::bhfitnesselliptical(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -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) {
|
||||
@@ -173,7 +211,7 @@ resistance_t bike::resistanceFromPowerRequest(uint16_t power) { return power / 1
|
||||
void bike::cadenceSensor(uint8_t cadence) { Cadence.setValue(cadence); }
|
||||
void bike::powerSensor(uint16_t power) { m_watt.setValue(power, false); }
|
||||
|
||||
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }
|
||||
BLUETOOTH_TYPE bike::deviceType() { return BIKE; }
|
||||
|
||||
void bike::clearStats() {
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class bike : public bluetoothdevice {
|
||||
virtual resistance_t resistanceFromPowerRequest(uint16_t power);
|
||||
virtual uint16_t powerFromResistanceRequest(resistance_t requestResistance);
|
||||
virtual bool ergManagedBySS2K() { return false; }
|
||||
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
|
||||
BLUETOOTH_TYPE deviceType() override;
|
||||
metric pelotonResistance();
|
||||
void clearStats() override;
|
||||
void setLap() override;
|
||||
@@ -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;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
bkoolbike::bkoolbike(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -49,7 +49,12 @@ void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
|
||||
if (gattWriteCharCustomId.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
|
||||
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer,
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
} else {
|
||||
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
|
||||
}
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
|
||||
@@ -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());
|
||||
@@ -423,7 +450,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool() ||
|
||||
settings.value(QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike).toBool() ||
|
||||
settings.value(QZSettings::toorx_bike_srx_500, QZSettings::default_toorx_bike_srx_500).toBool() ||
|
||||
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool()) &&
|
||||
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool() ||
|
||||
settings.value(QZSettings::taurua_ic90, QZSettings::default_taurua_ic90).toBool()) &&
|
||||
!toorx_ftms;
|
||||
bool snode_bike = settings.value(QZSettings::snode_bike, QZSettings::default_snode_bike).toBool();
|
||||
bool fitplus_bike = settings.value(QZSettings::fitplus_bike, QZSettings::default_fitplus_bike).toBool() ||
|
||||
@@ -470,6 +498,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 +522,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();
|
||||
@@ -655,7 +685,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
|
||||
filter = (b.name().compare(filterDevice, Qt::CaseInsensitive) == 0);
|
||||
}
|
||||
if (b.name().startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
|
||||
const QString deviceName = b.name();
|
||||
const QString upperDeviceName = deviceName.toUpper();
|
||||
bool isTrxAppGateUsbBikeTC = false;
|
||||
if (upperDeviceName.startsWith(QStringLiteral("TC")) && deviceName.length() == 5) {
|
||||
isTrxAppGateUsbBikeTC = true;
|
||||
for (int idx = 2; idx < deviceName.length(); ++idx) {
|
||||
if (!deviceName.at(idx).isDigit()) {
|
||||
isTrxAppGateUsbBikeTC = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deviceName.startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
|
||||
|
||||
if (m3ibike::isCorrectUnit(b)) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -890,6 +932,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) {
|
||||
@@ -972,7 +1028,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(domyosBike);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower &&
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower)) &&
|
||||
!trxappgateusbRower && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -991,7 +1048,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 +1084,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 +1109,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 +1389,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();
|
||||
@@ -1438,13 +1500,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("AB300S-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("TF04-")) || // Sport Synology Z5 Treadmill #2415
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FIT-")) && !b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) || // FIT-1596 and sports tech f37s treadmill #2412
|
||||
b.name().toUpper().startsWith(QStringLiteral("FIT-TM-")) || // FIT-TM- treadmill with real inclination
|
||||
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
|
||||
(b.name().toUpper().startsWith("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("TP1")) && b.name().length() == 3) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-AP-"))) || // Kingsmith WalkingPad R3 Hybrid+
|
||||
(b.name().toUpper().startsWith(QStringLiteral("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))) ||
|
||||
@@ -1453,7 +1521,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
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
|
||||
@@ -1464,7 +1531,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("S77")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F89")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM XP_")) // FTMS
|
||||
) &&
|
||||
!horizonTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1698,14 +1766,18 @@ 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("ORLAUF_ARES")) ||
|
||||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
|
||||
(b.name().toUpper().startsWith("XCX-")) ||
|
||||
(b.name().toUpper().startsWith("D500V2")) ||
|
||||
(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"))) ||
|
||||
(b.name().toUpper().startsWith("L-") && b.name().length() == 11) ||
|
||||
(b.name().toUpper().startsWith("DMASUN-") && b.name().toUpper().endsWith("-BIKE")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) ||
|
||||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
|
||||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
@@ -1715,8 +1787,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) ||
|
||||
(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) {
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10)) &&
|
||||
ftms_rower.contains(QZSettings::default_ftms_rower) &&
|
||||
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
ftmsBike = new ftmsbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
@@ -1730,6 +1804,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) {
|
||||
@@ -1744,7 +1819,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();
|
||||
@@ -1791,7 +1867,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")) &&
|
||||
@@ -1849,6 +1927,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")) ||
|
||||
@@ -2005,7 +2084,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(apexBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
apexBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(apexBike);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) && !bkoolBike && filter) {
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) && !bkoolBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
bkoolBike = new bkoolbike(noWriteResistance, noHeartService);
|
||||
@@ -2385,16 +2465,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(activioTreadmill, &activiotreadmill::debug, this, &bluetooth::debug);
|
||||
activioTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(activioTreadmill);
|
||||
} else if (((b.name().startsWith(QStringLiteral("TOORX"))) ||
|
||||
(b.name().startsWith(QStringLiteral("V-RUN"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("K80_"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-RUNNING"))) ||
|
||||
(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 &&
|
||||
} else if (((deviceName.startsWith(QStringLiteral("TOORX"))) ||
|
||||
(deviceName.startsWith(QStringLiteral("V-RUN"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("K80_"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("ICONSOLE+"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("I-RUNNING"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("DKN RUN"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("ADIDAS "))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("REEBOK")))) &&
|
||||
!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();
|
||||
@@ -2406,24 +2487,25 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(trxappgateusb, &trxappgateusbtreadmill::debug, this, &bluetooth::debug);
|
||||
trxappgateusb->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(trxappgateusb);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("TUN ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FITHIWAY")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FIT HI WAY")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BIKZU_")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("PASYOU-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("VIRTUFIT")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("IBIKING+")) ||
|
||||
((b.name().startsWith(QStringLiteral("TOORX")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOIE+")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("VIFHTR2.1")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK"))) ||
|
||||
b.name().toUpper().contains(QStringLiteral("CR011R")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
} else if (isTrxAppGateUsbBikeTC ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("TUN ")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("FITHIWAY")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("FIT HI WAY")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("BIKZU_")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("PASYOU-")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIRTUFIT")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("IBIKING+")) ||
|
||||
((deviceName.startsWith(QStringLiteral("TOORX")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOIE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("ICONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIFHTR2.1")) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("REEBOK"))) ||
|
||||
upperDeviceName.contains(QStringLiteral("CR011R")) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
(toorx_bike))) &&
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower) {
|
||||
!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 =
|
||||
@@ -2459,6 +2541,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);
|
||||
@@ -2526,7 +2609,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) ||
|
||||
(b.name().toUpper().startsWith("H9110 OSAKA")) ||
|
||||
b.name().startsWith(QStringLiteral("MRK-"))) &&
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && filter) {
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
fitPlusBike =
|
||||
@@ -2558,6 +2641,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) {
|
||||
@@ -2672,11 +2756,11 @@ void bluetooth::connectedAndDiscovered() {
|
||||
settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
|
||||
|
||||
// only at the first very connection, setting the user default resistance
|
||||
if (device() && firstConnected && device()->deviceType() == bluetoothdevice::BIKE &&
|
||||
if (device() && firstConnected && device()->deviceType() == BIKE &&
|
||||
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() != 1) {
|
||||
qobject_cast<bike *>(device())->changeResistance(
|
||||
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt());
|
||||
} else if (device() && firstConnected && device()->deviceType() == bluetoothdevice::ELLIPTICAL &&
|
||||
} else if (device() && firstConnected && device()->deviceType() == ELLIPTICAL &&
|
||||
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() !=
|
||||
1) {
|
||||
qobject_cast<elliptical *>(device())->changeResistance(
|
||||
@@ -2844,7 +2928,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
#else
|
||||
settings.setValue(QZSettings::power_sensor_address, b.deviceUuid().toString());
|
||||
#endif
|
||||
if (device() && device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (device() && device()->deviceType() == BIKE) {
|
||||
powerSensor = new stagesbike(false, false, true);
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
|
||||
@@ -2853,7 +2937,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
connect(powerSensor, &bluetoothdevice::cadenceChanged, this->device(),
|
||||
&bluetoothdevice::cadenceSensor);
|
||||
powerSensor->deviceDiscovered(b);
|
||||
} else if (device() && device()->deviceType() == bluetoothdevice::TREADMILL) {
|
||||
} else if (device() && device()->deviceType() == TREADMILL) {
|
||||
powerSensorRun = new strydrunpowersensor(false, false, true);
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
|
||||
@@ -2910,7 +2994,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().startsWith(eliteSterzoSmartName))) && !eliteSterzoSmart &&
|
||||
!eliteSterzoSmartName.startsWith(QStringLiteral("Disabled")) && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
settings.setValue(QZSettings::elite_sterzo_smart_lastdevice_name, b.name());
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
@@ -2932,7 +3016,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
if(settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("SRAM "))) && !sramAXSController && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
sramAXSController = new sramaxscontroller();
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
@@ -2951,7 +3035,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
if(settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("ZWIFT CLICK"))) && !zwiftClickRemote && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
if(b.manufacturerData(2378).size() > 0) {
|
||||
qDebug() << "this should be 9. is it? " << int(b.manufacturerData(2378).at(0));
|
||||
@@ -2976,7 +3060,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
eliteSquareController = new elitesquarecontroller(this->device());
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
@@ -2996,7 +3080,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool();
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if ((((b.name().toUpper().startsWith("ZWIFT PLAY"))) || b.name().toUpper().startsWith("ZWIFT RIDE") || b.name().toUpper().startsWith("ZWIFT SF2")) && zwiftPlayDevice.size() < 2 && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
if(b.manufacturerData(2378).size() > 0) {
|
||||
qDebug() << "this should be 3 or 2. is it? " << int(b.manufacturerData(2378).at(0));
|
||||
@@ -3028,18 +3112,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());
|
||||
device()->deviceType() == TREADMILL ||
|
||||
device()->deviceType() == ELLIPTICAL,
|
||||
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()) {
|
||||
@@ -3327,6 +3416,11 @@ void bluetooth::restart() {
|
||||
delete nordictrackifitadbElliptical;
|
||||
nordictrackifitadbElliptical = nullptr;
|
||||
}
|
||||
if (nordictrackifitadbRower) {
|
||||
|
||||
delete nordictrackifitadbRower;
|
||||
nordictrackifitadbRower = nullptr;
|
||||
}
|
||||
if (powerBike) {
|
||||
|
||||
delete powerBike;
|
||||
@@ -3801,6 +3895,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) {
|
||||
@@ -4041,7 +4137,7 @@ void bluetooth::stateFileUpdate() {
|
||||
if (!device()) {
|
||||
return;
|
||||
}
|
||||
if (device()->deviceType() != bluetoothdevice::TREADMILL) {
|
||||
if (device()->deviceType() != TREADMILL) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,7 @@ bluetoothdevice::~bluetoothdevice() {
|
||||
}
|
||||
}
|
||||
|
||||
bluetoothdevice::BLUETOOTH_TYPE bluetoothdevice::deviceType() { return bluetoothdevice::UNKNOWN; }
|
||||
BLUETOOTH_TYPE bluetoothdevice::deviceType() { return UNKNOWN; }
|
||||
void bluetoothdevice::start() { requestStart = 1; lastStart = QDateTime::currentMSecsSinceEpoch(); }
|
||||
void bluetoothdevice::stop(bool pause) {
|
||||
requestStop = 1;
|
||||
@@ -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) {
|
||||
@@ -192,7 +237,7 @@ void bluetoothdevice::update_metrics(bool watt_calc, const double watts, const b
|
||||
!power_as_bike && !power_as_treadmill)
|
||||
watt_calc = false;
|
||||
|
||||
if(deviceType() == bluetoothdevice::BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
|
||||
if(deviceType() == BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
|
||||
_ergTable.collectData(Cadence.value(), m_watt.value(), Resistance.value());
|
||||
|
||||
if (!_firstUpdate && !paused) {
|
||||
@@ -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,17 @@ 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;
|
||||
QSettings settings;
|
||||
bool useMiles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value(), useMiles);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void bluetoothdevice::clearStats() {
|
||||
@@ -279,6 +345,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 +372,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 +397,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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef BLUETOOTHDEVICE_H
|
||||
#define BLUETOOTHDEVICE_H
|
||||
|
||||
#include "bluetoothdevicetype.h"
|
||||
#include "definitions.h"
|
||||
#include "metric.h"
|
||||
#include "qzsettings.h"
|
||||
@@ -108,11 +109,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
|
||||
@@ -443,7 +452,6 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
void setTargetPowerZone(double pz) { TargetPowerZone = pz; }
|
||||
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
|
||||
enum WORKOUT_EVENT_STATE { STARTED = 0, PAUSED = 1, RESUMED = 2, STOPPED = 3 };
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@ bowflext216treadmill::bowflext216treadmill(uint32_t pollDeviceTime, bool noConso
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -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;
|
||||
|
||||
@@ -23,7 +23,7 @@ bowflextreadmill::bowflextreadmill(uint32_t pollDeviceTime, bool noConsole, bool
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
//#include <QtBluetooth/private/qlowenergyserviceprivate_p.h>
|
||||
|
||||
chronobike::chronobike(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
t_timeout = new QTimer(this);
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
computrainerbike::computrainerbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
QSettings settings;
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
target_watts.setType(metric::METRIC_WATT);
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
concept2skierg::concept2skierg(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -23,7 +23,7 @@ crossrope::crossrope(uint32_t pollDeviceTime, bool noConsole, bool noHeartServic
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -4,7 +4,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
csafeelliptical::csafeelliptical(bool noWriteResistance, bool noHeartService, bool noVirtualDevice,
|
||||
int8_t bikeResistanceOffset, double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
csaferower::csaferower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
cscbike::cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
cycleopsphantombike::cycleopsphantombike(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
deerruntreadmill::deerruntreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -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;
|
||||
|
||||
@@ -171,9 +171,9 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
|
||||
QSettings settings;
|
||||
DirconProcessorService *service;
|
||||
QList<DirconProcessorService *> services, proc_services;
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
bt = Bike;
|
||||
uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
|
||||
uint8_t type = dt == TREADMILL || dt == ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
|
||||
: DM_MACHINE_TYPE_BIKE;
|
||||
qDebug() << "Building Dircom Manager";
|
||||
uint16_t server_base_port =
|
||||
@@ -236,7 +236,7 @@ double DirconManager::currentGear() {
|
||||
QSettings settings;
|
||||
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool() && writeP0003)
|
||||
return writeP0003->currentGear();
|
||||
else if(bt && bt->deviceType() == bluetoothdevice::BIKE)
|
||||
else if(bt && bt->deviceType() == BIKE)
|
||||
return ((bike*)bt)->gears();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testResistance, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
domyoselliptical::domyoselliptical(bool noWriteResistance, bool noHeartService, bool testResistance,
|
||||
int8_t bikeResistanceOffset, double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
domyosrower::domyosrower(bool noWriteResistance, bool noHeartService, bool testResistance, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool n
|
||||
#ifdef Q_OS_IOS
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -23,7 +23,7 @@ echelonconnectsport::echelonconnectsport(bool noWriteResistance, bool noHeartSer
|
||||
#ifdef Q_OS_IOS
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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;
|
||||
|
||||
@@ -23,7 +23,7 @@ echelonrower::echelonrower(bool noWriteResistance, bool noHeartService, int8_t b
|
||||
#ifdef Q_OS_IOS
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
speedRaw.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
|
||||
@@ -15,7 +15,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
echelonstairclimber::echelonstairclimber(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -15,7 +15,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
echelonstride::echelonstride(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
eliterizer::eliterizer(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
elitesterzosmart::elitesterzosmart(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -22,11 +22,15 @@ void elliptical::update_metrics(bool watt_calc, const double watts) {
|
||||
WeightLoss = metric::calculateWeightLoss(KCal.value());
|
||||
WattKg = m_watt.value() / settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
} else if (m_watt.value() > 0) {
|
||||
m_watt = 0;
|
||||
if (watt_calc) {
|
||||
m_watt = 0;
|
||||
}
|
||||
WattKg = 0;
|
||||
}
|
||||
} else if (m_watt.value() > 0) {
|
||||
m_watt = 0;
|
||||
if (watt_calc) {
|
||||
m_watt = 0;
|
||||
}
|
||||
WattKg = 0;
|
||||
}
|
||||
|
||||
@@ -137,7 +141,7 @@ metric elliptical::currentInclination() { return Inclination; }
|
||||
uint8_t elliptical::fanSpeed() { return FanSpeed; }
|
||||
bool elliptical::connected() { return false; }
|
||||
|
||||
bluetoothdevice::BLUETOOTH_TYPE elliptical::deviceType() { return bluetoothdevice::ELLIPTICAL; }
|
||||
BLUETOOTH_TYPE elliptical::deviceType() { return ELLIPTICAL; }
|
||||
|
||||
void elliptical::clearStats() {
|
||||
moving.clear(true);
|
||||
|
||||
@@ -24,7 +24,7 @@ class elliptical : public bluetoothdevice {
|
||||
virtual int pelotonToEllipticalResistance(int pelotonResistance);
|
||||
virtual bool inclinationAvailableByHardware();
|
||||
virtual bool inclinationSeparatedFromResistance();
|
||||
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
|
||||
BLUETOOTH_TYPE deviceType() override;
|
||||
void clearStats() override;
|
||||
void setPaused(bool p) override;
|
||||
void setLap() override;
|
||||
|
||||
@@ -71,7 +71,7 @@ class CRC8
|
||||
|
||||
eslinkertreadmill::eslinkertreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService,
|
||||
double forceInitSpeed, double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -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);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
fakebike::fakebike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
fakeelliptical::fakeelliptical(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
fakerower::fakerower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
faketreadmill::faketreadmill(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -30,7 +30,7 @@ fitplusbike::fitplusbike(bool noWriteResistance, bool noHeartService, int8_t bik
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
}
|
||||
#endif
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ fitshowtreadmill::fitshowtreadmill(uint32_t pollDeviceTime, bool noConsole, bool
|
||||
double forceInitInclination) {
|
||||
Q_UNUSED(noConsole)
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
flywheelbike::flywheelbike(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -24,7 +24,7 @@ focustreadmill::focustreadmill(uint32_t pollDeviceTime, bool noConsole, bool noH
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = false;
|
||||
#endif
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -26,7 +26,7 @@ using namespace std::chrono_literals;
|
||||
ftmsbike::ftmsbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
QSettings settings;
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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,26 +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
|
||||
if(DU30_bike)
|
||||
return max_resistance;
|
||||
else
|
||||
return _ergTable.getMaxResistance();
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
@@ -249,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 {
|
||||
@@ -267,7 +254,7 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
if(SL010)
|
||||
Resistance = requestResistance;
|
||||
|
||||
if(JFBK5_0 || DIRETO_XR) {
|
||||
if(JFBK5_0 || DIRETO_XR || YPBM) {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00, 0x00};
|
||||
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
|
||||
write[2] = ((uint16_t)requestResistance * 10) >> 8;
|
||||
@@ -414,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);
|
||||
@@ -669,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) {
|
||||
@@ -745,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 {
|
||||
@@ -805,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()));
|
||||
@@ -871,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;
|
||||
}
|
||||
@@ -954,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
|
||||
@@ -963,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
|
||||
@@ -1095,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() &&
|
||||
@@ -1142,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()) {
|
||||
@@ -1171,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)) {
|
||||
@@ -1226,6 +1432,20 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
} else if(b.at(0) == FTMS_SET_TARGET_POWER && ((zwiftPlayService != nullptr && gears_zwift_ratio) || !ergModeSupported)) {
|
||||
qDebug() << "discarding";
|
||||
return;
|
||||
} else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) {
|
||||
// handling watt gain and offset for erg mode
|
||||
double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
|
||||
double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
|
||||
|
||||
if (watt_gain != 1.0 || watt_offset != 0) {
|
||||
uint16_t powerRequested = (((uint8_t)b.at(1)) + (b.at(2) << 8));
|
||||
qDebug() << "applying watt_gain/watt_offset from" << powerRequested;
|
||||
powerRequested = ((powerRequested / watt_gain) - watt_offset);
|
||||
qDebug() << "to" << powerRequested;
|
||||
|
||||
b[1] = powerRequested & 0xFF;
|
||||
b[2] = powerRequested >> 8;
|
||||
}
|
||||
}
|
||||
// gears on erg mode is quite useless and it's confusing
|
||||
/* else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) {
|
||||
@@ -1325,7 +1545,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;
|
||||
@@ -1336,6 +1558,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");
|
||||
@@ -1369,6 +1592,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;
|
||||
@@ -1377,11 +1601,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;
|
||||
@@ -1400,8 +1626,36 @@ 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;
|
||||
} else if(device.name().toUpper().startsWith("YPBM") && device.name().length() == 10) {
|
||||
qDebug() << QStringLiteral("YPBM found");
|
||||
YPBM = true;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
max_resistance = 32;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {
|
||||
resistance_lvl_mode = true;
|
||||
}
|
||||
@@ -1458,7 +1712,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,23 @@ 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;
|
||||
bool YPBM = 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
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
ftmsrower::ftmsrower(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
horizongr7bike::horizongr7bike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -27,7 +27,7 @@ horizontreadmill::horizontreadmill(bool noWriteResistance, bool noHeartService)
|
||||
|
||||
testProfileCRC();
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -948,7 +948,7 @@ void horizontreadmill::update() {
|
||||
requestInclination = treadmillInclinationOverrideReverse(requestInclination);
|
||||
|
||||
// this treadmill doesn't send the incline, so i'm forcing it manually
|
||||
if(schwinn_810_treadmill) {
|
||||
if(schwinn_810_treadmill || FIT_TM) {
|
||||
Inclination = requestInclination;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1624,6 +1624,18 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
Speed = 0;
|
||||
horizonPaused = true;
|
||||
qDebug() << "stop from the treadmill";
|
||||
} else if (TP1 && characteristic.uuid() == QBluetoothUuid((quint16)0x2ADA) && newValue.length() == 2 &&
|
||||
(uint8_t)newValue.at(0) == 0x02 && (uint8_t)newValue.at(1) == 0x01) {
|
||||
// TP1 treadmill start command received
|
||||
qDebug() << "TP1 treadmill: received start packet from treadmill, sending start command";
|
||||
emit debug(QStringLiteral("TP1 treadmill: received start packet from treadmill, sending start command"));
|
||||
|
||||
if (gattFTMSService) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), "requestControl", false, false);
|
||||
write[0] = {FTMS_START_RESUME};
|
||||
writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), "start TP1", false, false);
|
||||
}
|
||||
} else if (characteristic.uuid() == QBluetoothUuid((quint16)0x2AD2)) {
|
||||
union flags {
|
||||
struct {
|
||||
@@ -2551,6 +2563,9 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("MX-TM "))) {
|
||||
qDebug() << QStringLiteral("MX-TM found");
|
||||
MX_TM = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("FIT-TM-"))) {
|
||||
qDebug() << QStringLiteral("FIT-TM- found (real inclination)");
|
||||
FIT_TM = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("FIT-"))) {
|
||||
qDebug() << QStringLiteral("FIT- found");
|
||||
FIT = true;
|
||||
@@ -2560,6 +2575,9 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("3G ELITE "))) {
|
||||
qDebug() << QStringLiteral("3G ELITE");
|
||||
T3G_ELITE = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("TP1")) && device.name().length() == 3) {
|
||||
qDebug() << QStringLiteral("TP1 treadmill found");
|
||||
TP1 = true;
|
||||
}
|
||||
|
||||
if (device.name().toUpper().startsWith(QStringLiteral("TRX3500"))) {
|
||||
|
||||
@@ -108,8 +108,10 @@ class horizontreadmill : public treadmill {
|
||||
bool YPOO_MINI_PRO = false;
|
||||
bool MX_TM = false;
|
||||
bool FIT = false;
|
||||
bool FIT_TM = false;
|
||||
bool T3G_PRO = false;
|
||||
bool T3G_ELITE = false;
|
||||
bool TP1 = false;
|
||||
|
||||
void testProfileCRC();
|
||||
void updateProfileCRC();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user