mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
346 Commits
fixing_tra
...
android36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
777ccefc5e | ||
|
|
17a4c838b7 | ||
|
|
aecb0c97df | ||
|
|
9214ab63ad | ||
|
|
04f919fe86 | ||
|
|
eae3f59e4a | ||
|
|
9ad0137190 | ||
|
|
f62548ac60 | ||
|
|
986bc2252e | ||
|
|
7d744ee874 | ||
|
|
8f87074e69 | ||
|
|
acd141d32b | ||
|
|
a106438fb0 | ||
|
|
c336043bb2 | ||
|
|
b4fb3e339a | ||
|
|
1336460297 | ||
|
|
3a45b64d51 | ||
|
|
49be559ae8 | ||
|
|
f70f4717f7 | ||
|
|
f2da15c477 | ||
|
|
e3e15bf24d | ||
|
|
83690df66e | ||
|
|
cafc785420 | ||
|
|
0b1e6606cb | ||
|
|
82d9e252ac | ||
|
|
b9a7ddcaa0 | ||
|
|
1a6ef489d0 | ||
|
|
d6028c33b6 | ||
|
|
66554e0a4a | ||
|
|
bfd6de1d49 | ||
|
|
aa1058f6be | ||
|
|
33589f27d3 | ||
|
|
ae1147025e | ||
|
|
189c27eeb0 | ||
|
|
dab74e4bfd | ||
|
|
447cb04376 | ||
|
|
29dd4cf10a | ||
|
|
c4ea190370 | ||
|
|
8267661f70 | ||
|
|
204897680c | ||
|
|
b93df0d0c9 | ||
|
|
038c347cc2 | ||
|
|
4683084ed2 | ||
|
|
7e51db80e6 | ||
|
|
d475e50489 | ||
|
|
1c4a22041d | ||
|
|
b07ffac325 | ||
|
|
d1dab0cd79 | ||
|
|
f5a55a253e | ||
|
|
8f5b5bd5b7 | ||
|
|
9d946dd1c5 | ||
|
|
d6c65dd7d8 | ||
|
|
8d607ca0ba | ||
|
|
253c00f014 | ||
|
|
a032a5d51c | ||
|
|
b0c2fa5b17 | ||
|
|
7cf05276e9 | ||
|
|
708f28fffb | ||
|
|
c50b7655ba | ||
|
|
9db2b8c235 | ||
|
|
134a228473 | ||
|
|
f1ad62ce4a | ||
|
|
c28a16d8d2 | ||
|
|
83e229e55f | ||
|
|
bd9bbdb236 | ||
|
|
24095fc8cc | ||
|
|
a199ba3c2b | ||
|
|
bf07764bca | ||
|
|
1f7ce9b724 | ||
|
|
51ea12782d | ||
|
|
d7c2339783 | ||
|
|
0bed0ca76c | ||
|
|
eb4ae28fa7 | ||
|
|
d1827a48ee | ||
|
|
f5c1b175a5 | ||
|
|
76b14cccb4 | ||
|
|
19efaf6878 | ||
|
|
61a96dbc63 | ||
|
|
656f2b366b | ||
|
|
ee336436e7 | ||
|
|
739270b944 | ||
|
|
bcaf466fa1 | ||
|
|
196f77d22a | ||
|
|
077c995a08 | ||
|
|
6db0aba44d | ||
|
|
80fc8b8d46 | ||
|
|
fb2676ef37 | ||
|
|
9281e91f67 | ||
|
|
540108a207 | ||
|
|
27299a7805 | ||
|
|
52957c61a4 | ||
|
|
b06ac89f6e | ||
|
|
55921ed367 | ||
|
|
e89c668a96 | ||
|
|
43ca65baf2 | ||
|
|
eaba07cafd | ||
|
|
5b57f2c8aa | ||
|
|
5fea12e6bc | ||
|
|
dcdd4fdbb6 | ||
|
|
aac9f7a20e | ||
|
|
5146352494 | ||
|
|
f666c4cc55 | ||
|
|
882778b9ff | ||
|
|
783b832805 | ||
|
|
defb177f53 | ||
|
|
d511df4186 | ||
|
|
ffe79b434a | ||
|
|
9050be6063 | ||
|
|
744d2d138f | ||
|
|
612acf3610 | ||
|
|
a84e4ca84d | ||
|
|
e4b4dd943e | ||
|
|
121a046bb8 | ||
|
|
21e0a7edc8 | ||
|
|
83e1c136e4 | ||
|
|
9a19956b1c | ||
|
|
18571131ca | ||
|
|
eba9a2c8d5 | ||
|
|
dbf5b7005d | ||
|
|
64476f7e61 | ||
|
|
f111e9c4e4 | ||
|
|
0f7b240f4a | ||
|
|
63ee0f6d71 | ||
|
|
e38b00b735 | ||
|
|
97a5de93d8 | ||
|
|
9aabaddf6e | ||
|
|
2fb9d6043a | ||
|
|
d1afc5c6b3 | ||
|
|
2270d93419 | ||
|
|
4e3e6de6c1 | ||
|
|
201877f214 | ||
|
|
0941d3f218 | ||
|
|
7f8876d021 | ||
|
|
62c7e2125a | ||
|
|
13b2d6a18b | ||
|
|
2e854d8f1f | ||
|
|
093f5cbe33 | ||
|
|
b9da86d1ce | ||
|
|
5ac449a178 | ||
|
|
179e60b40b | ||
|
|
5603dc4259 | ||
|
|
b8d703d94f | ||
|
|
8ce49beefa | ||
|
|
2437c4c30c | ||
|
|
0029258fa5 | ||
|
|
8541ec0242 | ||
|
|
0c138d2c07 | ||
|
|
4f70b5d160 | ||
|
|
e0bcafa804 | ||
|
|
330e0b3725 | ||
|
|
46c12af44d | ||
|
|
74db47ba16 | ||
|
|
3bc848cdf4 | ||
|
|
a8d58a733e | ||
|
|
37c6c03ada | ||
|
|
f258b9e0ae | ||
|
|
6ec3936bf1 | ||
|
|
0f0991f956 | ||
|
|
4649744cb4 | ||
|
|
755c1765b4 | ||
|
|
6df9fe6acb | ||
|
|
32180bf1bd | ||
|
|
a373f41ee0 | ||
|
|
bb63e0c3cf | ||
|
|
e84f6d0468 | ||
|
|
b08cf90bdb | ||
|
|
9939056115 | ||
|
|
9ea09aca32 | ||
|
|
65e15ab29b | ||
|
|
1d1e63c40d | ||
|
|
7e854f5bb1 | ||
|
|
fa42eadf43 | ||
|
|
7ee4f85f67 | ||
|
|
afa02fab3f | ||
|
|
c10703316c | ||
|
|
93b09d64b0 | ||
|
|
12663907d2 | ||
|
|
f72717d440 | ||
|
|
c94174a994 | ||
|
|
2789c2bf0f | ||
|
|
0c69226747 | ||
|
|
9dc65861cc | ||
|
|
b14d917f3d | ||
|
|
23fb91b4d2 | ||
|
|
110eea144b | ||
|
|
f413068074 | ||
|
|
917d559ebf | ||
|
|
8d52455574 | ||
|
|
3602fa566c | ||
|
|
a53239df97 | ||
|
|
6154158254 | ||
|
|
f91b25a177 | ||
|
|
650a74dd8c | ||
|
|
d44f57285f | ||
|
|
33b1dcf0f8 | ||
|
|
e914be7ee0 | ||
|
|
b2ea3b0525 | ||
|
|
3c5c0518b4 | ||
|
|
2b1e11d2e0 | ||
|
|
b3eab44f50 | ||
|
|
bec8776c63 | ||
|
|
519e39d54b | ||
|
|
8e4ab441c0 | ||
|
|
6522baccd6 | ||
|
|
be851955b6 | ||
|
|
4083059155 | ||
|
|
9ace6dd570 | ||
|
|
b0677a768d | ||
|
|
dad99fe6bf | ||
|
|
b5c48bd9f7 | ||
|
|
8e73dd7a7c | ||
|
|
905c06771b | ||
|
|
255dbde832 | ||
|
|
eaaaee2b4b | ||
|
|
7227d32c3b | ||
|
|
988419b7e2 | ||
|
|
1d1401b1d6 | ||
|
|
282667c6e3 | ||
|
|
fa13ba1d72 | ||
|
|
a2ad67fbac | ||
|
|
5e42345889 | ||
|
|
684ed7d7e9 | ||
|
|
2ff3c1d3b4 | ||
|
|
e0ea3b2fe0 | ||
|
|
35fe2c3fd3 | ||
|
|
0ec6f99429 | ||
|
|
dbe407e784 | ||
|
|
93f0c714fc | ||
|
|
a1b57654c6 | ||
|
|
624234dc92 | ||
|
|
cdda64575a | ||
|
|
483a31d984 | ||
|
|
6d88e7e831 | ||
|
|
bb97e49982 | ||
|
|
4f8090e5bf | ||
|
|
8c18d6f179 | ||
|
|
7d615b4e65 | ||
|
|
e264a4c887 | ||
|
|
0991495f51 | ||
|
|
94c19d70eb | ||
|
|
805639bbfa | ||
|
|
a900ab939d | ||
|
|
34881df30a | ||
|
|
b35995a925 | ||
|
|
ea9a7170ca | ||
|
|
4e41a8aaac | ||
|
|
b718eb3f4c | ||
|
|
8b03718422 | ||
|
|
e0bb6f3c95 | ||
|
|
4b1649b26a | ||
|
|
20bd6dbdc4 | ||
|
|
2c6088e96c | ||
|
|
db3961443c | ||
|
|
83c3cea88f | ||
|
|
850f680e32 | ||
|
|
dcc0b3cbd8 | ||
|
|
fb372cee89 | ||
|
|
916af395b6 | ||
|
|
27b9ef9216 | ||
|
|
c85463b728 | ||
|
|
9561cc0269 | ||
|
|
e69c1689ec | ||
|
|
18497e92fd | ||
|
|
f692621565 | ||
|
|
934a1089db | ||
|
|
b0dfeb6e5f | ||
|
|
448702b081 | ||
|
|
8ed9117bee | ||
|
|
439bb30bbd | ||
|
|
fc29cd7051 | ||
|
|
51aca98536 | ||
|
|
2ab3f0b442 | ||
|
|
cb9105682e | ||
|
|
84d46e3aee | ||
|
|
9a97dc5221 | ||
|
|
ee810b9e0c | ||
|
|
516a96a4a8 | ||
|
|
506a9c0896 | ||
|
|
71196983ca | ||
|
|
fc600873b4 | ||
|
|
49e588e60e | ||
|
|
dc5beebb24 | ||
|
|
58d4e8b456 | ||
|
|
ca89072273 | ||
|
|
ff5d8baa1a | ||
|
|
9ac09db4c3 | ||
|
|
18e845f99f | ||
|
|
e6307dec97 | ||
|
|
d2941d94bc | ||
|
|
ba132a0546 | ||
|
|
d3bbd836f6 | ||
|
|
8777c2df64 | ||
|
|
e123c94e8a | ||
|
|
7b3af6ac90 | ||
|
|
ce522a75b5 | ||
|
|
24720e02f0 | ||
|
|
f34430348a | ||
|
|
667cdb1520 | ||
|
|
966bcfadab | ||
|
|
3c13f211f2 | ||
|
|
0033d261d3 | ||
|
|
e1ed32af92 | ||
|
|
0d51f4c1d4 | ||
|
|
4c0ffd483e | ||
|
|
3e4751070f | ||
|
|
4b6611ae2b | ||
|
|
1a58636cd4 | ||
|
|
11dc65bdf1 | ||
|
|
387a8f0efe | ||
|
|
0847f9237f | ||
|
|
2f5818ba5a | ||
|
|
d502f983c0 | ||
|
|
af79605268 | ||
|
|
0192da5a92 | ||
|
|
e563a9dc21 | ||
|
|
8f5d969f61 | ||
|
|
3a8fb33dd6 | ||
|
|
0bc2d74dcc | ||
|
|
13da5056be | ||
|
|
ecb2d98ad7 | ||
|
|
e88811c1fd | ||
|
|
56c3ab74cb | ||
|
|
ae131c7cad | ||
|
|
67f5446fc2 | ||
|
|
4ce8e1b20c | ||
|
|
57cdf246a9 | ||
|
|
4cc0b2520b | ||
|
|
8f4364f525 | ||
|
|
df0ab76187 | ||
|
|
b7530dcdff | ||
|
|
12dc37191e | ||
|
|
3b0b7e4bd9 | ||
|
|
a3e6640e3f | ||
|
|
27922da186 | ||
|
|
efb3b74149 | ||
|
|
c7cc127ea0 | ||
|
|
c1dbaf0f05 | ||
|
|
cbbc054ec8 | ||
|
|
cb0e7a9389 | ||
|
|
7256cb5ef5 | ||
|
|
f731651c71 | ||
|
|
38e9987fff | ||
|
|
28c4084780 | ||
|
|
02f4154577 | ||
|
|
6e8ef20efd | ||
|
|
7b01867934 |
353
.github/workflows/main.yml
vendored
353
.github/workflows/main.yml
vendored
@@ -128,7 +128,7 @@ jobs:
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
|
||||
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
|
||||
@@ -305,7 +305,7 @@ jobs:
|
||||
# qmake
|
||||
# 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 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
|
||||
@@ -510,7 +510,7 @@ jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
android-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
@@ -534,11 +534,21 @@ jobs:
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# This token is provided by Actions, you do not need to create your own token
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: recursive # or 'true' if you want to check out only immediate submodules
|
||||
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
|
||||
@@ -573,7 +583,7 @@ jobs:
|
||||
|
||||
# waiting github.com/jurplel/install-qt-action/issues/63
|
||||
- name: Install Qt Android
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
@@ -608,7 +618,7 @@ jobs:
|
||||
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 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
|
||||
@@ -639,19 +649,92 @@ jobs:
|
||||
name: fdroid-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/
|
||||
|
||||
# - name: Exit if not on master branch
|
||||
# if: github.ref == 'refs/heads/master'
|
||||
# run: exit 1
|
||||
android-emulator-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: android-build
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# - name: upload windows artifact
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ github.token }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk
|
||||
# asset_name: fdroid-android-trial.zip
|
||||
# asset_content_type: application/zip
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
# Download the APK from the previous job
|
||||
- name: Download APK Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: fdroid-android-trial
|
||||
path: apk-debug
|
||||
|
||||
- name: Setup Java for Android Emulator
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
# Use a smaller emulator configuration
|
||||
- name: Run tests on emulator
|
||||
uses: ReactiveCircus/android-emulator-runner@v2
|
||||
with:
|
||||
target: default # Use default instead of Google APIs
|
||||
arch: x86
|
||||
api-level: 29
|
||||
profile: Nexus 6
|
||||
disable-animations: true
|
||||
script: |
|
||||
# Display available space
|
||||
df -h
|
||||
|
||||
# List available files
|
||||
echo "Files in apk-debug directory:"
|
||||
ls -la apk-debug/
|
||||
|
||||
# Install the APK
|
||||
adb install apk-debug/android-debug.apk
|
||||
|
||||
# Grant necessary permissions for API 25
|
||||
echo "Granting permissions..."
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_FINE_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.READ_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WRITE_EXTERNAL_STORAGE || true
|
||||
|
||||
# Start the main activity
|
||||
adb shell am start -n org.cagnulen.qdomyoszwift/org.qtproject.qt5.android.bindings.QtActivity
|
||||
|
||||
# Wait for app to start
|
||||
sleep 40
|
||||
|
||||
# Verify the app is running
|
||||
echo "Checking if app is running..."
|
||||
adb shell "ps -A" > process_list.txt
|
||||
grep -q "qdomyos" process_list.txt || (echo "App process not found in process list" && echo "TEST FAILED: App process not running" && exit 1)
|
||||
echo "App is running successfully"
|
||||
|
||||
# Take a screenshot for verification
|
||||
adb shell screencap -p /sdcard/screenshot.png
|
||||
adb pull /sdcard/screenshot.png
|
||||
|
||||
# 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
|
||||
|
||||
- name: Upload test evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-emulator-test-evidence
|
||||
path: |
|
||||
screenshot.png
|
||||
process_list.txt
|
||||
if-no-files-found: warn
|
||||
|
||||
ios-build:
|
||||
# The type of runner that the job will run on
|
||||
@@ -712,7 +795,7 @@ jobs:
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
|
||||
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
|
||||
@@ -823,7 +906,7 @@ jobs:
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
|
||||
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
|
||||
@@ -854,7 +937,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=D:\a\qdomyos-zwift\vcpkg\installed
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Build
|
||||
@@ -1011,7 +1094,7 @@ jobs:
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
|
||||
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
|
||||
@@ -1042,7 +1125,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=D:\a\qdomyos-zwift\vcpkg\installed
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Build
|
||||
@@ -1106,7 +1189,7 @@ jobs:
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
|
||||
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
|
||||
@@ -1154,7 +1237,7 @@ jobs:
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0
|
||||
image: tonistiigi/binfmt:master
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
@@ -1165,7 +1248,7 @@ jobs:
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
|
||||
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
|
||||
@@ -1194,15 +1277,211 @@ jobs:
|
||||
|
||||
- name: Rename binary
|
||||
run: mv src/qdomyos-zwift src/qdomyos-zwift-64bit
|
||||
|
||||
- name: Archive Raspberry Pi 64bit binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: raspberry-pi-binary-64bit
|
||||
path: src/qdomyos-zwift-64bit
|
||||
|
||||
window-msvc2022-build:
|
||||
runs-on: windows-latest
|
||||
if: github.event_name == 'schedule'
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- {python: true}
|
||||
- {python: false}
|
||||
|
||||
steps:
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: refs/pull/1508/head
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: download python and paddleocr
|
||||
run: |
|
||||
python -VV
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade setuptools
|
||||
python -m pip install "protobuf<=3.20.2,>=3.1.0"
|
||||
python -m pip install paddlepaddle==2.5.1
|
||||
python -m pip install paddleocr
|
||||
python -m pip install imutils
|
||||
python -m pip install "Pillow<10.0.0"
|
||||
python -m pip install opencv-python
|
||||
python -m pip install numpy
|
||||
python -m pip install pywin32
|
||||
if: matrix.config.python
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: '6.8.2'
|
||||
host: 'windows'
|
||||
modules: 'qtnetworkauth qtcharts qtconnectivity qtspeech qtpositioning qtwebsockets qtlocation qtmultimedia qtwebengine qtwebview qthttpserver qtwebchannel'
|
||||
target: "desktop"
|
||||
arch: win64_msvc2022_64
|
||||
dir: "${{github.workspace}}/qt/"
|
||||
install-deps: "true"
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-windows'
|
||||
|
||||
- name: Install MSVC compiler
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
# 14.1 is for vs2017, 14.2 is vs2019, following the upstream vcpkg build from Qv2ray-deps repo
|
||||
#toolset: 14.2
|
||||
arch: x64
|
||||
|
||||
# - name: download 3rd party files for qthttpserver
|
||||
# run: |
|
||||
# cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
# - name: Build qthttpserver
|
||||
# run: |
|
||||
# cd src\qthttpserver
|
||||
# qmake
|
||||
# nmake
|
||||
# nmake install
|
||||
# cd ../..
|
||||
|
||||
- name: Secrets
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
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
|
||||
cd ..
|
||||
|
||||
- name: Clone vcpkg
|
||||
run: git clone https://github.com/microsoft/vcpkg.git
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Bootstrap vcpkg
|
||||
run: .\vcpkg\bootstrap-vcpkg.bat
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Create vcpkg.json
|
||||
working-directory: ${{ runner.workspace }}
|
||||
run: |
|
||||
echo '{
|
||||
"name": "qdomyos-zwift",
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
|
||||
"dependencies": [
|
||||
"protobuf",
|
||||
"protobuf-c",
|
||||
"abseil"
|
||||
],
|
||||
"builtin-baseline": "8c2fcacefba009d63672f9d137f192765e632c9f"
|
||||
}' > vcpkg.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
|
||||
working-directory: ${{ runner.workspace }}
|
||||
|
||||
- name: Build
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
|
||||
run: |
|
||||
qmake
|
||||
nmake
|
||||
cd src/debug
|
||||
mkdir output
|
||||
mkdir appx
|
||||
cp qdomyos-zwift.* output/
|
||||
cd output
|
||||
windeployqt --qmldir ../../ qdomyos-zwift.exe
|
||||
cp ../../../icons/iOS/iTunesArtwork@2x.png .
|
||||
cp ../../AppxManifest.xml .
|
||||
cp ../../windows/*.py .
|
||||
cp ../../windows/*.bat .
|
||||
cp ../../../windows_openssl/*.* .
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\bin\*.* -Destination . -Verbose
|
||||
mkdir adb
|
||||
mkdir python
|
||||
Copy-Item -Path C:\hostedtoolcache\windows\Python\3.7.9\x64 -Destination python -Recurse
|
||||
cp ../../adb/* adb/
|
||||
cd ..
|
||||
cd appx
|
||||
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
|
||||
if: matrix.config.python
|
||||
|
||||
|
||||
- name: Build without python
|
||||
# Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
|
||||
# Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
|
||||
# Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
|
||||
run: |
|
||||
qmake
|
||||
nmake
|
||||
cd src/debug
|
||||
mkdir output
|
||||
mkdir appx
|
||||
cp qdomyos-zwift.* output/
|
||||
cd output
|
||||
windeployqt --qmldir ../../ qdomyos-zwift.exe
|
||||
cp "C:/mingw64/bin/libwinpthread-1.dll" .
|
||||
cp "C:/mingw64/bin/libgcc_s_seh-1.dll" .
|
||||
cp "C:/mingw64/bin/libstdc++-6.dll" .
|
||||
cp ../../../icons/iOS/iTunesArtwork@2x.png .
|
||||
cp ../../AppxManifest.xml .
|
||||
cp ../../../windows_openssl/*.* .
|
||||
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\bin\*.* -Destination . -Verbose
|
||||
mkdir adb
|
||||
cp ../../adb/* adb/
|
||||
cd ..
|
||||
cd appx
|
||||
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
|
||||
if: matrix.config.python == false
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/windows/5.15.2/binary/msvc2019/*.* ${{ github.workspace }}/src/debug/output/
|
||||
|
||||
- name: Zip artifact for deployment
|
||||
run: Compress-Archive src/debug/output windows-msvc2022-binary.zip
|
||||
if: matrix.config.python
|
||||
|
||||
- name: Zip artifact for deployment
|
||||
run: Compress-Archive src/debug/output windows-msvc2022-binary-no-python.zip
|
||||
if: ${{ ! matrix.config.python }}
|
||||
|
||||
- name: Archive windows binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-msvc2022-binary
|
||||
path: windows-msvc2022-binary.zip
|
||||
if: matrix.config.python
|
||||
|
||||
- name: Archive windows binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-msvc2022-binary-no-python
|
||||
path: windows-msvc2022-binary-no-python.zip
|
||||
if: ${{ ! matrix.config.python }}
|
||||
|
||||
upload_to_release:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
needs: [linux-x86-build, window-msvc2019-build, ios-build, window-build, android-build, raspberry-pi-build] # Specify the job dependencies
|
||||
needs: [linux-x86-build, window-msvc2019-build, window-msvc2022-build, ios-build, window-build, android-build, raspberry-pi-build]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Update nightly release
|
||||
uses: andelf/nightly-release@main
|
||||
env:
|
||||
@@ -1220,14 +1499,24 @@ jobs:
|
||||
of new complex code, the stability of the program may suffer compared to
|
||||
official releases, so **use it with caution**!
|
||||
|
||||
## Windows Builds:
|
||||
- **windows-msvc2019**: Recommended for Windows 10
|
||||
- **windows-msvc2022**: Recommended for Windows 11 (experimental, QT6)
|
||||
- **windows**: MinGW build (alternative version)
|
||||
|
||||
## Other Platforms:
|
||||
- **fdroid-android-trial**: Android build
|
||||
- **raspberry-pi-binary**: Raspberry Pi build
|
||||
|
||||
__Please help us improve QZ by reporting any issues you encounter!__ :wink:
|
||||
files: |
|
||||
windows-msvc2019-binary-no-python/*
|
||||
windows-msvc2019-binary/*
|
||||
windows-msvc2022-binary-no-python/*
|
||||
windows-msvc2022-binary/*
|
||||
windows-msvc2019-ai-server-binary/*
|
||||
windows-binary-no-python/*
|
||||
windows-binary/*
|
||||
fdroid-android-trial/*
|
||||
raspberry-pi-binary/qdomyos-zwift-32bit
|
||||
#raspberry-pi-64bit-binary/qdomyos-zwift-64bit
|
||||
#2024-10-22-raspios-bookworm-arm64-lite.img.xz
|
||||
raspberry-pi-binary/qdomyos-zwift-64bit
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -50,3 +50,4 @@ src/inner_templates/googlemaps/cesium-key.js
|
||||
.vscode/settings.json
|
||||
/tst/Devices/.vs
|
||||
src/inner_templates/googlemaps/cesium-key.js
|
||||
src/qdomyos-zwift.pro.user.49de507
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -12,7 +12,7 @@
|
||||
[submodule "tst/googletest"]
|
||||
path = tst/googletest
|
||||
url = https://github.com/google/googletest.git
|
||||
branch = tags/release-1.12.1
|
||||
tag = release-1.12.1
|
||||
[submodule "src/qthttpserver"]
|
||||
path = src/qthttpserver
|
||||
url = https://github.com/qt-labs/qthttpserver
|
||||
|
||||
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
|
||||
@@ -299,6 +299,10 @@
|
||||
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8762D5112601F89500F6F049 /* scanrecordresult.cpp */; };
|
||||
87646C2027B5064600F82131 /* bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C1E27B5064500F82131 /* bhfitnesselliptical.cpp */; };
|
||||
87646C2227B5065100F82131 /* moc_bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */; };
|
||||
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */; };
|
||||
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */; };
|
||||
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */; };
|
||||
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */; };
|
||||
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */; };
|
||||
8768C8BA2BBC11C80099DBE1 /* file_sync_client.c in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8768C89C2BBC11C70099DBE1 /* file_sync_client.c */; };
|
||||
8768C8BB2BBC11C80099DBE1 /* protocol.txt in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 8768C89D2BBC11C70099DBE1 /* protocol.txt */; };
|
||||
@@ -392,8 +396,11 @@
|
||||
8785D5442B3DD105005A2EB7 /* moc_zwift_client_auth.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */; };
|
||||
87873AEE2D09A8AA005F86B4 /* moc_sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */; };
|
||||
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */; };
|
||||
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */; };
|
||||
878A331A25AB4FF800BD13E1 /* yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331725AB4FF800BD13E1 /* yesoulbike.cpp */; };
|
||||
878A331D25AB50C300BD13E1 /* moc_yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */; };
|
||||
878C9DC92DF01C16001114D5 /* speraxtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */; };
|
||||
878C9DCA2DF01C16001114D5 /* moc_speraxtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */; };
|
||||
878C9E6928B77E7C00669129 /* nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */; };
|
||||
878C9E6B28B77E9800669129 /* moc_nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */; };
|
||||
878D83742A1F33C600D7F004 /* bkoolbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878D83732A1F33C600D7F004 /* bkoolbike.cpp */; };
|
||||
@@ -472,6 +479,7 @@
|
||||
87BE6FDE272D2A3E00C35795 /* moc_horizongr7bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BE6FDD272D2A3E00C35795 /* moc_horizongr7bike.cpp */; };
|
||||
87BF116D298E28CA00B5B6E7 /* pelotonbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */; };
|
||||
87BF116F298E28EC00B5B6E7 /* moc_pelotonbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BF116E298E28EC00B5B6E7 /* moc_pelotonbike.cpp */; };
|
||||
87BFEA2F2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */; };
|
||||
87C424262BC1294000503687 /* moc_treadmillErgTable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */; };
|
||||
87C481FA26DFA7C3006211AD /* eliterizer.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C481F926DFA7C3006211AD /* eliterizer.cpp */; };
|
||||
87C481FC26DFA7D1006211AD /* moc_eliterizer.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C481FB26DFA7D1006211AD /* moc_eliterizer.cpp */; };
|
||||
@@ -523,6 +531,12 @@
|
||||
87D5DC4228230496008CCDE7 /* moc_truetreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D5DC4128230496008CCDE7 /* moc_truetreadmill.cpp */; };
|
||||
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D91F992800B9970026D43C /* proformwifibike.cpp */; };
|
||||
87D91F9C2800B9B90026D43C /* moc_proformwifibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D91F9B2800B9B90026D43C /* moc_proformwifibike.cpp */; };
|
||||
87DA62A42D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */; };
|
||||
87DA62A52D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */; };
|
||||
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */; };
|
||||
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */; };
|
||||
87DA62AF2D2426F2008ADA0F /* characteristicnotifier0004.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */; };
|
||||
87DA62B02D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */; };
|
||||
87DA76502848F98200A71B64 /* libQt5TextToSpeech.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87DA764F2848F8F200A71B64 /* libQt5TextToSpeech.a */; };
|
||||
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA8464284933D200B550E9 /* fakeelliptical.cpp */; };
|
||||
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA8466284933DE00B550E9 /* moc_fakeelliptical.cpp */; };
|
||||
@@ -532,6 +546,12 @@
|
||||
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
|
||||
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
|
||||
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
|
||||
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
|
||||
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
|
||||
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
|
||||
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */; };
|
||||
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */; };
|
||||
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */; };
|
||||
87DED80627D1273900BE4FBB /* filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80427D1273800BE4FBB /* filedownloader.cpp */; };
|
||||
87DED80827D1274600BE4FBB /* moc_filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */; };
|
||||
87DF68B825E2673B00FCDA46 /* eslinkertreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DF68B625E2673600FCDA46 /* eslinkertreadmill.cpp */; };
|
||||
@@ -560,6 +580,12 @@
|
||||
87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */; };
|
||||
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */; };
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */; };
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */; };
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A52D39214E00348B15 /* workoutmodel.cpp */; };
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */; };
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
|
||||
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
|
||||
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
|
||||
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
|
||||
@@ -567,6 +593,10 @@
|
||||
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
|
||||
87F02E4229178545000DB52C /* moc_octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */; };
|
||||
87F1179E26A5FBDE00541B3A /* libqtwebview_darwin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87420DF7269D7CE1000C5EC6 /* libqtwebview_darwin.a */; };
|
||||
87F1BD682DBFBCE700416506 /* moc_android_antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */; };
|
||||
87F1BD692DBFBCE700416506 /* android_antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD662DBFBCE700416506 /* android_antbike.cpp */; };
|
||||
87F1BD712DC0D59600416506 /* moc_coresensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD702DC0D59600416506 /* moc_coresensor.cpp */; };
|
||||
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD6F2DC0D59600416506 /* coresensor.cpp */; };
|
||||
87F4FB5A29D550C00061BB4A /* schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */; };
|
||||
87F4FB5C29D550E00061BB4A /* moc_schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */; };
|
||||
87F527BE28EEB5AA00A9F8D5 /* qzsettings.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F527BC28EEB5AA00A9F8D5 /* qzsettings.cpp */; };
|
||||
@@ -1199,6 +1229,13 @@
|
||||
87646C1E27B5064500F82131 /* bhfitnesselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = bhfitnesselliptical.cpp; path = ../src/devices/bhfitnesselliptical/bhfitnesselliptical.cpp; sourceTree = "<group>"; };
|
||||
87646C1F27B5064500F82131 /* bhfitnesselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bhfitnesselliptical.h; path = ../src/devices/bhfitnesselliptical/bhfitnesselliptical.h; sourceTree = "<group>"; };
|
||||
87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bhfitnesselliptical.cpp; sourceTree = "<group>"; };
|
||||
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = elitesquarecontroller.h; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.h; sourceTree = SOURCE_ROOT; };
|
||||
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = elitesquarecontroller.cpp; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.cpp; sourceTree = SOURCE_ROOT; };
|
||||
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elitesquarecontroller.cpp; sourceTree = "<group>"; };
|
||||
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_wahookickrsnapbike.h; path = ../src/ios/ios_wahookickrsnapbike.h; sourceTree = SOURCE_ROOT; };
|
||||
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_wahookickrsnapbike.mm; path = ../src/ios/ios_wahookickrsnapbike.mm; sourceTree = SOURCE_ROOT; };
|
||||
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_zwiftclickremote.h; path = ../src/ios/ios_zwiftclickremote.h; sourceTree = SOURCE_ROOT; };
|
||||
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_zwiftclickremote.mm; path = ../src/ios/ios_zwiftclickremote.mm; sourceTree = SOURCE_ROOT; };
|
||||
8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor.cpp; path = ../src/characteristics/characteristicwriteprocessor.cpp; sourceTree = "<group>"; };
|
||||
8768C89C2BBC11C70099DBE1 /* file_sync_client.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = file_sync_client.c; path = ../src/ios/adb/adb/file_sync_client.c; sourceTree = "<group>"; };
|
||||
8768C89D2BBC11C70099DBE1 /* protocol.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = protocol.txt; path = ../src/ios/adb/adb/protocol.txt; sourceTree = "<group>"; };
|
||||
@@ -1347,10 +1384,15 @@
|
||||
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusrower.cpp; sourceTree = "<group>"; };
|
||||
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = sportsplusrower.cpp; sourceTree = "<group>"; };
|
||||
87873AF22D09AADF005F86B4 /* sportsplusrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportsplusrower.h; path = ../src/devices/sportsplusrower/sportsplusrower.h; sourceTree = SOURCE_ROOT; };
|
||||
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = inclinationresistancetable.h; path = ../src/inclinationresistancetable.h; sourceTree = SOURCE_ROOT; };
|
||||
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inclinationresistancetable.cpp; sourceTree = "<group>"; };
|
||||
8789DCDB6A4F681A76DF3F92 /* Qt5Widgets */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = Qt5Widgets; path = "/Users/cagnulein/Qt/5.15.2/ios/lib/libQt5Widgets$(QT_LIBRARY_SUFFIX).a"; sourceTree = "<absolute>"; };
|
||||
878A331725AB4FF800BD13E1 /* yesoulbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = yesoulbike.cpp; path = ../src/devices/yesoulbike/yesoulbike.cpp; sourceTree = "<group>"; };
|
||||
878A331825AB4FF800BD13E1 /* yesoulbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = yesoulbike.h; path = ../src/devices/yesoulbike/yesoulbike.h; sourceTree = "<group>"; };
|
||||
878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_yesoulbike.cpp; sourceTree = "<group>"; };
|
||||
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_speraxtreadmill.cpp; sourceTree = "<group>"; };
|
||||
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = speraxtreadmill.h; path = ../src/devices/speraxtreadmill/speraxtreadmill.h; sourceTree = SOURCE_ROOT; };
|
||||
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = speraxtreadmill.cpp; path = ../src/devices/speraxtreadmill/speraxtreadmill.cpp; sourceTree = SOURCE_ROOT; };
|
||||
878C9E6728B77E7B00669129 /* nordictrackifitadbbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbbike.h; path = ../src/devices/nordictrackifitadbbike/nordictrackifitadbbike.h; sourceTree = "<group>"; };
|
||||
878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbbike.cpp; path = ../src/devices/nordictrackifitadbbike/nordictrackifitadbbike.cpp; sourceTree = "<group>"; };
|
||||
878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbbike.cpp; sourceTree = "<group>"; };
|
||||
@@ -1469,6 +1511,8 @@
|
||||
87BF116B298E28CA00B5B6E7 /* pelotonbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = pelotonbike.h; path = ../src/devices/pelotonbike/pelotonbike.h; sourceTree = "<group>"; };
|
||||
87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = pelotonbike.cpp; path = ../src/devices/pelotonbike/pelotonbike.cpp; sourceTree = "<group>"; };
|
||||
87BF116E298E28EC00B5B6E7 /* moc_pelotonbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_pelotonbike.cpp; sourceTree = "<group>"; };
|
||||
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_echelonconnectsport.h; path = ../src/ios/ios_echelonconnectsport.h; sourceTree = SOURCE_ROOT; };
|
||||
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_echelonconnectsport.mm; path = ../src/ios/ios_echelonconnectsport.mm; sourceTree = SOURCE_ROOT; };
|
||||
87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_treadmillErgTable.cpp; sourceTree = "<group>"; };
|
||||
87C481F826DFA7C3006211AD /* eliterizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = eliterizer.h; path = ../src/devices/eliterizer/eliterizer.h; sourceTree = "<group>"; };
|
||||
87C481F926DFA7C3006211AD /* eliterizer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = eliterizer.cpp; path = ../src/devices/eliterizer/eliterizer.cpp; sourceTree = "<group>"; };
|
||||
@@ -1546,6 +1590,12 @@
|
||||
87D91F982800B9970026D43C /* proformwifibike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformwifibike.h; path = ../src/devices/proformwifibike/proformwifibike.h; sourceTree = "<group>"; };
|
||||
87D91F992800B9970026D43C /* proformwifibike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformwifibike.cpp; path = ../src/devices/proformwifibike/proformwifibike.cpp; sourceTree = "<group>"; };
|
||||
87D91F9B2800B9B90026D43C /* moc_proformwifibike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformwifibike.cpp; sourceTree = "<group>"; };
|
||||
87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicnotifier0002.cpp; sourceTree = "<group>"; };
|
||||
87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicwriteprocessor0003.cpp; sourceTree = "<group>"; };
|
||||
87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicnotifier0002.cpp; path = ../src/characteristics/characteristicnotifier0002.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor0003.cpp; path = ../src/characteristics/characteristicwriteprocessor0003.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicnotifier0004.cpp; path = ../src/characteristics/characteristicnotifier0004.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicnotifier0004.cpp; sourceTree = "<group>"; };
|
||||
87DA764F2848F8F200A71B64 /* libQt5TextToSpeech.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5TextToSpeech.a; path = ../../Qt/5.15.2/ios/lib/libQt5TextToSpeech.a; sourceTree = "<group>"; };
|
||||
87DA8463284933D200B550E9 /* fakeelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fakeelliptical.h; path = ../src/devices/fakeelliptical/fakeelliptical.h; sourceTree = "<group>"; };
|
||||
87DA8464284933D200B550E9 /* fakeelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakeelliptical.cpp; path = ../src/devices/fakeelliptical/fakeelliptical.cpp; sourceTree = "<group>"; };
|
||||
@@ -1559,6 +1609,15 @@
|
||||
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
|
||||
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
|
||||
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = stairclimber.cpp; path = ../src/devices/stairclimber.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_echelonstairclimber.cpp; sourceTree = "<group>"; };
|
||||
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_stairclimber.cpp; sourceTree = "<group>"; };
|
||||
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_moxy5sensor.cpp; sourceTree = "<group>"; };
|
||||
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = moxy5sensor.h; path = ../src/devices/moxy5sensor/moxy5sensor.h; sourceTree = SOURCE_ROOT; };
|
||||
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = moxy5sensor.cpp; path = ../src/devices/moxy5sensor/moxy5sensor.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87DED80427D1273800BE4FBB /* filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = filedownloader.cpp; path = ../src/filedownloader.cpp; sourceTree = "<group>"; };
|
||||
87DED80527D1273900BE4FBB /* filedownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = filedownloader.h; path = ../src/filedownloader.h; sourceTree = "<group>"; };
|
||||
87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_filedownloader.cpp; sourceTree = "<group>"; };
|
||||
@@ -1600,6 +1659,15 @@
|
||||
87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = qdomyoszwift_qmltyperegistrations.cpp; sourceTree = "<group>"; };
|
||||
87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproductqmltype.cpp; sourceTree = "<group>"; };
|
||||
87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproduct.cpp; sourceTree = "<group>"; };
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitdatabaseprocessor.h; path = ../src/fitdatabaseprocessor.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitdatabaseprocessor.cpp; path = ../src/fitdatabaseprocessor.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitdatabaseprocessor.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutloaderworker.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutmodel.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutloaderworker.h; path = ../src/workoutloaderworker.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutloaderworker.cpp; path = ../src/workoutloaderworker.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutmodel.h; path = ../src/workoutmodel.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutmodel.cpp; path = ../src/workoutmodel.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
|
||||
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
@@ -1609,6 +1677,12 @@
|
||||
87F02E3E29178523000DB52C /* octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = octaneelliptical.cpp; path = ../src/devices/octaneelliptical/octaneelliptical.cpp; sourceTree = "<group>"; };
|
||||
87F02E3F29178524000DB52C /* octaneelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = octaneelliptical.h; path = ../src/devices/octaneelliptical/octaneelliptical.h; sourceTree = "<group>"; };
|
||||
87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_octaneelliptical.cpp; sourceTree = "<group>"; };
|
||||
87F1BD652DBFBCE700416506 /* android_antbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = android_antbike.h; path = ../src/devices/android_antbike/android_antbike.h; sourceTree = SOURCE_ROOT; };
|
||||
87F1BD662DBFBCE700416506 /* android_antbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = android_antbike.cpp; path = ../src/devices/android_antbike/android_antbike.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_android_antbike.cpp; sourceTree = "<group>"; };
|
||||
87F1BD6E2DC0D59600416506 /* coresensor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = coresensor.h; path = ../src/devices/coresensor/coresensor.h; sourceTree = SOURCE_ROOT; };
|
||||
87F1BD6F2DC0D59600416506 /* coresensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = coresensor.cpp; path = ../src/devices/coresensor/coresensor.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_coresensor.cpp; sourceTree = "<group>"; };
|
||||
87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = schwinn170bike.cpp; path = ../src/devices/schwinn170bike/schwinn170bike.cpp; sourceTree = "<group>"; };
|
||||
87F4FB5929D550BF0061BB4A /* schwinn170bike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = schwinn170bike.h; path = ../src/devices/schwinn170bike/schwinn170bike.h; sourceTree = "<group>"; };
|
||||
87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_schwinn170bike.cpp; sourceTree = "<group>"; };
|
||||
@@ -2007,6 +2081,8 @@
|
||||
25B08E2869634E9BCBA333A2 /* Generated Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */,
|
||||
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */,
|
||||
87C4E5BC2C1C1D2600D0750E /* moc_crossrope.cpp */,
|
||||
87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */,
|
||||
874823FD2B935ADA006F3CA1 /* moc_ergtable.cpp */,
|
||||
@@ -2179,6 +2255,35 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
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 */,
|
||||
87F1BD6E2DC0D59600416506 /* coresensor.h */,
|
||||
87F1BD6F2DC0D59600416506 /* coresensor.cpp */,
|
||||
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */,
|
||||
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */,
|
||||
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */,
|
||||
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */,
|
||||
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */,
|
||||
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */,
|
||||
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */,
|
||||
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */,
|
||||
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */,
|
||||
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */,
|
||||
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */,
|
||||
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */,
|
||||
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */,
|
||||
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */,
|
||||
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */,
|
||||
8798FDC02D66075B00CF8EE8 /* osxbluetooth_p.h */,
|
||||
8798FDC12D66075B00CF8EE8 /* osxbtcentralmanager_p.h */,
|
||||
8798FDC22D66075B00CF8EE8 /* osxbtgcdtimer_p.h */,
|
||||
@@ -2674,6 +2779,13 @@
|
||||
87FE06822D170D5600CDAAF6 /* trxappgateusbrower.h */,
|
||||
87FE06832D170D5600CDAAF6 /* trxappgateusbrower.cpp */,
|
||||
87FE06802D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp */,
|
||||
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */,
|
||||
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */,
|
||||
87DC27E82D9BDB53007A1B9D /* stairclimber.h */,
|
||||
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */,
|
||||
87F1BD652DBFBCE700416506 /* android_antbike.h */,
|
||||
87F1BD662DBFBCE700416506 /* android_antbike.cpp */,
|
||||
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */,
|
||||
);
|
||||
name = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -2827,6 +2939,19 @@
|
||||
name = AppleWatchToIpad;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8798FDCA2D6F338200CF8EE8 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */,
|
||||
87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */,
|
||||
87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */,
|
||||
87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */,
|
||||
87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */,
|
||||
87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
87DF60DE337FB58864343E39 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3245,6 +3370,7 @@
|
||||
AF39DD055C3EF8226FBE929D /* Frameworks */,
|
||||
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
|
||||
FE0A091FDBFB3E9C31B7A1BD /* Products */,
|
||||
8798FDCA2D6F338200CF8EE8 /* Recovered References */,
|
||||
);
|
||||
name = qdomyoszwift;
|
||||
sourceTree = "<group>";
|
||||
@@ -3441,6 +3567,8 @@
|
||||
873824C027E64707004F1B46 /* moc_dirconmanager.cpp in Compile Sources */,
|
||||
2F0E70F826316F3600E11F3A /* virtualbike_zwift.swift in Compile Sources */,
|
||||
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */,
|
||||
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */,
|
||||
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */,
|
||||
87062646259480B200D06586 /* ViewController.swift in Compile Sources */,
|
||||
87D269A425F535340076AA48 /* moc_m3ibike.cpp in Compile Sources */,
|
||||
87BAFE482B8CA7AA00065FCD /* moc_focustreadmill.cpp in Compile Sources */,
|
||||
@@ -3461,6 +3589,8 @@
|
||||
873824BC27E64707004F1B46 /* moc_resolver.cpp in Compile Sources */,
|
||||
87FA11AD27C5ECE4008AC5D1 /* moc_ultrasportbike.cpp in Compile Sources */,
|
||||
871235BF26B297670012D0F2 /* kingsmithr1protreadmill.cpp in Compile Sources */,
|
||||
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */,
|
||||
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */,
|
||||
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */,
|
||||
20A50533946A39CBD2C89104 /* bluetoothdevice.cpp in Compile Sources */,
|
||||
87C5F0D126285E7E0067A1B5 /* moc_stagesbike.cpp in Compile Sources */,
|
||||
@@ -3497,9 +3627,12 @@
|
||||
87F02E4229178545000DB52C /* moc_octaneelliptical.cpp in Compile Sources */,
|
||||
87E2F85D291ED308002BDC65 /* lifefitnesstreadmill.cpp in Compile Sources */,
|
||||
8752C0E82B15D85600C3D1A5 /* eliteariafan.cpp in Compile Sources */,
|
||||
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */,
|
||||
87917A7328E768D200F8D9AC /* Browser.swift in Compile Sources */,
|
||||
873CD20B27EF8D8A000131BC /* inapptransaction.cpp in Compile Sources */,
|
||||
8727C7D52B3BF1E4005429EB /* moc_proformtelnetbike.cpp in Compile Sources */,
|
||||
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */,
|
||||
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */,
|
||||
873824EF27E647A9004F1B46 /* query.cpp in Compile Sources */,
|
||||
876F45FF279350D9003CDA5A /* moc_concept2skierg.cpp in Compile Sources */,
|
||||
BE93C6EF2C2A6BFEEC9EA565 /* fit_buffered_mesg_broadcaster.cpp in Compile Sources */,
|
||||
@@ -3507,6 +3640,7 @@
|
||||
87DAE16426E9FF3A00B0527E /* kingsmithr2treadmill.cpp in Compile Sources */,
|
||||
87F93427278E0EC00088B596 /* domyosrower.cpp in Compile Sources */,
|
||||
87B617EE25F25FED0094A1CB /* snodebike.cpp in Compile Sources */,
|
||||
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */,
|
||||
87C5F0B526285E5F0067A1B5 /* mimemessage.cpp in Compile Sources */,
|
||||
8768C8C92BBC11C80099DBE1 /* adb_client.c in Compile Sources */,
|
||||
873063C0259DF2C500DA0F44 /* moc_heartratebelt.cpp in Compile Sources */,
|
||||
@@ -3576,6 +3710,8 @@
|
||||
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */,
|
||||
87CC3BA425A0885F001EC5A8 /* elliptical.cpp in Compile Sources */,
|
||||
4AD2C93A2B8FD5855E521630 /* fit_encode.cpp in Compile Sources */,
|
||||
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
|
||||
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
|
||||
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
|
||||
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
|
||||
@@ -3604,6 +3740,7 @@
|
||||
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */,
|
||||
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */,
|
||||
876E50F52B701C050080FAAF /* moc_zwiftclickremote.cpp in Compile Sources */,
|
||||
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */,
|
||||
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
|
||||
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
|
||||
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
|
||||
@@ -3631,6 +3768,8 @@
|
||||
8720890E2CE65451008C2C17 /* qmqttsubscriptionproperties.cpp in Compile Sources */,
|
||||
8720890F2CE65451008C2C17 /* qmqttconnectionproperties.cpp in Compile Sources */,
|
||||
872089102CE65451008C2C17 /* qmqttconnection.cpp in Compile Sources */,
|
||||
878C9DC92DF01C16001114D5 /* speraxtreadmill.cpp in Compile Sources */,
|
||||
878C9DCA2DF01C16001114D5 /* moc_speraxtreadmill.cpp in Compile Sources */,
|
||||
872089112CE65451008C2C17 /* qmqttsubscription.cpp in Compile Sources */,
|
||||
872089122CE65451008C2C17 /* qmqttclient.cpp in Compile Sources */,
|
||||
872089132CE65451008C2C17 /* qmqtttopicfilter.cpp in Compile Sources */,
|
||||
@@ -3690,6 +3829,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 */,
|
||||
@@ -3707,6 +3852,7 @@
|
||||
873824BB27E64707004F1B46 /* moc_prober_p.cpp in Compile Sources */,
|
||||
877758B32C98627300BB1697 /* moc_sportstechelliptical.cpp in Compile Sources */,
|
||||
8742C2B227C92C30007D3FA0 /* wahookickrsnapbike.cpp in Compile Sources */,
|
||||
87BFEA2F2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm in Compile Sources */,
|
||||
87EB918327EE5FE7002535E1 /* moc_inappstore.cpp in Compile Sources */,
|
||||
87CF516B293C87B000A7CABC /* moc_characteristicwriteprocessore005.cpp in Compile Sources */,
|
||||
8780D949264FB8B800192D41 /* moc_smartspin2k.cpp in Compile Sources */,
|
||||
@@ -3804,7 +3950,11 @@
|
||||
1FBBC7C86C436CAAAFD37E56 /* moc_domyostreadmill.cpp in Compile Sources */,
|
||||
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */,
|
||||
873824BD27E64707004F1B46 /* moc_resolver_p.cpp in Compile Sources */,
|
||||
87DA62A42D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp in Compile Sources */,
|
||||
87DA62A52D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp in Compile Sources */,
|
||||
876ED21A25C3E9010065F3DC /* moc_material.cpp in Compile Sources */,
|
||||
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */,
|
||||
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */,
|
||||
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */,
|
||||
87D10552290996EA00B3935B /* mepanelbike.cpp in Compile Sources */,
|
||||
9D9484EED654597C394345DE /* moc_echelonconnectsport.cpp in Compile Sources */,
|
||||
@@ -3824,6 +3974,8 @@
|
||||
87A33F1A2D611D8400BFFF29 /* moc_logwriter.cpp in Compile Sources */,
|
||||
87D105542909971100B3935B /* moc_mepanelbike.cpp in Compile Sources */,
|
||||
87B617F325F260150094A1CB /* moc_snodebike.cpp in Compile Sources */,
|
||||
87F1BD712DC0D59600416506 /* moc_coresensor.cpp in Compile Sources */,
|
||||
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */,
|
||||
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */,
|
||||
87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */,
|
||||
877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */,
|
||||
@@ -3845,6 +3997,8 @@
|
||||
873824B227E64706004F1B46 /* moc_hostname.cpp in Compile Sources */,
|
||||
873824EB27E647A8004F1B46 /* prober.cpp in Compile Sources */,
|
||||
875CA9462D0C740000667EE6 /* moc_kineticinroadbike.cpp in Compile Sources */,
|
||||
87DA62AF2D2426F2008ADA0F /* characteristicnotifier0004.cpp in Compile Sources */,
|
||||
87DA62B02D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp in Compile Sources */,
|
||||
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */,
|
||||
87B617F425F260150094A1CB /* moc_screencapture.cpp in Compile Sources */,
|
||||
87B617ED25F25FED0094A1CB /* fitshowtreadmill.cpp in Compile Sources */,
|
||||
@@ -3865,6 +4019,8 @@
|
||||
87A0771029B641D600A368BF /* wahookickrheadwind.cpp in Compile Sources */,
|
||||
87EAC3D32D1D8D1D004FE975 /* moc_pitpatbike.cpp in Compile Sources */,
|
||||
8791A8AB25C861BD003B50B2 /* inspirebike.cpp in Compile Sources */,
|
||||
87F1BD682DBFBCE700416506 /* moc_android_antbike.cpp in Compile Sources */,
|
||||
87F1BD692DBFBCE700416506 /* android_antbike.cpp in Compile Sources */,
|
||||
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */,
|
||||
871235C126B297720012D0F2 /* moc_kingsmithr1protreadmill.cpp in Compile Sources */,
|
||||
87061397286D8CFE00D2446E /* PathController.cpp in Compile Sources */,
|
||||
@@ -4255,7 +4411,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1035;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
|
||||
@@ -4291,6 +4447,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,
|
||||
@@ -4337,7 +4494,7 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
);
|
||||
MARKETING_VERSION = 2.18;
|
||||
MARKETING_VERSION = 2.19;
|
||||
OTHER_CFLAGS = (
|
||||
"-pipe",
|
||||
"-g",
|
||||
@@ -4449,7 +4606,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1035;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4487,6 +4644,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,
|
||||
@@ -4533,7 +4691,7 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
);
|
||||
MARKETING_VERSION = 2.18;
|
||||
MARKETING_VERSION = 2.19;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = (
|
||||
"-pipe",
|
||||
@@ -4679,7 +4837,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1035;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4704,7 +4862,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.18;
|
||||
MARKETING_VERSION = 2.19;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
@@ -4775,7 +4933,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1035;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -4796,7 +4954,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.18;
|
||||
MARKETING_VERSION = 2.19;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
@@ -4867,7 +5025,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1035;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4912,7 +5070,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.18;
|
||||
MARKETING_VERSION = 2.19;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
@@ -4983,7 +5141,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1035;
|
||||
CURRENT_PROJECT_VERSION = 1115;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5024,7 +5182,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.18;
|
||||
MARKETING_VERSION = 2.19;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
|
||||
|
||||
@@ -54,20 +54,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):
|
||||
@@ -227,9 +233,11 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
let quantity = HKQuantity(unit: unit,
|
||||
doubleValue: totalEnergyBurned)
|
||||
|
||||
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
|
||||
|
||||
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
|
||||
quantity: quantity,
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sample]) {(success, error) in}
|
||||
@@ -249,7 +257,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
@@ -268,6 +276,67 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(sport == 4) { // Rowing
|
||||
// Guard to check if steps quantity type is available
|
||||
guard let quantityTypeSteps = HKQuantityType.quantityType(
|
||||
forIdentifier: .stepCount) else {
|
||||
return
|
||||
}
|
||||
|
||||
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
|
||||
|
||||
// Create a sample for total steps
|
||||
let sampleSteps = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeSteps,
|
||||
quantity: stepsQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
// Add the steps sample to workout builder
|
||||
workoutBuilder.add([sampleSteps]) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Per il rowing, HealthKit utilizza un tipo specifico di distanza
|
||||
// Se non esiste un tipo specifico per il rowing, possiamo usare un tipo generico di distanza
|
||||
var quantityTypeDistance: HKQuantityType?
|
||||
|
||||
// In watchOS 10 e versioni successive, possiamo usare un tipo specifico se disponibile
|
||||
if #available(watchOSApplicationExtension 10.0, *) {
|
||||
// Verifica se esiste un tipo specifico per il rowing, altrimenti utilizza un tipo generico
|
||||
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)
|
||||
} else {
|
||||
// Nelle versioni precedenti, usa il tipo generico
|
||||
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)
|
||||
}
|
||||
|
||||
guard let typeDistance = quantityTypeDistance else {
|
||||
return
|
||||
}
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: typeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.finishWorkout{ (workout, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Guard to check if steps quantity type is available
|
||||
@@ -282,7 +351,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
|
||||
@@ -314,7 +383,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
|
||||
188
helpers/dircon-parser.py
Normal file
188
helpers/dircon-parser.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple
|
||||
import re
|
||||
|
||||
@dataclass
|
||||
class HubRidingData:
|
||||
power: Optional[int] = None
|
||||
cadence: Optional[int] = None
|
||||
speed_x100: Optional[int] = None
|
||||
hr: Optional[int] = None
|
||||
unknown1: Optional[int] = None
|
||||
unknown2: Optional[int] = None
|
||||
|
||||
def __str__(self):
|
||||
return (f"Power={self.power}W Cadence={self.cadence}rpm "
|
||||
f"Speed={self.speed_x100/100 if self.speed_x100 else 0:.1f}km/h "
|
||||
f"HR={self.hr}bpm Unknown1={self.unknown1} Unknown2={self.unknown2}")
|
||||
|
||||
def parse_protobuf_varint(data: bytes, offset: int = 0) -> Tuple[int, int]:
|
||||
result = 0
|
||||
shift = 0
|
||||
while offset < len(data):
|
||||
byte = data[offset]
|
||||
result |= (byte & 0x7F) << shift
|
||||
offset += 1
|
||||
if not (byte & 0x80):
|
||||
break
|
||||
shift += 7
|
||||
return result, offset
|
||||
|
||||
def parse_hub_riding_data(data: bytes) -> Optional[HubRidingData]:
|
||||
try:
|
||||
riding_data = HubRidingData()
|
||||
offset = 0
|
||||
while offset < len(data):
|
||||
key, new_offset = parse_protobuf_varint(data, offset)
|
||||
wire_type = key & 0x07
|
||||
field_number = key >> 3
|
||||
offset = new_offset
|
||||
|
||||
if wire_type == 0:
|
||||
value, offset = parse_protobuf_varint(data, offset)
|
||||
if field_number == 1:
|
||||
riding_data.power = value
|
||||
elif field_number == 2:
|
||||
riding_data.cadence = value
|
||||
elif field_number == 3:
|
||||
riding_data.speed_x100 = value
|
||||
elif field_number == 4:
|
||||
riding_data.hr = value
|
||||
elif field_number == 5:
|
||||
riding_data.unknown1 = value
|
||||
elif field_number == 6:
|
||||
riding_data.unknown2 = value
|
||||
return riding_data
|
||||
except Exception as e:
|
||||
print(f"Error parsing protobuf: {e}")
|
||||
return None
|
||||
|
||||
@dataclass
|
||||
class DirconPacket:
|
||||
message_version: int = 1
|
||||
identifier: int = 0xFF
|
||||
sequence_number: int = 0
|
||||
response_code: int = 0
|
||||
length: int = 0
|
||||
uuid: int = 0
|
||||
uuids: List[int] = None
|
||||
additional_data: bytes = b''
|
||||
is_request: bool = False
|
||||
riding_data: Optional[HubRidingData] = None
|
||||
|
||||
def __str__(self):
|
||||
uuids_str = ','.join(f'{u:04x}' for u in (self.uuids or []))
|
||||
base_str = (f"vers={self.message_version} Id={self.identifier} sn={self.sequence_number} "
|
||||
f"resp={self.response_code} len={self.length} req?={self.is_request} "
|
||||
f"uuid={self.uuid:04x} dat={self.additional_data.hex()} uuids=[{uuids_str}]")
|
||||
if self.riding_data:
|
||||
base_str += f"\nRiding Data: {self.riding_data}"
|
||||
return base_str
|
||||
|
||||
def parse_dircon_packet(data: bytes, offset: int = 0) -> Tuple[Optional[DirconPacket], int]:
|
||||
if len(data) - offset < 6:
|
||||
return None, 0
|
||||
|
||||
packet = DirconPacket()
|
||||
packet.message_version = data[offset]
|
||||
packet.identifier = data[offset + 1]
|
||||
packet.sequence_number = data[offset + 2]
|
||||
packet.response_code = data[offset + 3]
|
||||
packet.length = (data[offset + 4] << 8) | data[offset + 5]
|
||||
|
||||
total_length = 6 + packet.length
|
||||
if len(data) - offset < total_length:
|
||||
return None, 0
|
||||
|
||||
curr_offset = offset + 6
|
||||
|
||||
if packet.identifier == 0x01: # DPKT_MSGID_DISCOVER_SERVICES
|
||||
if packet.length == 0:
|
||||
packet.is_request = True
|
||||
elif packet.length % 16 == 0:
|
||||
packet.uuids = []
|
||||
while curr_offset + 16 <= offset + total_length:
|
||||
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
packet.uuids.append(uuid)
|
||||
curr_offset += 16
|
||||
|
||||
elif packet.identifier == 0x02: # DPKT_MSGID_DISCOVER_CHARACTERISTICS
|
||||
if packet.length >= 16:
|
||||
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
if packet.length == 16:
|
||||
packet.is_request = True
|
||||
elif (packet.length - 16) % 17 == 0:
|
||||
curr_offset += 16
|
||||
packet.uuids = []
|
||||
packet.additional_data = b''
|
||||
while curr_offset + 17 <= offset + total_length:
|
||||
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
packet.uuids.append(uuid)
|
||||
packet.additional_data += bytes([data[curr_offset + 16]])
|
||||
curr_offset += 17
|
||||
|
||||
elif packet.identifier in [0x03, 0x04, 0x05, 0x06]: # READ/WRITE/NOTIFY characteristics
|
||||
if packet.length >= 16:
|
||||
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
if packet.length > 16:
|
||||
packet.additional_data = data[curr_offset + 16:offset + total_length]
|
||||
if packet.uuid == 0x0002:
|
||||
packet.riding_data = parse_hub_riding_data(packet.additional_data)
|
||||
if packet.identifier != 0x06:
|
||||
packet.is_request = True
|
||||
|
||||
return packet, total_length
|
||||
|
||||
def extract_bytes_from_c_array(content: str) -> List[Tuple[str, bytes]]:
|
||||
packets = []
|
||||
pattern = r'static const unsigned char (\w+)\[\d+\] = \{([^}]+)\};'
|
||||
matches = re.finditer(pattern, content)
|
||||
|
||||
for match in matches:
|
||||
name = match.group(1)
|
||||
hex_str = match.group(2)
|
||||
|
||||
hex_values = []
|
||||
for line in hex_str.split('\n'):
|
||||
line = line.split('//')[0]
|
||||
values = re.findall(r'0x[0-9a-fA-F]{2}', line)
|
||||
hex_values.extend(values)
|
||||
|
||||
byte_data = bytes([int(x, 16) for x in hex_values])
|
||||
packets.append((name, byte_data))
|
||||
|
||||
return packets
|
||||
|
||||
def get_tcp_payload(data: bytes) -> bytes:
|
||||
ip_header_start = 14 # Skip Ethernet header
|
||||
ip_header_len = (data[ip_header_start] & 0x0F) * 4
|
||||
tcp_header_start = ip_header_start + ip_header_len
|
||||
tcp_header_len = ((data[tcp_header_start + 12] >> 4) & 0x0F) * 4
|
||||
payload_start = tcp_header_start + tcp_header_len
|
||||
return data[payload_start:]
|
||||
|
||||
def parse_file(filename: str):
|
||||
with open(filename, 'r') as f:
|
||||
content = f.read()
|
||||
packets = extract_bytes_from_c_array(content)
|
||||
|
||||
for name, data in packets:
|
||||
print(f"\nPacket {name}:")
|
||||
payload = get_tcp_payload(data)
|
||||
print(f"Dircon payload: {payload.hex()}")
|
||||
|
||||
offset = 0
|
||||
while offset < len(payload):
|
||||
packet, consumed = parse_dircon_packet(payload, offset)
|
||||
if packet is None or consumed == 0:
|
||||
break
|
||||
print(f"Frame: {packet}")
|
||||
offset += consumed
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python script.py <input_file>")
|
||||
sys.exit(1)
|
||||
|
||||
parse_file(sys.argv[1])
|
||||
331
helpers/wahoo-simulator.py
Normal file
331
helpers/wahoo-simulator.py
Normal file
@@ -0,0 +1,331 @@
|
||||
import sys
|
||||
import logging
|
||||
import asyncio
|
||||
import threading
|
||||
import random
|
||||
import struct
|
||||
import binascii
|
||||
import time
|
||||
from typing import Any, Union
|
||||
|
||||
# Verificare che siamo su macOS
|
||||
if sys.platform != 'darwin':
|
||||
print("Questo script è progettato specificamente per macOS!")
|
||||
sys.exit(1)
|
||||
|
||||
# Importare bless
|
||||
try:
|
||||
from bless import (
|
||||
BlessServer,
|
||||
BlessGATTCharacteristic,
|
||||
GATTCharacteristicProperties,
|
||||
GATTAttributePermissions,
|
||||
)
|
||||
except ImportError:
|
||||
print("Errore: impossibile importare bless. Installarlo con: pip install bless")
|
||||
sys.exit(1)
|
||||
|
||||
# Configurazione logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Trigger per eventi
|
||||
trigger = threading.Event()
|
||||
|
||||
# Informazioni sul dispositivo
|
||||
DEVICE_NAME = "Wahoo KICKR 51A6"
|
||||
|
||||
# UUID dei servizi standard
|
||||
CYCLING_POWER_SERVICE = "00001818-0000-1000-8000-00805f9b34fb"
|
||||
USER_DATA_SERVICE = "0000181c-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_SERVICE = "00001826-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
# UUID dei servizi Wahoo personalizzati
|
||||
WAHOO_SERVICE_1 = "a026ee01-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_SERVICE_3 = "a026ee03-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_SERVICE_6 = "a026ee06-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_SERVICE_B = "a026ee0b-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
|
||||
# UUID delle caratteristiche standard
|
||||
CYCLING_POWER_MEASUREMENT = "00002a63-0000-1000-8000-00805f9b34fb"
|
||||
CYCLING_POWER_FEATURE = "00002a65-0000-1000-8000-00805f9b34fb"
|
||||
SENSOR_LOCATION = "00002a5d-0000-1000-8000-00805f9b34fb"
|
||||
CYCLING_POWER_CONTROL_POINT = "00002a66-0000-1000-8000-00805f9b34fb"
|
||||
WEIGHT = "00002a98-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_FEATURE = "00002acc-0000-1000-8000-00805f9b34fb"
|
||||
TRAINING_STATUS = "00002ad3-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_CONTROL_POINT = "00002ad9-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_STATUS = "00002ada-0000-1000-8000-00805f9b34fb"
|
||||
INDOOR_BIKE_DATA = "00002ad2-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
# UUID delle caratteristiche Wahoo personalizzate
|
||||
WAHOO_CUSTOM_CP_CHAR = "a026e005-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_1 = "a026e002-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_2 = "a026e004-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_3 = "a026e00a-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_4 = "a026e023-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_5 = "a026e037-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
|
||||
# Stato dispositivo - variabili globali
|
||||
current_power = 120
|
||||
current_cadence = 85
|
||||
current_speed = 25.0
|
||||
current_resistance = 5
|
||||
|
||||
# Funzioni di callback
|
||||
def read_request(characteristic, **kwargs):
|
||||
logger.debug(f"Lettura: {characteristic.value}")
|
||||
return characteristic.value
|
||||
|
||||
def write_request(characteristic, value, **kwargs):
|
||||
uuid_str = str(characteristic.uuid).lower()
|
||||
logger.info(f"Scrittura su caratteristica: {uuid_str}, valore: {binascii.hexlify(value)}")
|
||||
|
||||
# Gestione delle richieste di scrittura
|
||||
if uuid_str == FITNESS_MACHINE_CONTROL_POINT.lower():
|
||||
handle_ftms_control_point(value)
|
||||
elif uuid_str == CYCLING_POWER_CONTROL_POINT.lower():
|
||||
handle_cp_control_point(value)
|
||||
elif uuid_str in [WAHOO_CHAR_1.lower(), WAHOO_CHAR_3.lower(), WAHOO_CHAR_4.lower(), WAHOO_CHAR_5.lower()]:
|
||||
handle_wahoo_char_write(uuid_str, value)
|
||||
|
||||
characteristic.value = value
|
||||
|
||||
# Gestori di richieste di scrittura
|
||||
def handle_ftms_control_point(data):
|
||||
global current_power, current_resistance
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
op_code = data[0]
|
||||
logger.info(f"Comando FTMS Control Point: {op_code:#x}")
|
||||
|
||||
if op_code == 0x05: # Set Target Power (ERG mode)
|
||||
if len(data) >= 3:
|
||||
target_power = int.from_bytes(data[1:3], byteorder='little')
|
||||
logger.info(f"Target power impostato: {target_power}W")
|
||||
current_power = target_power
|
||||
|
||||
def handle_cp_control_point(data):
|
||||
if not data:
|
||||
return
|
||||
|
||||
op_code = data[0]
|
||||
logger.info(f"Comando CP Control Point: {op_code:#x}")
|
||||
|
||||
def handle_wahoo_char_write(uuid_str, data):
|
||||
logger.info(f"Scrittura su caratteristica Wahoo {uuid_str}: {binascii.hexlify(data)}")
|
||||
|
||||
# Funzioni per generare dati
|
||||
def generate_cycling_power_data():
|
||||
global current_power, current_cadence
|
||||
|
||||
# Varia leggermente i valori
|
||||
current_power += random.randint(-3, 3)
|
||||
current_power = max(0, min(2000, current_power))
|
||||
|
||||
current_cadence += random.randint(-1, 1)
|
||||
current_cadence = max(0, min(200, current_cadence))
|
||||
|
||||
# Crea Cycling Power Measurement
|
||||
power_data = bytearray(16)
|
||||
power_data[0:2] = (0x0034).to_bytes(2, byteorder='little')
|
||||
power_data[2:4] = (current_power).to_bytes(2, byteorder='little')
|
||||
power_data[4:8] = (int(current_power * 10)).to_bytes(4, byteorder='little')
|
||||
power_data[8:12] = (0).to_bytes(4, byteorder='little')
|
||||
power_data[12:14] = (current_cadence).to_bytes(2, byteorder='little')
|
||||
power_data[14:16] = (0xBAD8).to_bytes(2, byteorder='little')
|
||||
|
||||
return bytes(power_data)
|
||||
|
||||
def generate_indoor_bike_data():
|
||||
global current_speed, current_cadence
|
||||
|
||||
# Varia leggermente i valori
|
||||
current_speed += random.uniform(-0.2, 0.2)
|
||||
current_speed = max(0, min(60.0, current_speed))
|
||||
|
||||
# Crea Indoor Bike Data
|
||||
bike_data = bytearray(8)
|
||||
bike_data[0:2] = (0x0044).to_bytes(2, byteorder='little')
|
||||
bike_data[2:4] = (int(current_speed * 100)).to_bytes(2, byteorder='little')
|
||||
bike_data[4:6] = (current_cadence).to_bytes(2, byteorder='little')
|
||||
bike_data[6:8] = (0).to_bytes(2, byteorder='little')
|
||||
|
||||
return bytes(bike_data)
|
||||
|
||||
async def run():
|
||||
# Crea server con minimo di parametri
|
||||
server = BlessServer(name=DEVICE_NAME)
|
||||
server.read_request_func = read_request
|
||||
server.write_request_func = write_request
|
||||
|
||||
logger.info(f"Configurazione del simulatore {DEVICE_NAME}...")
|
||||
|
||||
# 1. Servizi standard
|
||||
# Aggiungi Cycling Power Service
|
||||
await server.add_new_service(CYCLING_POWER_SERVICE)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
CYCLING_POWER_MEASUREMENT,
|
||||
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
CYCLING_POWER_FEATURE,
|
||||
GATTCharacteristicProperties.read,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
CYCLING_POWER_CONTROL_POINT,
|
||||
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
WAHOO_CUSTOM_CP_CHAR,
|
||||
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
# Aggiungi Fitness Machine Service
|
||||
await server.add_new_service(FITNESS_MACHINE_SERVICE)
|
||||
await server.add_new_characteristic(
|
||||
FITNESS_MACHINE_SERVICE,
|
||||
INDOOR_BIKE_DATA,
|
||||
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
FITNESS_MACHINE_SERVICE,
|
||||
FITNESS_MACHINE_CONTROL_POINT,
|
||||
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
FITNESS_MACHINE_SERVICE,
|
||||
FITNESS_MACHINE_FEATURE,
|
||||
GATTCharacteristicProperties.read,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
|
||||
# 2. Servizi Wahoo personalizzati
|
||||
# Wahoo Service 1
|
||||
await server.add_new_service(WAHOO_SERVICE_1)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_1,
|
||||
WAHOO_CHAR_1,
|
||||
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_1,
|
||||
WAHOO_CHAR_2,
|
||||
GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
|
||||
# Wahoo Service 3
|
||||
await server.add_new_service(WAHOO_SERVICE_3)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_3,
|
||||
WAHOO_CHAR_3,
|
||||
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
# Wahoo Service 6
|
||||
await server.add_new_service(WAHOO_SERVICE_6)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_6,
|
||||
WAHOO_CHAR_4,
|
||||
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
# Wahoo Service B
|
||||
await server.add_new_service(WAHOO_SERVICE_B)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_B,
|
||||
WAHOO_CHAR_5,
|
||||
GATTCharacteristicProperties.read | GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
logger.info("Configurazione dei servizi completata")
|
||||
|
||||
# Avvia il server
|
||||
await server.start()
|
||||
logger.info(f"{DEVICE_NAME} è ora in fase di advertising")
|
||||
|
||||
# Imposta i valori iniziali DOPO l'avvio del server
|
||||
# Valori per servizi standard
|
||||
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
|
||||
server.get_characteristic(CYCLING_POWER_FEATURE).value = (0x08).to_bytes(4, byteorder='little')
|
||||
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
|
||||
server.get_characteristic(FITNESS_MACHINE_FEATURE).value = (0x02C6).to_bytes(4, byteorder='little')
|
||||
|
||||
# Valori per caratteristiche Wahoo
|
||||
server.get_characteristic(WAHOO_CHAR_1).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_2).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_3).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_4).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_5).value = bytearray(1)
|
||||
|
||||
# Loop di aggiornamento
|
||||
try:
|
||||
counter = 0
|
||||
while True:
|
||||
# Aggiorna i dati principali
|
||||
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
|
||||
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
|
||||
|
||||
# Invia notifiche
|
||||
server.update_value(FITNESS_MACHINE_SERVICE, INDOOR_BIKE_DATA)
|
||||
server.update_value(CYCLING_POWER_SERVICE, CYCLING_POWER_MEASUREMENT)
|
||||
|
||||
if counter % 10 == 0: # Log ogni 10 cicli
|
||||
logger.info(f"Potenza: {current_power}W, Cadenza: {current_cadence}rpm, Velocità: {current_speed:.1f}km/h")
|
||||
|
||||
counter += 1
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Arresto richiesto dall'utente")
|
||||
except Exception as e:
|
||||
logger.error(f"Errore durante l'esecuzione: {e}")
|
||||
finally:
|
||||
await server.stop()
|
||||
logger.info("Server arrestato")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 80)
|
||||
print(f"Wahoo KICKR 51A6 BLE Simulator per macOS (Versione completa)")
|
||||
print("=" * 80)
|
||||
print(f"Avvio della simulazione di {DEVICE_NAME}")
|
||||
print("Premi Ctrl+C per terminare il server")
|
||||
print("=" * 80)
|
||||
|
||||
try:
|
||||
asyncio.run(run())
|
||||
except KeyboardInterrupt:
|
||||
print("\nSimulazione fermata dall'utente")
|
||||
except Exception as e:
|
||||
print(f"Errore: {e}")
|
||||
print("Potrebbe essere necessario eseguire questo script con sudo")
|
||||
sys.exit(1)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,15 @@ import QtQuick.Layouts 1.3
|
||||
import QtWebView 1.1
|
||||
|
||||
Item {
|
||||
id: pelotonAuthPage
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: true
|
||||
|
||||
// Signal to notify the parent stack when we want to go back
|
||||
signal goBack()
|
||||
|
||||
WebView {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
@@ -27,7 +31,11 @@ Item {
|
||||
parent: Overlay.overlay
|
||||
enabled: rootItem.pelotonPopupVisible
|
||||
onEnabledChanged: { if(rootItem.pelotonPopupVisible) popupPelotonConnectedWeb.open() }
|
||||
onClosed: { rootItem.pelotonPopupVisible = false; }
|
||||
onClosed: {
|
||||
rootItem.pelotonPopupVisible = false;
|
||||
// Attempt to go back to the previous view after the popup is closed
|
||||
goBack();
|
||||
}
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
@@ -45,14 +53,28 @@ Item {
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 370
|
||||
height: 120
|
||||
text: qsTr("Your Peloton account is now connected!<br><br>Restart the app to apply this!")
|
||||
text: qsTr("Your Peloton account is now connected!")
|
||||
}
|
||||
}
|
||||
|
||||
// Add a MouseArea to capture clicks anywhere on the popup
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
popupPelotonConnectedWeb.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Component is being completed
|
||||
Component.onCompleted: {
|
||||
console.log("WebPelotonAuth loaded")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Qt.labs.settings 1.0
|
||||
|
||||
Page {
|
||||
id: wizardPage
|
||||
objectName: "wizardPage"
|
||||
|
||||
property int currentStep: 0
|
||||
property var selectedOptions: ({})
|
||||
@@ -335,7 +336,7 @@ Page {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Peloton Login")
|
||||
text: qsTr("Connect to Peloton")
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: "white"
|
||||
@@ -343,56 +344,38 @@ Page {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Username")
|
||||
text: qsTr("Click the button below to connect your Peloton account")
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
width: stackViewLocal.width * 0.8
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "white"
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: pelotonUsernameTextField
|
||||
text: settings.peloton_username
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
onAccepted: settings.peloton_username = text
|
||||
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
|
||||
}
|
||||
source: "icons/icons/Button_Connect_Rect_DarkMode.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
width: parent.width * 0.8
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Password")
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
color: "white"
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: pelotonPasswordTextField
|
||||
text: settings.peloton_password
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
inputMethodHints: Qt.ImhHiddenText
|
||||
echoMode: TextInput.PasswordEchoOnEdit
|
||||
onAccepted: settings.peloton_password = text
|
||||
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
stackViewLocal.push("WebPelotonAuth.qml")
|
||||
stackViewLocal.currentItem.goBack.connect(function() {
|
||||
stackViewLocal.pop();
|
||||
stackViewLocal.push(pelotonDifficultyComponent)
|
||||
})
|
||||
peloton_connect_clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 50
|
||||
}
|
||||
|
||||
WizardButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Next")
|
||||
onClicked: {
|
||||
settings.peloton_username = pelotonUsernameTextField.text;
|
||||
settings.peloton_password = pelotonPasswordTextField.text;
|
||||
stackViewLocal.push(pelotonDifficultyComponent)
|
||||
}
|
||||
}
|
||||
|
||||
WizardButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Back")
|
||||
|
||||
227
src/WorkoutsHistory.qml
Normal file
227
src/WorkoutsHistory.qml
Normal file
@@ -0,0 +1,227 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtCharts 2.15
|
||||
|
||||
Page {
|
||||
id: workoutHistoryPage
|
||||
|
||||
// Signal for chart preview
|
||||
signal fitfile_preview_clicked(var url)
|
||||
|
||||
// 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"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Workout History"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: workoutModel
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
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: "🗑️ Delete"
|
||||
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"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 16
|
||||
|
||||
// Sport icon
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Text {
|
||||
text: getSportIcon(sport)
|
||||
font.pixelSize: 32
|
||||
}
|
||||
}
|
||||
|
||||
// Workout info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: title
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
}
|
||||
|
||||
Text {
|
||||
text: date
|
||||
color: "#666666"
|
||||
}
|
||||
|
||||
// Stats row
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "⏱ " + duration
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "📏 " + distance.toFixed(2) + " km"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "🔥 " + Math.round(calories) + " kcal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
|
||||
var fileUrl = Qt.resolvedUrl("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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.18.20" android:versionCode="1020" 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.19.2" android:versionCode="1114" 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 ->
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
@@ -23,110 +22,145 @@ import android.widget.Button;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.content.Intent;
|
||||
|
||||
public class Ant {
|
||||
|
||||
private ChannelService.ChannelServiceComm mChannelService = null;
|
||||
private boolean mChannelServiceBound = false;
|
||||
private final String TAG = "Ant";
|
||||
public static Activity activity = null;
|
||||
static boolean speedRequest = false;
|
||||
static boolean heartRequest = false;
|
||||
static boolean bikeRequest = false; // Added bike request flag
|
||||
static boolean garminKey = false;
|
||||
static boolean treadmill = false;
|
||||
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill) {
|
||||
Log.v(TAG, "antStart");
|
||||
speedRequest = SpeedRequest;
|
||||
heartRequest = HeartRequest;
|
||||
treadmill = Treadmill;
|
||||
garminKey = GarminKey;
|
||||
// Updated antStart method with BikeRequest parameter at the end
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest) {
|
||||
QLog.v(TAG, "antStart");
|
||||
speedRequest = SpeedRequest;
|
||||
heartRequest = HeartRequest;
|
||||
treadmill = Treadmill;
|
||||
garminKey = GarminKey;
|
||||
bikeRequest = BikeRequest; // Set bike request flag
|
||||
activity = a;
|
||||
if(a != null)
|
||||
QLog.v(TAG, "antStart activity is valid");
|
||||
else
|
||||
{
|
||||
QLog.v(TAG, "antStart activity is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
activity = a;
|
||||
if(a != null)
|
||||
Log.v(TAG, "antStart activity is valid");
|
||||
else
|
||||
{
|
||||
Log.v(TAG, "antStart activity is invalid");
|
||||
return;
|
||||
}
|
||||
if(!mChannelServiceBound) doBindChannelService();
|
||||
}
|
||||
|
||||
private ServiceConnection mChannelServiceConnection = new ServiceConnection()
|
||||
{
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
|
||||
{
|
||||
Log.v(TAG, "mChannelServiceConnection.onServiceConnected...");
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
|
||||
{
|
||||
QLog.v(TAG, "mChannelServiceConnection.onServiceConnected...");
|
||||
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
|
||||
QLog.v(TAG, "...mChannelServiceConnection.onServiceConnected");
|
||||
}
|
||||
|
||||
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
|
||||
|
||||
|
||||
Log.v(TAG, "...mChannelServiceConnection.onServiceConnected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0)
|
||||
{
|
||||
Log.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
|
||||
|
||||
// Clearing and disabling when disconnecting from ChannelService
|
||||
mChannelService = null;
|
||||
|
||||
Log.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0)
|
||||
{
|
||||
QLog.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
|
||||
// Clearing and disabling when disconnecting from ChannelService
|
||||
mChannelService = null;
|
||||
QLog.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
|
||||
}
|
||||
};
|
||||
|
||||
private void doBindChannelService()
|
||||
{
|
||||
Log.v(TAG, "doBindChannelService...");
|
||||
|
||||
// Binds to ChannelService. ChannelService binds and manages connection between the
|
||||
// app and the ANT Radio Service
|
||||
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection , Context.BIND_AUTO_CREATE);
|
||||
|
||||
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
|
||||
doUnbindChannelService();
|
||||
|
||||
Log.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
|
||||
|
||||
Log.v(TAG, "...doBindChannelService");
|
||||
}
|
||||
QLog.v(TAG, "doBindChannelService...");
|
||||
// Binds to ChannelService. ChannelService binds and manages connection between the
|
||||
// app and the ANT Radio Service
|
||||
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
|
||||
doUnbindChannelService();
|
||||
QLog.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
|
||||
QLog.v(TAG, "...doBindChannelService");
|
||||
}
|
||||
|
||||
public void doUnbindChannelService()
|
||||
{
|
||||
Log.v(TAG, "doUnbindChannelService...");
|
||||
|
||||
if(mChannelServiceBound)
|
||||
{
|
||||
activity.unbindService(mChannelServiceConnection);
|
||||
|
||||
mChannelServiceBound = false;
|
||||
}
|
||||
|
||||
Log.v(TAG, "...doUnbindChannelService");
|
||||
}
|
||||
QLog.v(TAG, "doUnbindChannelService...");
|
||||
if(mChannelServiceBound)
|
||||
{
|
||||
activity.unbindService(mChannelServiceConnection);
|
||||
mChannelServiceBound = false;
|
||||
}
|
||||
QLog.v(TAG, "...doUnbindChannelService");
|
||||
}
|
||||
|
||||
public void setCadenceSpeedPower(float speed, int power, int cadence)
|
||||
{
|
||||
if(mChannelService == null)
|
||||
return;
|
||||
|
||||
Log.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
|
||||
mChannelService.setSpeed(speed);
|
||||
mChannelService.setPower(power);
|
||||
mChannelService.setCadence(cadence);
|
||||
if(mChannelService == null)
|
||||
return;
|
||||
QLog.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
|
||||
mChannelService.setSpeed(speed);
|
||||
mChannelService.setPower(power);
|
||||
mChannelService.setCadence(cadence);
|
||||
}
|
||||
|
||||
public int getHeart()
|
||||
{
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getHeart");
|
||||
return mChannelService.getHeart();
|
||||
}
|
||||
|
||||
Log.v(TAG, "getHeart");
|
||||
return mChannelService.getHeart();
|
||||
// Added bike-related getter methods
|
||||
public int getBikeCadence() {
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getBikeCadence");
|
||||
return mChannelService.getBikeCadence();
|
||||
}
|
||||
|
||||
public int getBikePower() {
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getBikePower");
|
||||
return mChannelService.getBikePower();
|
||||
}
|
||||
|
||||
public double getBikeSpeed() {
|
||||
if(mChannelService == null)
|
||||
return 0.0;
|
||||
QLog.v(TAG, "getBikeSpeed");
|
||||
return mChannelService.getBikeSpeed();
|
||||
}
|
||||
|
||||
public long getBikeDistance() {
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getBikeDistance");
|
||||
return mChannelService.getBikeDistance();
|
||||
}
|
||||
|
||||
public boolean isBikeConnected() {
|
||||
if(mChannelService == null)
|
||||
return false;
|
||||
QLog.v(TAG, "isBikeConnected");
|
||||
return mChannelService.isBikeConnected();
|
||||
}
|
||||
|
||||
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
|
||||
double elapsedTimeSeconds, int resistance,
|
||||
double inclination) {
|
||||
if(mChannelService == null)
|
||||
return;
|
||||
QLog.v(TAG, "updateBikeTransmitterExtendedMetrics");
|
||||
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
|
||||
elapsedTimeSeconds, resistance,
|
||||
inclination);
|
||||
}
|
||||
}
|
||||
|
||||
239
src/android/src/BikeChannelController.java
Normal file
239
src/android/src/BikeChannelController.java
Normal file
@@ -0,0 +1,239 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.app.Activity;
|
||||
|
||||
// ANT+ Plugin imports
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IFitnessEquipmentStateReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IBikeDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IGeneralFitnessEquipmentDataReceiver;
|
||||
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.defines.DeviceState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
|
||||
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IDeviceStateChangeReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IPluginAccessResultReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle;
|
||||
|
||||
// Java imports
|
||||
import java.math.BigDecimal;
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class BikeChannelController {
|
||||
private static final String TAG = BikeChannelController.class.getSimpleName();
|
||||
|
||||
private Context context;
|
||||
private AntPlusFitnessEquipmentPcc fePcc = null;
|
||||
private PccReleaseHandle<AntPlusFitnessEquipmentPcc> releaseHandle = null;
|
||||
private boolean isConnected = false;
|
||||
|
||||
// Bike data fields
|
||||
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
|
||||
public long distance = 0; // Total distance in meters
|
||||
public long calories = 0; // Total calories burned
|
||||
public EquipmentType equipmentType = EquipmentType.UNKNOWN;
|
||||
public EquipmentState equipmentState = EquipmentState.ASLEEP_OFF;
|
||||
public int heartRate = 0; // Heart rate from equipment
|
||||
public HeartRateDataSource heartRateSource = HeartRateDataSource.UNKNOWN;
|
||||
public BigDecimal elapsedTime = new BigDecimal(0); // Elapsed time in seconds
|
||||
|
||||
// Fitness equipment state receiver
|
||||
private final IFitnessEquipmentStateReceiver mFitnessEquipmentStateReceiver =
|
||||
new IFitnessEquipmentStateReceiver() {
|
||||
@Override
|
||||
public void onNewFitnessEquipmentState(long estTimestamp,
|
||||
EnumSet<EventFlag> eventFlags,
|
||||
EquipmentType type,
|
||||
EquipmentState state) {
|
||||
equipmentType = type;
|
||||
equipmentState = state;
|
||||
QLog.d(TAG, "Equipment type: " + type + ", State: " + state);
|
||||
|
||||
// Only subscribe to bike specific data if this is actually a bike
|
||||
if (type == EquipmentType.BIKE && !isSubscribedToBikeData) {
|
||||
subscribeToBikeSpecificData();
|
||||
isSubscribedToBikeData = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public BikeChannelController() {
|
||||
this.context = Ant.activity;
|
||||
openChannel();
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
// Request access to first available fitness equipment device
|
||||
// Using requestNewOpenAccess from the sample code
|
||||
releaseHandle = AntPlusFitnessEquipmentPcc.requestNewOpenAccess(
|
||||
(Activity)context,
|
||||
context,
|
||||
new IPluginAccessResultReceiver<AntPlusFitnessEquipmentPcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusFitnessEquipmentPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
fePcc = result;
|
||||
isConnected = true;
|
||||
QLog.d(TAG, "Connected to fitness equipment: " + result.getDeviceName());
|
||||
subscribeToBikeEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isConnected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mFitnessEquipmentStateReceiver
|
||||
);
|
||||
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
private void subscribeToBikeEvents() {
|
||||
if (fePcc != null) {
|
||||
// General fitness equipment data
|
||||
fePcc.subscribeGeneralFitnessEquipmentDataEvent(new IGeneralFitnessEquipmentDataReceiver() {
|
||||
@Override
|
||||
public void onNewGeneralFitnessEquipmentData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal elapsedTime, long cumulativeDistance,
|
||||
BigDecimal instantaneousSpeed, boolean virtualInstantaneousSpeed,
|
||||
int instantaneousHeartRate, HeartRateDataSource source) {
|
||||
|
||||
if (elapsedTime != null && elapsedTime.intValue() != -1) {
|
||||
BikeChannelController.this.elapsedTime = elapsedTime;
|
||||
}
|
||||
|
||||
if (cumulativeDistance != -1) {
|
||||
distance = cumulativeDistance;
|
||||
}
|
||||
|
||||
if (instantaneousSpeed != null && instantaneousSpeed.intValue() != -1) {
|
||||
speed = instantaneousSpeed;
|
||||
}
|
||||
|
||||
if (instantaneousHeartRate != -1) {
|
||||
heartRate = instantaneousHeartRate;
|
||||
heartRateSource = source;
|
||||
}
|
||||
|
||||
QLog.d(TAG, "General Data - Time: " + elapsedTime + "s, Distance: " +
|
||||
distance + "m, Speed: " + speed + "m/s, HR: " + heartRate + "bpm");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSubscribedToBikeData = false;
|
||||
|
||||
private void subscribeToBikeSpecificData() {
|
||||
if (fePcc != null) {
|
||||
// Subscribe to bike specific data
|
||||
fePcc.getBikeMethods().subscribeBikeDataEvent(new IBikeDataReceiver() {
|
||||
@Override
|
||||
public void onNewBikeData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
int instantaneousCadence, int instantaneousPower) {
|
||||
|
||||
if (instantaneousCadence != -1) {
|
||||
cadence = instantaneousCadence;
|
||||
}
|
||||
|
||||
if (instantaneousPower != -1) {
|
||||
power = instantaneousPower;
|
||||
}
|
||||
|
||||
QLog.d(TAG, "Bike Data - Cadence: " + cadence + "rpm, Power: " + power + "W");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (releaseHandle != null) {
|
||||
releaseHandle.close();
|
||||
releaseHandle = null;
|
||||
}
|
||||
fePcc = null;
|
||||
isConnected = false;
|
||||
QLog.d(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
// Getter methods for bike data
|
||||
public int getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public int getPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public double getSpeedKph() {
|
||||
// Convert from m/s to km/h
|
||||
return speed.doubleValue() * 3.6;
|
||||
}
|
||||
|
||||
public double getSpeedMps() {
|
||||
return speed.doubleValue();
|
||||
}
|
||||
|
||||
public long getDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
public long getCalories() {
|
||||
return calories;
|
||||
}
|
||||
|
||||
public int getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
public BigDecimal getElapsedTime() {
|
||||
return elapsedTime;
|
||||
}
|
||||
|
||||
public EquipmentState getEquipmentState() {
|
||||
return equipmentState;
|
||||
}
|
||||
|
||||
public EquipmentType getEquipmentType() {
|
||||
return equipmentType;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
}
|
||||
651
src/android/src/BikeTransmitterController.java
Normal file
651
src/android/src/BikeTransmitterController.java
Normal file
@@ -0,0 +1,651 @@
|
||||
/*
|
||||
* Copyright 2012 Dynastream Innovations Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
import com.dsi.ant.channel.IAntChannelEventHandler;
|
||||
import com.dsi.ant.message.ChannelId;
|
||||
import com.dsi.ant.message.ChannelType;
|
||||
import com.dsi.ant.message.EventCode;
|
||||
import com.dsi.ant.message.fromant.AcknowledgedDataMessage;
|
||||
import com.dsi.ant.message.fromant.ChannelEventMessage;
|
||||
import com.dsi.ant.message.fromant.MessageFromAntType;
|
||||
import com.dsi.ant.message.ipc.AntMessageParcel;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* ANT+ Bike Transmitter Controller
|
||||
* Follows exactly the same pattern as PowerChannelController but for Fitness Equipment
|
||||
*/
|
||||
public class BikeTransmitterController {
|
||||
public static final int FITNESS_EQUIPMENT_SENSOR_ID = 0x9e3d4b67; // Different from power sensor
|
||||
// The device type and transmission type to be part of the channel ID message
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE = 17; // Fitness Equipment
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE = 5;
|
||||
// The period and frequency values the channel will be configured to
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_PERIOD = 8192; // 4 Hz for FE
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_FREQUENCY = 57;
|
||||
private static final String TAG = BikeTransmitterController.class.getSimpleName();
|
||||
|
||||
// ANT+ Data Page IDs for Fitness Equipment
|
||||
private static final byte DATA_PAGE_GENERAL_FE = 0x10;
|
||||
private static final byte DATA_PAGE_BIKE_DATA = 0x19;
|
||||
private static final byte DATA_PAGE_TRAINER_DATA = 0x1A;
|
||||
private static final byte DATA_PAGE_GENERAL_SETTINGS = 0x11;
|
||||
|
||||
private static Random randGen = new Random();
|
||||
|
||||
// Current bike metrics to transmit
|
||||
int currentCadence = 0; // Current cadence in RPM
|
||||
int currentPower = 0; // Current power in watts
|
||||
double currentSpeedKph = 0.0; // Current speed in km/h
|
||||
long totalDistance = 0; // Total distance in meters
|
||||
int currentHeartRate = 0; // Heart rate in BPM
|
||||
double elapsedTimeSeconds = 0.0; // Elapsed time in seconds
|
||||
int currentResistance = 0; // Current resistance level (0-100)
|
||||
double currentInclination = 0.0; // Current inclination in percentage
|
||||
|
||||
// Control commands received from ANT+ devices
|
||||
private int requestedResistance = -1; // Requested resistance from controller
|
||||
private int requestedPower = -1; // Requested power from controller
|
||||
private double requestedInclination = -100; // Requested inclination from controller
|
||||
|
||||
private AntChannel mAntChannel;
|
||||
private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback();
|
||||
private boolean mIsOpen;
|
||||
|
||||
// Callbacks for control commands
|
||||
public interface ControlCommandListener {
|
||||
void onResistanceChangeRequested(int resistance);
|
||||
void onPowerChangeRequested(int power);
|
||||
void onInclinationChangeRequested(double inclination);
|
||||
}
|
||||
|
||||
private ControlCommandListener controlListener = null;
|
||||
|
||||
public BikeTransmitterController(AntChannel antChannel) {
|
||||
mAntChannel = antChannel;
|
||||
openChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listener for control commands received from ANT+ devices
|
||||
*/
|
||||
public void setControlCommandListener(ControlCommandListener listener) {
|
||||
this.controlListener = listener;
|
||||
}
|
||||
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type
|
||||
ChannelId channelId = new ChannelId(FITNESS_EQUIPMENT_SENSOR_ID & 0xFFFF,
|
||||
CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE, CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE);
|
||||
|
||||
try {
|
||||
// Setting the channel event handler so that we can receive messages from ANT
|
||||
mAntChannel.setChannelEventHandler(mChannelEventCallback);
|
||||
|
||||
// Performs channel assignment by assigning the type to the channel
|
||||
mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER);
|
||||
|
||||
// Configures the channel ID, messaging period and rf frequency after assigning,
|
||||
// then opening the channel.
|
||||
mAntChannel.setChannelId(channelId);
|
||||
mAntChannel.setPeriod(CHANNEL_FITNESS_EQUIPMENT_PERIOD);
|
||||
mAntChannel.setRfFrequency(CHANNEL_FITNESS_EQUIPMENT_FREQUENCY);
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
QLog.d(TAG, "Opened fitness equipment channel with device number: " + FITNESS_EQUIPMENT_SENSOR_ID);
|
||||
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
} catch (AntCommandFailedException e) {
|
||||
// This will release, and therefore unassign if required
|
||||
channelError("Open failed", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QLog.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
}
|
||||
|
||||
public boolean startTransmission() {
|
||||
return openChannel();
|
||||
}
|
||||
|
||||
public void stopTransmission() {
|
||||
close();
|
||||
}
|
||||
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
QLog.e(TAG, logString);
|
||||
}
|
||||
|
||||
void channelError(String error, AntCommandFailedException e) {
|
||||
StringBuilder logString;
|
||||
|
||||
if (e.getResponseMessage() != null) {
|
||||
String initiatingMessageId = "0x" + Integer.toHexString(
|
||||
e.getResponseMessage().getInitiatingMessageId());
|
||||
String rawResponseCode = "0x" + Integer.toHexString(
|
||||
e.getResponseMessage().getRawResponseCode());
|
||||
|
||||
logString = new StringBuilder(error)
|
||||
.append(". Command ")
|
||||
.append(initiatingMessageId)
|
||||
.append(" failed with code ")
|
||||
.append(rawResponseCode);
|
||||
} else {
|
||||
String attemptedMessageId = "0x" + Integer.toHexString(
|
||||
e.getAttemptedMessageType().getMessageId());
|
||||
String failureReason = e.getFailureReason().toString();
|
||||
|
||||
logString = new StringBuilder(error)
|
||||
.append(". Command ")
|
||||
.append(attemptedMessageId)
|
||||
.append(" failed with reason ")
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
QLog.e(TAG, logString.toString());
|
||||
mAntChannel.release();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (null != mAntChannel) {
|
||||
mIsOpen = false;
|
||||
// Releasing the channel to make it available for others.
|
||||
// After releasing, the AntChannel instance cannot be reused.
|
||||
mAntChannel.release();
|
||||
mAntChannel = null;
|
||||
}
|
||||
QLog.e(TAG, "Fitness Equipment Channel Closed");
|
||||
}
|
||||
|
||||
// Setter methods for updating bike metrics from the main application
|
||||
public void setCadence(int cadence) {
|
||||
this.currentCadence = Math.max(0, cadence);
|
||||
}
|
||||
|
||||
public void setPower(int power) {
|
||||
this.currentPower = Math.max(0, power);
|
||||
}
|
||||
|
||||
public void setSpeedKph(double speedKph) {
|
||||
this.currentSpeedKph = Math.max(0, speedKph);
|
||||
}
|
||||
|
||||
public void setDistance(long distance) {
|
||||
this.totalDistance = Math.max(0, distance);
|
||||
}
|
||||
|
||||
public void setHeartRate(int heartRate) {
|
||||
this.currentHeartRate = Math.max(0, Math.min(255, heartRate));
|
||||
}
|
||||
|
||||
public void setElapsedTime(double timeSeconds) {
|
||||
this.elapsedTimeSeconds = Math.max(0, timeSeconds);
|
||||
}
|
||||
|
||||
public void setResistance(int resistance) {
|
||||
this.currentResistance = Math.max(0, Math.min(100, resistance));
|
||||
}
|
||||
|
||||
public void setInclination(double inclination) {
|
||||
this.currentInclination = Math.max(-100, Math.min(100, inclination));
|
||||
}
|
||||
|
||||
// Getter methods for the last requested control values
|
||||
public int getRequestedResistance() {
|
||||
return requestedResistance;
|
||||
}
|
||||
|
||||
public int getRequestedPower() {
|
||||
return requestedPower;
|
||||
}
|
||||
|
||||
public double getRequestedInclination() {
|
||||
return requestedInclination;
|
||||
}
|
||||
|
||||
public void clearControlRequests() {
|
||||
requestedResistance = -1;
|
||||
requestedPower = -1;
|
||||
requestedInclination = -100;
|
||||
}
|
||||
|
||||
public boolean isTransmitting() {
|
||||
return mIsOpen;
|
||||
}
|
||||
|
||||
public String getTransmissionInfo() {
|
||||
if (!mIsOpen) {
|
||||
return "Transmission: STOPPED";
|
||||
}
|
||||
|
||||
return String.format("Transmission: ACTIVE - Cadence: %drpm, Power: %dW, " +
|
||||
"Speed: %.1fkm/h, Resistance: %d, Inclination: %.1f%%",
|
||||
currentCadence, currentPower, currentSpeedKph,
|
||||
currentResistance, currentInclination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert byte array to hex string for debugging
|
||||
*/
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
hex.append(String.format("%02X ", b & 0xFF));
|
||||
}
|
||||
return hex.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Channel Event Handler Interface following PowerChannelController pattern
|
||||
*/
|
||||
public class ChannelEventCallback implements IAntChannelEventHandler {
|
||||
|
||||
int cnt = 0;
|
||||
int eventCount = 0;
|
||||
int eventPowerCount = 0;
|
||||
int cumulativeDistance = 0;
|
||||
int cumulativeWatt = 0;
|
||||
int accumulatedTorque32 = 0;
|
||||
Timer carousalTimer = null;
|
||||
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
QLog.e(TAG, "Fitness Equipment Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
// Start unsolicited transmission timer like PowerChannelController
|
||||
if(carousalTimer == null) {
|
||||
carousalTimer = new Timer(); // At this line a new Thread will be created
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d(TAG, "Tx Unsolicited Fitness Equipment Data");
|
||||
byte[] payload = new byte[8];
|
||||
String debugString = "";
|
||||
eventCount = (eventCount + 1) & 0xFF;
|
||||
cumulativeDistance = (cumulativeDistance + (int)(currentSpeedKph / 3.6)) & 0xFFFF; // rough distance calc
|
||||
|
||||
cnt += 1;
|
||||
|
||||
// Cycle through different data pages like PowerChannelController
|
||||
if (cnt % 5 == 0) {
|
||||
// General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
} else if (cnt % 5 == 1) {
|
||||
// Bike Data Page (0x19)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 5 == 2) {
|
||||
// Trainer Data Page (0x1A)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 5 == 3) {
|
||||
// General Settings Page (0x11)
|
||||
debugString = buildGeneralSettingsPage(payload);
|
||||
} else {
|
||||
// Default General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
}
|
||||
|
||||
// Log the hex data and parsed values
|
||||
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
|
||||
QLog.d(TAG, debugString);
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
// Setting the data to be broadcast on the next channel period
|
||||
mAntChannel.setBroadcastData(payload);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0, 250); // Every 250ms for 4Hz
|
||||
}
|
||||
|
||||
// Switching on message type to handle different types of messages
|
||||
switch (messageType) {
|
||||
case BROADCAST_DATA:
|
||||
// Rx Data
|
||||
break;
|
||||
case ACKNOWLEDGED_DATA:
|
||||
// Handle control commands
|
||||
payload = new AcknowledgedDataMessage(antParcel).getPayload();
|
||||
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
|
||||
handleControlCommand(payload);
|
||||
break;
|
||||
case CHANNEL_EVENT:
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
case TX:
|
||||
cnt += 1;
|
||||
String debugString = "";
|
||||
|
||||
// Cycle through different data pages like PowerChannelController
|
||||
if (cnt % 16 == 1) {
|
||||
// General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
} else if (cnt % 16 == 5) {
|
||||
// Bike Data Page (0x19)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 16 == 9) {
|
||||
// Trainer Data Page (0x1A)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 16 == 13) {
|
||||
// General Settings Page (0x11)
|
||||
debugString = buildGeneralSettingsPage(payload);
|
||||
} else {
|
||||
// Default General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
}
|
||||
|
||||
// Log the hex data and parsed values
|
||||
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
|
||||
QLog.d(TAG, debugString);
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
// Setting the data to be broadcast on the next channel period
|
||||
mAntChannel.setBroadcastData(payload);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COLLISION:
|
||||
cnt += 1;
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
QLog.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
case RX_FAIL_GO_TO_SEARCH:
|
||||
case TRANSFER_RX_FAILED:
|
||||
case TRANSFER_TX_COMPLETED:
|
||||
case TRANSFER_TX_FAILED:
|
||||
case TRANSFER_TX_START:
|
||||
case UNKNOWN:
|
||||
// TODO More complex communication will need to handle these events
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ANT_VERSION:
|
||||
case BURST_TRANSFER_DATA:
|
||||
case CAPABILITIES:
|
||||
case CHANNEL_ID:
|
||||
case CHANNEL_RESPONSE:
|
||||
case CHANNEL_STATUS:
|
||||
case SERIAL_NUMBER:
|
||||
case OTHER:
|
||||
// TODO More complex communication will need to handle these message types
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build General Fitness Equipment Data Page (0x10) - Page 16
|
||||
* Following Table 8-7 format exactly
|
||||
* @param payload byte array to populate
|
||||
* @return debug string with hex and parsed values
|
||||
*/
|
||||
private String buildGeneralFEDataPage(byte[] payload) {
|
||||
payload[0] = 0x10; // Data Page Number = 0x10 (Page 16)
|
||||
|
||||
// Byte 1: Equipment Type Bit Field (Refer to Table 8-8)
|
||||
payload[1] = 0x19; // Equipment type: Bike (stationary bike = 0x19)
|
||||
|
||||
// Byte 2: Elapsed Time (0.25 seconds resolution, rollover at 64s)
|
||||
int elapsedTime025s = (int) (elapsedTimeSeconds * 4) & 0xFF;
|
||||
payload[2] = (byte) elapsedTime025s;
|
||||
|
||||
// Byte 3: Distance Traveled (1 meter resolution, rollover at 256m)
|
||||
int distanceMeters = (int) (totalDistance) & 0xFF;
|
||||
payload[3] = (byte) distanceMeters;
|
||||
|
||||
// Bytes 4-5: Speed (0.001 m/s resolution, 0xFFFF = invalid)
|
||||
int speedMms = (int) (currentSpeedKph / 3.6 * 1000);
|
||||
if (speedMms > 65534) speedMms = 65534; // Max valid value
|
||||
payload[4] = (byte) (speedMms & 0xFF); // Speed LSB
|
||||
payload[5] = (byte) ((speedMms >> 8) & 0xFF); // Speed MSB
|
||||
|
||||
// Byte 6: Heart Rate (0xFF = invalid)
|
||||
payload[6] = (byte) (currentHeartRate == 0 ? 0xFF : currentHeartRate);
|
||||
|
||||
// Byte 7: Capabilities Bit Field (4 bits 0:3) + FE State Bit Field (4 bits 4:7)
|
||||
payload[7] = 0x00; // Set to 0x00 for now (refer to Tables 8-9 and 8-10)
|
||||
|
||||
// Create debug string
|
||||
return String.format(Locale.US,
|
||||
"General FE Data Page (0x10): " +
|
||||
"Page=0x%02X, Equipment=0x%02X(Bike), " +
|
||||
"ElapsedTime=0x%02X(%.1fs), Distance=0x%02X(%dm), " +
|
||||
"Speed=0x%02X%02X(%.1fkm/h), HeartRate=0x%02X(%s), " +
|
||||
"Capabilities=0x%02X",
|
||||
payload[0] & 0xFF, payload[1] & 0xFF,
|
||||
payload[2] & 0xFF, elapsedTimeSeconds,
|
||||
payload[3] & 0xFF, distanceMeters,
|
||||
payload[5] & 0xFF, payload[4] & 0xFF, currentSpeedKph,
|
||||
payload[6] & 0xFF, currentHeartRate == 0 ? "Invalid" : currentHeartRate + "bpm",
|
||||
payload[7] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Specific Trainer/Stationary Bike Data Page (0x19) - Page 25
|
||||
* Following Table 8-25 format exactly
|
||||
* @param payload byte array to populate
|
||||
* @return debug string with hex and parsed values
|
||||
*/
|
||||
private String buildBikeDataPage(byte[] payload) {
|
||||
payload[0] = 0x19; // Data Page Number = 0x19 (Page 25)
|
||||
|
||||
// Byte 1: Update Event Count (increments with each information update)
|
||||
eventPowerCount = (eventPowerCount + 1) & 0xFF;
|
||||
payload[1] = (byte) eventPowerCount;
|
||||
|
||||
// Byte 2: Instantaneous Cadence (RPM, 0xFF = invalid)
|
||||
payload[2] = (byte) (currentCadence == 0 ? 0xFF : currentCadence);
|
||||
|
||||
// Bytes 3-4: Accumulated Power (1 watt resolution, rollover at 65536W)
|
||||
// This is cumulative power, not instantaneous
|
||||
cumulativeWatt = (cumulativeWatt + currentPower);
|
||||
payload[3] = (byte) (cumulativeWatt & 0xFF); // Accumulated Power LSB
|
||||
payload[4] = (byte) ((cumulativeWatt >> 8) & 0xFF); // Accumulated Power MSB
|
||||
|
||||
// Bytes 5-6: Instantaneous Power (1.5 bytes, 0xFFF = invalid for both fields)
|
||||
if (currentPower > 4094) {
|
||||
// 0xFFF indicates BOTH instantaneous and accumulated power fields are invalid
|
||||
payload[5] = (byte) 0xFF; // Instantaneous Power LSB
|
||||
payload[6] = (byte) 0xFF; // Instantaneous Power MSB (bits 0-3) + Trainer Status (bits 4-7)
|
||||
} else {
|
||||
payload[5] = (byte) (currentPower & 0xFF); // Instantaneous Power LSB
|
||||
payload[6] = (byte) ((currentPower >> 8) & 0x0F); // Instantaneous Power MSN (bits 0-3)
|
||||
// Bits 4-7 of byte 6: Trainer Status Bit Field (refer to Table 8-27)
|
||||
payload[6] |= 0x00; // Trainer status = 0 for now
|
||||
}
|
||||
|
||||
// Byte 7: Flags Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
|
||||
payload[7] = 0x00; // Set to 0x00 for now
|
||||
|
||||
// Create debug string
|
||||
String cadenceStr = currentCadence == 0 ? "Invalid" : currentCadence + "rpm";
|
||||
String powerStr = currentPower > 4094 ? "Invalid" : currentPower + "W";
|
||||
|
||||
return String.format(Locale.US,
|
||||
"Bike Data Page (0x19): " +
|
||||
"Page=0x%02X, EventCount=0x%02X(%d), " +
|
||||
"Cadence=0x%02X(%s), AccumPower=0x%02X%02X(%dW), " +
|
||||
"InstPower=0x%X%02X(%s), Flags=0x%02X",
|
||||
payload[0] & 0xFF, payload[1] & 0xFF, eventCount,
|
||||
payload[2] & 0xFF, cadenceStr,
|
||||
payload[4] & 0xFF, payload[3] & 0xFF, cumulativeWatt,
|
||||
(payload[6] & 0x0F), payload[5] & 0xFF, powerStr,
|
||||
payload[7] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build General Settings Page (0x11) - Page 17
|
||||
* Following Table 8-11 format exactly
|
||||
* @param payload byte array to populate
|
||||
* @return debug string with hex and parsed values
|
||||
*/
|
||||
private String buildGeneralSettingsPage(byte[] payload) {
|
||||
payload[0] = 0x11; // Data Page Number = 0x11 (Page 17)
|
||||
|
||||
// Byte 1: Reserved (0xFF - Do not interpret)
|
||||
payload[1] = (byte) 0xFF;
|
||||
|
||||
// Byte 2: Reserved (0xFF - Do not interpret)
|
||||
payload[2] = (byte) 0xFF;
|
||||
|
||||
// Byte 3: Cycle length (0.01 meters resolution, 0xFF = invalid)
|
||||
// Length of one 'cycle' - for bike this could be wheel circumference
|
||||
int cycleLengthCm = 210; // 2.1m wheel circumference = 210cm
|
||||
payload[3] = (byte) (cycleLengthCm & 0xFF);
|
||||
|
||||
// Bytes 4-5: Incline Percentage (signed integer, 0.01% resolution, 0x7FFF = invalid)
|
||||
int inclinePercent001 = (int) (currentInclination * 100); // Convert to 0.01% units
|
||||
if (inclinePercent001 < -10000) inclinePercent001 = -10000; // Min -100.00%
|
||||
if (inclinePercent001 > 10000) inclinePercent001 = 10000; // Max +100.00%
|
||||
payload[4] = (byte) (inclinePercent001 & 0xFF); // Incline LSB
|
||||
payload[5] = (byte) ((inclinePercent001 >> 8) & 0xFF); // Incline MSB
|
||||
|
||||
// Byte 6: Resistance Level (0.5% resolution, percentage of maximum applicable resistance)
|
||||
int resistanceLevel05 = (int) (currentResistance * 2); // Convert to 0.5% units
|
||||
if (resistanceLevel05 > 200) resistanceLevel05 = 200; // Max 100% = 200 in 0.5% units
|
||||
payload[6] = (byte) (resistanceLevel05 & 0xFF);
|
||||
|
||||
// Byte 7: Capabilities Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
|
||||
payload[7] = 0x00; // Set to 0x00 for now
|
||||
|
||||
// Create debug string
|
||||
return String.format(Locale.US,
|
||||
"General Settings Page (0x11): " +
|
||||
"Page=0x%02X, Reserved1=0x%02X, Reserved2=0x%02X, " +
|
||||
"CycleLength=0x%02X(%.2fm), Incline=0x%02X%02X(%.2f%%), " +
|
||||
"Resistance=0x%02X(%d%%), Capabilities=0x%02X",
|
||||
payload[0] & 0xFF, payload[1] & 0xFF, payload[2] & 0xFF,
|
||||
payload[3] & 0xFF, cycleLengthCm / 100.0,
|
||||
payload[5] & 0xFF, payload[4] & 0xFF, currentInclination,
|
||||
payload[6] & 0xFF, currentResistance,
|
||||
payload[7] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming control commands
|
||||
*/
|
||||
private void handleControlCommand(byte[] data) {
|
||||
if (data.length < 8) return;
|
||||
|
||||
byte pageNumber = data[0];
|
||||
QLog.d(TAG, "Received control command page: 0x" + String.format("%02X", pageNumber));
|
||||
QLog.d(TAG, "Control Command HEX: " + bytesToHex(data));
|
||||
|
||||
// Handle control command pages
|
||||
switch (pageNumber) {
|
||||
case 0x30: // Basic Resistance
|
||||
handleBasicResistanceCommand(data);
|
||||
break;
|
||||
case 0x31: // Target Power
|
||||
handleTargetPowerCommand(data);
|
||||
break;
|
||||
case 0x33: // Track Resistance
|
||||
handleTrackResistanceCommand(data);
|
||||
break;
|
||||
default:
|
||||
QLog.d(TAG, "Unknown control page: 0x" + String.format("%02X", pageNumber));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBasicResistanceCommand(byte[] data) {
|
||||
int resistance = data[7] & 0xFF; // Resistance in 0.5% increments
|
||||
double resistancePercent = resistance * 0.5;
|
||||
|
||||
QLog.d(TAG, String.format(Locale.US,
|
||||
"Basic Resistance Command (0x30): Resistance=0x%02X(%.1f%%)",
|
||||
resistance, resistancePercent));
|
||||
|
||||
if (resistancePercent != requestedResistance && controlListener != null) {
|
||||
requestedResistance = (int) resistancePercent;
|
||||
controlListener.onResistanceChangeRequested(requestedResistance);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTargetPowerCommand(byte[] data) {
|
||||
int targetPower = ((data[7] & 0xFF) << 8) | (data[6] & 0xFF);
|
||||
targetPower = targetPower / 4;
|
||||
|
||||
QLog.d(TAG, String.format(Locale.US,
|
||||
"Target Power Command (0x31): Power=0x%02X%02X(%dW)",
|
||||
data[7] & 0xFF, data[6] & 0xFF, targetPower));
|
||||
|
||||
if (targetPower != requestedPower && controlListener != null) {
|
||||
requestedPower = targetPower;
|
||||
controlListener.onPowerChangeRequested(targetPower);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTrackResistanceCommand(byte[] data) {
|
||||
// Grade is in 0.01% increments, signed 16-bit
|
||||
int gradeRaw = ((data[6] & 0xFF) << 8) | (data[5] & 0xFF);
|
||||
if (gradeRaw > 32767) gradeRaw -= 65536; // Convert to signed
|
||||
double grade = (gradeRaw - 0x4E20) * 0.01;
|
||||
|
||||
QLog.d(TAG, String.format(Locale.US,
|
||||
"Track Resistance Command (0x33): Grade=0x%02X%02X(%.2f%%)",
|
||||
data[6] & 0xFF, data[5] & 0xFF, grade));
|
||||
|
||||
if (Math.abs(grade - requestedInclination) > 0.1 && controlListener != null) {
|
||||
requestedInclination = grade;
|
||||
controlListener.onInclinationChangeRequested(grade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IntentFilter;
|
||||
@@ -89,12 +89,12 @@ public class BleAdvertiser {
|
||||
private static AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
|
||||
@Override
|
||||
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
|
||||
Log.d("BleAdvertiser", "Advertising started successfully");
|
||||
QLog.d("BleAdvertiser", "Advertising started successfully");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartFailure(int errorCode) {
|
||||
Log.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
|
||||
QLog.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class CSafeRowerUSBHID {
|
||||
|
||||
@@ -34,21 +34,21 @@ public class CSafeRowerUSBHID {
|
||||
static int lastReadLen = 0;
|
||||
|
||||
public static void open(Context context) {
|
||||
Log.d("QZ","CSafeRowerUSBHID open");
|
||||
QLog.d("QZ","CSafeRowerUSBHID open");
|
||||
hidBridge = new HidBridge(context, 0x0002, 0x17A4);
|
||||
boolean ret = hidBridge.OpenDevice();
|
||||
Log.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
QLog.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
if(ret == false) {
|
||||
hidBridge = new HidBridge(context, 0x0001, 0x17A4);
|
||||
ret = hidBridge.OpenDevice();
|
||||
Log.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
QLog.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
}
|
||||
hidBridge.StartReadingThread();
|
||||
Log.d("QZ","hidBridge.StartReadingThread");
|
||||
QLog.d("QZ","hidBridge.StartReadingThread");
|
||||
}
|
||||
|
||||
public static void write (byte[] bytes) {
|
||||
Log.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
|
||||
QLog.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
|
||||
hidBridge.WriteData(bytes);
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ public class CSafeRowerUSBHID {
|
||||
if(hidBridge.IsThereAnyReceivedData()) {
|
||||
receiveData = hidBridge.GetReceivedDataFromQueue();
|
||||
lastReadLen = receiveData.length;
|
||||
Log.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
|
||||
QLog.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
|
||||
return receiveData;
|
||||
} else {
|
||||
Log.d("QZ","CSafeRowerUSBHID empty data");
|
||||
QLog.d("QZ","CSafeRowerUSBHID empty data");
|
||||
lastReadLen = 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.SparseArray;
|
||||
import android.os.Build;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -49,15 +49,21 @@ public class ChannelService extends Service {
|
||||
private AntChannelProvider mAntChannelProvider = null;
|
||||
private boolean mAllowAddChannel = false;
|
||||
|
||||
public static native void nativeSetResistance(int resistance);
|
||||
public static native void nativeSetPower(int power);
|
||||
public static native void nativeSetInclination(double inclination);
|
||||
|
||||
HeartChannelController heartChannelController = null;
|
||||
PowerChannelController powerChannelController = null;
|
||||
SpeedChannelController speedChannelController = null;
|
||||
SDMChannelController sdmChannelController = null;
|
||||
BikeChannelController bikeChannelController = null; // Added BikeChannelController reference
|
||||
BikeTransmitterController bikeTransmitterController = null; // Added BikeTransmitterController reference
|
||||
|
||||
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
Log.v(TAG, "onServiceConnected");
|
||||
QLog.v(TAG, "onServiceConnected");
|
||||
// Must pass in the received IBinder object to correctly construct an AntService object
|
||||
mAntRadioService = new AntService(service);
|
||||
|
||||
@@ -72,7 +78,7 @@ public class ChannelService extends Service {
|
||||
// radio by attempting to acquire a channel.
|
||||
boolean legacyInterfaceInUse = mAntChannelProvider.isLegacyInterfaceInUse();
|
||||
|
||||
Log.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
|
||||
QLog.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
|
||||
|
||||
// If there are channels OR legacy interface in use, allow adding channels
|
||||
if (mChannelAvailable || legacyInterfaceInUse) {
|
||||
@@ -85,7 +91,7 @@ public class ChannelService extends Service {
|
||||
try {
|
||||
openAllChannels();
|
||||
} catch (ChannelNotAvailableException exception) {
|
||||
Log.e(TAG, "Channel not available!!");
|
||||
QLog.e(TAG, "Channel not available!!");
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// TODO Auto-generated catch block
|
||||
@@ -117,12 +123,20 @@ public class ChannelService extends Service {
|
||||
if (null != sdmChannelController) {
|
||||
sdmChannelController.speed = speed;
|
||||
}
|
||||
// Update bike transmitter with speed data (only if not treadmill)
|
||||
if (!Ant.treadmill && null != bikeTransmitterController) {
|
||||
bikeTransmitterController.setSpeedKph(speed);
|
||||
}
|
||||
}
|
||||
|
||||
void setPower(int power) {
|
||||
if (null != powerChannelController) {
|
||||
powerChannelController.power = power;
|
||||
}
|
||||
// Update bike transmitter with power data (only if not treadmill)
|
||||
if (!Ant.treadmill && null != bikeTransmitterController) {
|
||||
bikeTransmitterController.setPower(power);
|
||||
}
|
||||
}
|
||||
|
||||
void setCadence(int cadence) {
|
||||
@@ -135,16 +149,164 @@ public class ChannelService extends Service {
|
||||
if (null != sdmChannelController) {
|
||||
sdmChannelController.cadence = cadence;
|
||||
}
|
||||
// Update bike transmitter with cadence data (only if not treadmill)
|
||||
if (!Ant.treadmill && null != bikeTransmitterController) {
|
||||
bikeTransmitterController.setCadence(cadence);
|
||||
}
|
||||
}
|
||||
|
||||
int getHeart() {
|
||||
if (null != heartChannelController) {
|
||||
Log.v(TAG, "getHeart");
|
||||
QLog.v(TAG, "getHeart");
|
||||
return heartChannelController.heart;
|
||||
}
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getHeartRate();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Added getters for bike channel data
|
||||
int getBikeCadence() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getCadence();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int getBikePower() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getPower();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
double getBikeSpeed() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getSpeedKph();
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
long getBikeDistance() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getDistance();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
boolean isBikeConnected() {
|
||||
return (bikeChannelController != null && bikeChannelController.isConnected());
|
||||
}
|
||||
|
||||
// ========== BIKE TRANSMITTER METHODS ==========
|
||||
|
||||
/**
|
||||
* Start the bike transmitter (only available if not treadmill)
|
||||
*/
|
||||
boolean startBikeTransmitter() {
|
||||
QLog.v(TAG, "ChannelServiceComm.startBikeTransmitter");
|
||||
|
||||
if (Ant.treadmill) {
|
||||
QLog.w(TAG, "Bike transmitter not available in treadmill mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.startTransmission();
|
||||
}
|
||||
QLog.w(TAG, "Bike transmitter controller is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the bike transmitter
|
||||
*/
|
||||
void stopBikeTransmitter() {
|
||||
QLog.v(TAG, "ChannelServiceComm.stopBikeTransmitter");
|
||||
|
||||
if (bikeTransmitterController != null) {
|
||||
bikeTransmitterController.stopTransmission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bike transmitter is active (only if not treadmill)
|
||||
*/
|
||||
boolean isBikeTransmitterActive() {
|
||||
if (Ant.treadmill) {
|
||||
return false;
|
||||
}
|
||||
return (bikeTransmitterController != null && bikeTransmitterController.isTransmitting());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bike transmitter with extended metrics (only if not treadmill)
|
||||
*/
|
||||
void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
|
||||
double elapsedTimeSeconds, int resistance,
|
||||
double inclination) {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
bikeTransmitterController.setDistance(distanceMeters);
|
||||
bikeTransmitterController.setHeartRate(heartRate);
|
||||
bikeTransmitterController.setElapsedTime(elapsedTimeSeconds);
|
||||
bikeTransmitterController.setResistance(resistance);
|
||||
bikeTransmitterController.setInclination(inclination);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last requested resistance from ANT+ controller (only if not treadmill)
|
||||
*/
|
||||
int getRequestedResistanceFromAnt() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getRequestedResistance();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last requested power from ANT+ controller (only if not treadmill)
|
||||
*/
|
||||
int getRequestedPowerFromAnt() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getRequestedPower();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last requested inclination from ANT+ controller (only if not treadmill)
|
||||
*/
|
||||
double getRequestedInclinationFromAnt() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getRequestedInclination();
|
||||
}
|
||||
return -100.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any pending control requests (only if not treadmill)
|
||||
*/
|
||||
void clearAntControlRequests() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
bikeTransmitterController.clearControlRequests();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transmission info for debugging (only if not treadmill)
|
||||
*/
|
||||
String getBikeTransmitterInfo() {
|
||||
if (Ant.treadmill) {
|
||||
return "Bike transmitter disabled in treadmill mode";
|
||||
}
|
||||
if (bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getTransmissionInfo();
|
||||
}
|
||||
return "Bike transmitter not initialized";
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all channels currently added.
|
||||
*/
|
||||
@@ -165,6 +327,72 @@ public class ChannelService extends Service {
|
||||
speedChannelController = new SpeedChannelController(acquireChannel());
|
||||
}
|
||||
}
|
||||
|
||||
// Add initialization for BikeChannelController (receiver)
|
||||
if (Ant.bikeRequest && bikeChannelController == null) {
|
||||
bikeChannelController = new BikeChannelController();
|
||||
}
|
||||
|
||||
// Add initialization for BikeTransmitterController (transmitter) - only when NOT treadmill
|
||||
if (!Ant.treadmill && bikeTransmitterController == null) {
|
||||
QLog.v(TAG, "Initializing BikeTransmitterController (not treadmill mode)");
|
||||
try {
|
||||
// Acquire channel like other controllers
|
||||
AntChannel transmitterChannel = acquireChannel();
|
||||
if (transmitterChannel != null) {
|
||||
bikeTransmitterController = new BikeTransmitterController(transmitterChannel);
|
||||
|
||||
// Set up control command listener to handle requests from ANT+ devices
|
||||
bikeTransmitterController.setControlCommandListener(new BikeTransmitterController.ControlCommandListener() {
|
||||
@Override
|
||||
public void onResistanceChangeRequested(int resistance) {
|
||||
QLog.d(TAG, "ChannelService: ANT+ Resistance change requested: " + resistance);
|
||||
// Send broadcast intent to notify the main application
|
||||
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_RESISTANCE_CHANGE");
|
||||
intent.putExtra("resistance", resistance);
|
||||
nativeSetResistance(resistance);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPowerChangeRequested(int power) {
|
||||
QLog.d(TAG, "ChannelService: ANT+ Power change requested: " + power + "W");
|
||||
// Send broadcast intent to notify the main application
|
||||
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_POWER_CHANGE");
|
||||
intent.putExtra("power", power);
|
||||
nativeSetPower(power);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInclinationChangeRequested(double inclination) {
|
||||
QLog.d(TAG, "ChannelService: ANT+ Inclination change requested: " + inclination + "%");
|
||||
// Send broadcast intent to notify the main application
|
||||
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_INCLINATION_CHANGE");
|
||||
intent.putExtra("inclination", inclination);
|
||||
nativeSetInclination(inclination);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
});
|
||||
|
||||
QLog.i(TAG, "BikeTransmitterController initialized successfully (bike mode)");
|
||||
|
||||
// Start the bike transmitter immediately after initialization
|
||||
boolean transmissionStarted = bikeTransmitterController.startTransmission();
|
||||
if (transmissionStarted) {
|
||||
QLog.i(TAG, "BikeTransmitterController transmission started automatically");
|
||||
} else {
|
||||
QLog.w(TAG, "Failed to start BikeTransmitterController transmission");
|
||||
}
|
||||
} else {
|
||||
QLog.e(TAG, "Failed to acquire channel for BikeTransmitterController");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to initialize BikeTransmitterController: " + e.getMessage());
|
||||
bikeTransmitterController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closeAllChannels() {
|
||||
@@ -176,10 +404,18 @@ public class ChannelService extends Service {
|
||||
speedChannelController.close();
|
||||
if (sdmChannelController != null)
|
||||
sdmChannelController.close();
|
||||
if (bikeChannelController != null) // Added closing bikeChannelController
|
||||
bikeChannelController.close();
|
||||
if (bikeTransmitterController != null) { // Added closing bikeTransmitterController
|
||||
bikeTransmitterController.close(); // Use close() method like other controllers
|
||||
}
|
||||
|
||||
heartChannelController = null;
|
||||
powerChannelController = null;
|
||||
speedChannelController = null;
|
||||
sdmChannelController = null;
|
||||
bikeChannelController = null; // Added nullifying bikeChannelController
|
||||
bikeTransmitterController = null; // Added nullifying bikeTransmitterController
|
||||
}
|
||||
|
||||
AntChannel acquireChannel() throws ChannelNotAvailableException {
|
||||
@@ -200,13 +436,13 @@ public class ChannelService extends Service {
|
||||
else {
|
||||
NetworkKey mNK = new NetworkKey(new byte[]{(byte) 0xb9, (byte) 0xa5, (byte) 0x21, (byte) 0xfb,
|
||||
(byte) 0xbd, (byte) 0x72, (byte) 0xc3, (byte) 0x45});
|
||||
Log.v(TAG, mNK.toString());
|
||||
QLog.v(TAG, mNK.toString());
|
||||
mAntChannel = mAntChannelProvider.acquireChannelOnPrivateNetwork(this, mNK);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.v(TAG, "ACP Remote Ex");
|
||||
QLog.v(TAG, "ACP Remote Ex");
|
||||
} catch (UnsupportedFeatureException e) {
|
||||
Log.v(TAG, "ACP UnsupportedFeature Ex");
|
||||
QLog.v(TAG, "ACP UnsupportedFeature Ex");
|
||||
}
|
||||
}
|
||||
return mAntChannel;
|
||||
@@ -223,14 +459,14 @@ public class ChannelService extends Service {
|
||||
private final BroadcastReceiver mChannelProviderStateChangedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "onReceive");
|
||||
QLog.d(TAG, "onReceive");
|
||||
if (AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED.equals(intent.getAction())) {
|
||||
boolean update = false;
|
||||
// Retrieving the data contained in the intent
|
||||
int numChannels = intent.getIntExtra(AntChannelProvider.NUM_CHANNELS_AVAILABLE, 0);
|
||||
boolean legacyInterfaceInUse = intent.getBooleanExtra(AntChannelProvider.LEGACY_INTERFACE_IN_USE, false);
|
||||
|
||||
Log.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
|
||||
QLog.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
|
||||
|
||||
if (mAllowAddChannel) {
|
||||
// Was a acquire channel allowed
|
||||
@@ -249,7 +485,7 @@ public class ChannelService extends Service {
|
||||
try {
|
||||
openAllChannels();
|
||||
} catch (ChannelNotAvailableException exception) {
|
||||
Log.e(TAG, "Channel not available!!");
|
||||
QLog.e(TAG, "Channel not available!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +494,7 @@ public class ChannelService extends Service {
|
||||
};
|
||||
|
||||
private void doBindAntRadioService() {
|
||||
if (BuildConfig.DEBUG) Log.v(TAG, "doBindAntRadioService");
|
||||
if (BuildConfig.DEBUG) QLog.v(TAG, "doBindAntRadioService");
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
@@ -273,14 +509,14 @@ public class ChannelService extends Service {
|
||||
}
|
||||
|
||||
private void doUnbindAntRadioService() {
|
||||
if (BuildConfig.DEBUG) Log.v(TAG, "doUnbindAntRadioService");
|
||||
if (BuildConfig.DEBUG) QLog.v(TAG, "doUnbindAntRadioService");
|
||||
|
||||
// Stop listing for channel available intents
|
||||
try {
|
||||
unregisterReceiver(mChannelProviderStateChangedReceiver);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
|
||||
QLog.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
|
||||
}
|
||||
|
||||
if (mAntRadioServiceBound) {
|
||||
@@ -315,7 +551,7 @@ public class ChannelService extends Service {
|
||||
}
|
||||
|
||||
static void die(String error) {
|
||||
Log.e(TAG, "DIE: " + error);
|
||||
QLog.e(TAG, "DIE: " + error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,20 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class ContentHelper {
|
||||
|
||||
public static String getFileName(Context context, Uri uri) {
|
||||
String result = null;
|
||||
if (uri.getScheme().equals("content")) {
|
||||
Log.d("ContentHelper", "content");
|
||||
QLog.d("ContentHelper", "content");
|
||||
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
Log.d("ContentHelper", "cursor " + cursor);
|
||||
QLog.d("ContentHelper", "cursor " + cursor);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||
Log.d("ContentHelper", "result " + result);
|
||||
QLog.d("ContentHelper", "result " + result);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
|
||||
34
src/android/src/CustomQtActivity.java
Normal file
34
src/android/src/CustomQtActivity.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||
|
||||
public class CustomQtActivity extends QtActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Handle Android 16 API 36 WindowInsetsController for fullscreen support
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11 (API 30) and above - use WindowInsetsController
|
||||
getWindow().setDecorFitsSystemWindows(false);
|
||||
WindowInsetsController controller = getWindow().getDecorView().getWindowInsetsController();
|
||||
if (controller != null) {
|
||||
controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
} else {
|
||||
// Fallback for older Android versions (API < 30)
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -29,32 +29,26 @@ public class FloatingHandler {
|
||||
static public int _width;
|
||||
static public int _height;
|
||||
static public int _alpha;
|
||||
static public String _htmlPage = "floating.htm";
|
||||
|
||||
public static void show(Context context, int port, int width, int height, int transparency) {
|
||||
_context = context;
|
||||
_port = port;
|
||||
_width = width;
|
||||
_height = height;
|
||||
_alpha = transparency;
|
||||
public static void show(Context context, int port, int width, int height, int transparency, String htmlPage) {
|
||||
_context = context;
|
||||
_port = port;
|
||||
_width = width;
|
||||
_height = height;
|
||||
_alpha = transparency;
|
||||
_htmlPage = htmlPage;
|
||||
|
||||
// First it confirms whether the
|
||||
// 'Display over other apps' permission in given
|
||||
if (checkOverlayDisplayPermission()) {
|
||||
if(_intent == null)
|
||||
_intent = new Intent(context, FloatingWindowGFG.class);
|
||||
// FloatingWindowGFG service is started
|
||||
context.startService(_intent);
|
||||
// The MainActivity closes here
|
||||
//finish();
|
||||
} else {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
|
||||
|
||||
// This method will start the intent. It takes two parameter, one is the Intent and the other is
|
||||
// an requestCode Integer. Here it is -1.
|
||||
Activity a = (Activity)_context;
|
||||
a.startActivityForResult(intent, -1);
|
||||
}
|
||||
}
|
||||
if (checkOverlayDisplayPermission()) {
|
||||
if (_intent == null)
|
||||
_intent = new Intent(context, FloatingWindowGFG.class);
|
||||
context.startService(_intent);
|
||||
} else {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
|
||||
Activity a = (Activity) _context;
|
||||
a.startActivityForResult(intent, -1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hide() {
|
||||
if(_intent != null)
|
||||
|
||||
@@ -24,7 +24,7 @@ import android.widget.Toast;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class FloatingWindowGFG extends Service {
|
||||
@@ -82,14 +82,14 @@ public class FloatingWindowGFG extends Service {
|
||||
});
|
||||
WebSettings settings = wv.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/floating.htm");
|
||||
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
wv.clearView();
|
||||
wv.measure(100, 100);
|
||||
wv.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setUseWideViewPort(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
Log.d("QZ","loadurl");
|
||||
QLog.d("QZ","loadurl");
|
||||
|
||||
|
||||
// WindowManager.LayoutParams takes a lot of parameters to set the
|
||||
@@ -153,7 +153,7 @@ public class FloatingWindowGFG extends Service {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
|
||||
Log.d("QZ","onTouch");
|
||||
QLog.d("QZ","onTouch");
|
||||
|
||||
switch (event.getAction()) {
|
||||
// When the window will be touched,
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class ForegroundService extends Service {
|
||||
public static final String CHANNEL_ID = "ForegroundServiceChannel";
|
||||
@@ -43,7 +43,7 @@ public class ForegroundService extends Service {
|
||||
startForeground(1, notification);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("ForegroundService", "Failed to start foreground service", e);
|
||||
QLog.e("ForegroundService", "Failed to start foreground service", e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
//do heavy work on a background thread
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import com.garmin.android.connectiq.ConnectIQ;
|
||||
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
|
||||
import com.garmin.android.connectiq.IQApp;
|
||||
@@ -53,22 +53,22 @@ public class Garmin {
|
||||
private static Integer Power = 0;
|
||||
|
||||
public static int getHR() {
|
||||
Log.d(TAG, "getHR " + HR);
|
||||
QLog.d(TAG, "getHR " + HR);
|
||||
return HR;
|
||||
}
|
||||
|
||||
public static int getPower() {
|
||||
Log.d(TAG, "getPower " + Power);
|
||||
QLog.d(TAG, "getPower " + Power);
|
||||
return Power;
|
||||
}
|
||||
|
||||
public static double getSpeed() {
|
||||
Log.d(TAG, "getSpeed " + Speed);
|
||||
QLog.d(TAG, "getSpeed " + Speed);
|
||||
return Speed;
|
||||
}
|
||||
|
||||
public static int getFootCad() {
|
||||
Log.d(TAG, "getFootCad " + FootCad);
|
||||
QLog.d(TAG, "getFootCad " + FootCad);
|
||||
return FootCad;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public class Garmin {
|
||||
|
||||
@Override
|
||||
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
|
||||
Log.e(TAG, errStatus.toString());
|
||||
QLog.e(TAG, errStatus.toString());
|
||||
connectIqReady = false;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public class Garmin {
|
||||
public void onSdkReady() {
|
||||
connectIqInitializing = false;
|
||||
connectIqReady = true;
|
||||
Log.i(TAG, " onSdkReady");
|
||||
QLog.i(TAG, " onSdkReady");
|
||||
|
||||
registerWatchMessagesReceiver();
|
||||
registerDeviceStatusReceiver();
|
||||
@@ -118,16 +118,16 @@ public class Garmin {
|
||||
try {
|
||||
List<IQDevice> devices = connectIQ.getConnectedDevices();
|
||||
if (devices != null && devices.size() > 0) {
|
||||
Log.v(TAG, "getDevice connected: " + devices.get(0).toString() );
|
||||
QLog.v(TAG, "getDevice connected: " + devices.get(0).toString() );
|
||||
deviceCache = devices.get(0);
|
||||
return deviceCache;
|
||||
} else {
|
||||
return deviceCache;
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
} catch (ServiceUnavailableException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -193,33 +193,33 @@ public class Garmin {
|
||||
|
||||
@Override
|
||||
public void onApplicationInfoReceived(IQApp app) {
|
||||
Log.d(TAG, "App installed.");
|
||||
QLog.d(TAG, "App installed.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationNotInstalled(String applicationId) {
|
||||
if (getDevice() != null) {
|
||||
Toast.makeText(context, "App not installed on your Garmin watch", Toast.LENGTH_LONG).show();
|
||||
Log.d(TAG, "watch app not installed.");
|
||||
QLog.d(TAG, "watch app not installed.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (InvalidStateException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
} catch (ServiceUnavailableException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerDeviceStatusReceiver() {
|
||||
Log.d(TAG, "registerDeviceStatusReceiver");
|
||||
QLog.d(TAG, "registerDeviceStatusReceiver");
|
||||
IQDevice device = getDevice();
|
||||
try {
|
||||
if (device != null) {
|
||||
connectIQ.registerForDeviceEvents(device, new ConnectIQ.IQDeviceEventListener() {
|
||||
@Override
|
||||
public void onDeviceStatusChanged(IQDevice device, IQDevice.IQDeviceStatus newStatus) {
|
||||
Log.d(TAG, "Device status changed, now " + newStatus);
|
||||
QLog.d(TAG, "Device status changed, now " + newStatus);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -229,7 +229,7 @@ public class Garmin {
|
||||
}
|
||||
|
||||
private static void registerWatchMessagesReceiver(){
|
||||
Log.d(TAG, "registerWatchMessageReceiver");
|
||||
QLog.d(TAG, "registerWatchMessageReceiver");
|
||||
IQDevice device = getDevice();
|
||||
try {
|
||||
if (device != null) {
|
||||
@@ -238,7 +238,7 @@ public class Garmin {
|
||||
public void onMessageReceived(IQDevice device, IQApp app, List<Object> message, ConnectIQ.IQMessageStatus status) {
|
||||
if (status == ConnectIQ.IQMessageStatus.SUCCESS) {
|
||||
//MessageHandler.getInstance().handleMessageFromWatchUsingCIQ(message, status, context);
|
||||
Log.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
|
||||
QLog.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
|
||||
try {
|
||||
String var[] = message.toArray()[0].toString().split(",");
|
||||
HR = Integer.parseInt(var[0].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
|
||||
@@ -249,21 +249,21 @@ public class Garmin {
|
||||
Speed = Double.parseDouble(var[1].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "HR " + HR);
|
||||
Log.d(TAG, "FootCad " + FootCad);
|
||||
QLog.d(TAG, "HR " + HR);
|
||||
QLog.d(TAG, "FootCad " + FootCad);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Processing error", e);
|
||||
QLog.e(TAG, "Processing error", e);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "onMessageReceived error, status: " + status.toString());
|
||||
QLog.d(TAG, "onMessageReceived error, status: " + status.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.d(TAG, "registerWatchMessagesReceiver: No device found.");
|
||||
QLog.d(TAG, "registerWatchMessagesReceiver: No device found.");
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,19 +273,19 @@ public class Garmin {
|
||||
|
||||
try {
|
||||
if (context != null) {
|
||||
Log.d(TAG, "Shutting down with wrapped context");
|
||||
QLog.d(TAG, "Shutting down with wrapped context");
|
||||
connectIQ.shutdown(context);
|
||||
} else {
|
||||
Log.d(TAG, "Shutting down without wrapped context");
|
||||
QLog.d(TAG, "Shutting down without wrapped context");
|
||||
connectIQ.shutdown(applicationContext);
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
// This is usually because the SDK was already shut down so no worries.
|
||||
Log.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
|
||||
QLog.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,11 +299,11 @@ public class Garmin {
|
||||
}
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.app.Activity;
|
||||
|
||||
// ANT+ Plugin imports
|
||||
@@ -57,26 +57,26 @@ public class HeartChannelController {
|
||||
case SUCCESS:
|
||||
hrPcc = result;
|
||||
isConnected = true;
|
||||
Log.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
|
||||
subscribeToHrEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
Log.e(TAG, "Channel Not Available");
|
||||
QLog.e(TAG, "Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
Log.e(TAG, "ANT Adapter Not Available");
|
||||
QLog.e(TAG, "ANT Adapter Not Available");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
Log.e(TAG, "Bad request parameters");
|
||||
QLog.e(TAG, "Bad request parameters");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
Log.e(TAG, "RequestAccess failed");
|
||||
QLog.e(TAG, "RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
Log.e(TAG, "Dependency not installed");
|
||||
QLog.e(TAG, "Dependency not installed");
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unrecognized result: " + resultCode);
|
||||
QLog.e(TAG, "Unrecognized result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class HeartChannelController {
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
Log.d(TAG, "Device State Changed to: " + newDeviceState);
|
||||
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isConnected = false;
|
||||
}
|
||||
@@ -104,7 +104,7 @@ public class HeartChannelController {
|
||||
BigDecimal heartBeatEventTime, DataState dataState) {
|
||||
|
||||
heart = computedHeartRate;
|
||||
Log.d(TAG, "Heart Rate: " + heart);
|
||||
QLog.d(TAG, "Heart Rate: " + heart);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -117,7 +117,7 @@ public class HeartChannelController {
|
||||
}
|
||||
hrPcc = null;
|
||||
isConnected = false;
|
||||
Log.d(TAG, "Channel Closed");
|
||||
QLog.d(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
public int getHeartRate() {
|
||||
|
||||
@@ -16,7 +16,7 @@ import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.os.Build;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
@@ -169,7 +169,7 @@ public class HidBridge {
|
||||
} catch(NullPointerException e)
|
||||
{
|
||||
Log("Error happened while writing. Could not connect to the device or interface is busy?");
|
||||
Log.e("HidBridge", Log.getStackTraceString(e));
|
||||
QLog.e("HidBridge", QLog.getStackTraceString(e));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -289,7 +289,7 @@ public class HidBridge {
|
||||
|
||||
catch (NullPointerException e) {
|
||||
Log("Error happened while reading. No device or the connection is busy");
|
||||
Log.e("HidBridge", Log.getStackTraceString(e));
|
||||
QLog.e("HidBridge", QLog.getStackTraceString(e));
|
||||
}
|
||||
catch (ThreadDeath e) {
|
||||
if (readConnection != null) {
|
||||
@@ -332,7 +332,7 @@ public class HidBridge {
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.d("TAG", "permission denied for the device " + device);
|
||||
QLog.d("TAG", "permission denied for the device " + device);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,7 +344,7 @@ public class HidBridge {
|
||||
* @param message to log.
|
||||
*/
|
||||
private void Log(String message) {
|
||||
Log.d("HidBridge", message);
|
||||
QLog.d("HidBridge", message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.garmin.android.connectiq.IQDevice;
|
||||
|
||||
import java.nio.BufferUnderflowException;
|
||||
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
private final BroadcastReceiver receiver;
|
||||
@@ -20,7 +20,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "onReceive intent " + intent.getAction());
|
||||
QLog.d(TAG, "onReceive intent " + intent.getAction());
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
@@ -32,7 +32,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
try {
|
||||
receiver.onReceive(context, intent);
|
||||
} catch (IllegalArgumentException | BufferUnderflowException e) {
|
||||
Log.d(TAG, e.toString());
|
||||
QLog.d(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
intent.putExtra(extraName, device.getDeviceIdentifier());
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
Log.d(TAG, e.toString());
|
||||
QLog.d(TAG, e.toString());
|
||||
// It's already a long, i.e. on the simulator.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.LocationManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class LocationHelper {
|
||||
private static final String TAG = "LocationHelper";
|
||||
@@ -13,7 +13,7 @@ public class LocationHelper {
|
||||
private static boolean isBluetoothEnabled = false;
|
||||
|
||||
public static boolean start(Context context) {
|
||||
Log.d(TAG, "Starting LocationHelper check...");
|
||||
QLog.d(TAG, "Starting LocationHelper check...");
|
||||
|
||||
isLocationEnabled = isLocationEnabled(context);
|
||||
isBluetoothEnabled = isBluetoothEnabled();
|
||||
@@ -23,7 +23,7 @@ public class LocationHelper {
|
||||
|
||||
public static void requestPermissions(Context context) {
|
||||
if (!isLocationEnabled || !isBluetoothEnabled) {
|
||||
Log.d(TAG, "Some services are disabled. Prompting user...");
|
||||
QLog.d(TAG, "Some services are disabled. Prompting user...");
|
||||
if (!isLocationEnabled) {
|
||||
promptEnableLocation(context);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class LocationHelper {
|
||||
promptEnableBluetooth(context);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "All services are already enabled.");
|
||||
QLog.d(TAG, "All services are already enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,14 +50,14 @@ public class LocationHelper {
|
||||
}
|
||||
|
||||
private static void promptEnableLocation(Context context) {
|
||||
Log.d(TAG, "Prompting to enable Location...");
|
||||
QLog.d(TAG, "Prompting to enable Location...");
|
||||
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private static void promptEnableBluetooth(Context context) {
|
||||
Log.d(TAG, "Prompting to enable Bluetooth...");
|
||||
QLog.d(TAG, "Prompting to enable Bluetooth...");
|
||||
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.os.Build;
|
||||
|
||||
public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
@@ -13,7 +13,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d("MediaButtonReceiver", "Received intent: " + intent.toString());
|
||||
QLog.d("MediaButtonReceiver", "Received intent: " + intent.toString());
|
||||
String intentAction = intent.getAction();
|
||||
if ("android.media.VOLUME_CHANGED_ACTION".equals(intentAction)) {
|
||||
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
@@ -21,7 +21,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
int currentVolume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1);
|
||||
int previousVolume = intent.getIntExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", -1);
|
||||
|
||||
Log.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
|
||||
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
|
||||
nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
|
||||
|
||||
if (context == null) {
|
||||
Log.e("MediaButtonReceiver", "Context is null, cannot register receiver");
|
||||
QLog.e("MediaButtonReceiver", "Context is null, cannot register receiver");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,21 +44,21 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
try {
|
||||
context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED);
|
||||
} catch (SecurityException se) {
|
||||
Log.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
context.registerReceiver(instance, filter);
|
||||
} catch (SecurityException se) {
|
||||
Log.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
}
|
||||
}
|
||||
Log.d("MediaButtonReceiver", "Receiver registered successfully");
|
||||
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
|
||||
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
Log.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
|
||||
QLog.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.util.DisplayMetrics;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.app.AppOpsManager;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.annotation.TargetApi;
|
||||
|
||||
import com.rvalerio.fgchecker.AppChecker;
|
||||
@@ -83,11 +83,11 @@ public class MediaProjection {
|
||||
@Override
|
||||
public void onForeground(String packageName) {
|
||||
_packageName = packageName;
|
||||
/*Log.e("MediaProjection", packageName);
|
||||
/*QLog.e("MediaProjection", packageName);
|
||||
if(isLandscape())
|
||||
Log.e("MediaProjection", "Landscape");
|
||||
QLog.e("MediaProjection", "Landscape");
|
||||
else
|
||||
Log.e("MediaProjection", "Portrait");*/
|
||||
QLog.e("MediaProjection", "Portrait");*/
|
||||
}
|
||||
})
|
||||
.timeout(1000)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
|
||||
|
||||
@@ -12,6 +12,6 @@ public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
this.getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
activity_ = this;
|
||||
Log.v(TAG, "onCreate");
|
||||
QLog.v(TAG, "onCreate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -12,7 +12,7 @@ public class NativeScanCallback extends ScanCallback {
|
||||
public native void scanError(int code);
|
||||
@Override
|
||||
public void onScanResult(int callbackType, ScanResult result) {
|
||||
Log.i(TAG, "Res " + result);
|
||||
QLog.i(TAG, "Res " + result);
|
||||
newScanResult(new ScanRecordResult(result));
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class NativeScanCallback extends ScanCallback {
|
||||
|
||||
@Override
|
||||
public void onScanFailed(int errorCode) {
|
||||
Log.i(TAG, "onScanFailed "+errorCode);
|
||||
QLog.i(TAG, "onScanFailed "+errorCode);
|
||||
scanError(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
@@ -61,7 +61,7 @@ public class PowerChannelController {
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
Log.w(TAG, "Channel was already open");
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type. In
|
||||
// order for master (TX) channels and slave (RX) channels to connect, they
|
||||
@@ -92,7 +92,7 @@ public class PowerChannelController {
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
Log.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
|
||||
QLog.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
|
||||
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
@@ -102,7 +102,7 @@ public class PowerChannelController {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "No channel available");
|
||||
QLog.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
@@ -112,7 +112,7 @@ public class PowerChannelController {
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
|
||||
Log.e(TAG, logString);
|
||||
QLog.e(TAG, logString);
|
||||
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class PowerChannelController {
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
Log.e(TAG, logString.toString());
|
||||
QLog.e(TAG, logString.toString());
|
||||
|
||||
mAntChannel.release();
|
||||
}
|
||||
@@ -158,7 +158,7 @@ public class PowerChannelController {
|
||||
mAntChannel = null;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Channel Closed");
|
||||
QLog.e(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,13 +175,13 @@ public class PowerChannelController {
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
Log.e(TAG, "Channel Death");
|
||||
QLog.e(TAG, "Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
Log.d(TAG, "Rx: " + antParcel);
|
||||
Log.d(TAG, "Message Type: " + messageType);
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
if(carousalTimer == null) {
|
||||
@@ -189,7 +189,7 @@ public class PowerChannelController {
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(TAG, "Tx Unsollicited");
|
||||
QLog.d(TAG, "Tx Unsollicited");
|
||||
byte[] payload = new byte[8];
|
||||
eventCount = (eventCount + 1) & 0xFF;
|
||||
cumulativePower = (cumulativePower + power) & 0xFFFF;
|
||||
@@ -225,7 +225,7 @@ public class PowerChannelController {
|
||||
// Rx Data
|
||||
//updateData(new AcknowledgedDataMessage(antParcel).getPayload());
|
||||
payload = new AcknowledgedDataMessage(antParcel).getPayload();
|
||||
Log.d(TAG, "AcknowledgedDataMessage: " + payload);
|
||||
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
|
||||
|
||||
if ((payload[0] == 0) && (payload[1] == 1) && (payload[2] == (byte)0xAA)) {
|
||||
payload[0] = (byte) 0x01;
|
||||
@@ -268,7 +268,7 @@ public class PowerChannelController {
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
Log.d(TAG, "Event Code: " + code);
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
@@ -320,7 +320,7 @@ public class PowerChannelController {
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
// TODO May want to keep searching
|
||||
Log.e(TAG, "No Device Found");
|
||||
QLog.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
|
||||
121
src/android/src/QLog.java
Normal file
121
src/android/src/QLog.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* QLog - Wrapper for Android's Log class that redirects logs to Qt's logging system
|
||||
* Usage: import org.cagnulen.qdomyoszwift.Log;
|
||||
*/
|
||||
public class QLog {
|
||||
public static native void sendToQt(int level, String tag, String message);
|
||||
|
||||
static {
|
||||
try {
|
||||
// Try to load the native library if needed
|
||||
System.loadLibrary("qtlogging_native");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Library might be loaded elsewhere, or will be loaded later
|
||||
Log.w("QLog", "Native library not loaded yet: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Debug level methods
|
||||
public static int d(String tag, String msg) {
|
||||
sendToQt(3, tag, msg);
|
||||
return Log.d(tag, msg);
|
||||
}
|
||||
|
||||
public static int d(String tag, String msg, Throwable tr) {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
return Log.d(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Error level methods
|
||||
public static int e(String tag, String msg) {
|
||||
sendToQt(6, tag, msg);
|
||||
return Log.e(tag, msg);
|
||||
}
|
||||
|
||||
public static int e(String tag, String msg, Throwable tr) {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
return Log.e(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Info level methods
|
||||
public static int i(String tag, String msg) {
|
||||
sendToQt(4, tag, msg);
|
||||
return Log.i(tag, msg);
|
||||
}
|
||||
|
||||
public static int i(String tag, String msg, Throwable tr) {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
return Log.i(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Verbose level methods
|
||||
public static int v(String tag, String msg) {
|
||||
sendToQt(2, tag, msg);
|
||||
return Log.v(tag, msg);
|
||||
}
|
||||
|
||||
public static int v(String tag, String msg, Throwable tr) {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
return Log.v(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Warning level methods
|
||||
public static int w(String tag, String msg) {
|
||||
sendToQt(5, tag, msg);
|
||||
return Log.w(tag, msg);
|
||||
}
|
||||
|
||||
public static int w(String tag, String msg, Throwable tr) {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
return Log.w(tag, msg, tr);
|
||||
}
|
||||
|
||||
public static int w(String tag, Throwable tr) {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
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);
|
||||
return Log.wtf(tag, msg);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
return Log.wtf(tag, tr);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, String msg, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
return Log.wtf(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
public static String getStackTraceString(Throwable tr) {
|
||||
return Log.getStackTraceString(tr);
|
||||
}
|
||||
|
||||
public static boolean isLoggable(String tag, int level) {
|
||||
return Log.isLoggable(tag, level);
|
||||
}
|
||||
|
||||
// Additional utility methods
|
||||
public static int println(int priority, String tag, String msg) {
|
||||
sendToQt(priority, tag, msg);
|
||||
return Log.println(priority, tag, msg);
|
||||
}
|
||||
|
||||
// API Level 28+ (Android 9+) methods
|
||||
public static RuntimeException getStackTraceElement() {
|
||||
try {
|
||||
return (RuntimeException) Log.class.getMethod("getStackTraceElement").invoke(null);
|
||||
} catch (Exception e) {
|
||||
return new RuntimeException("QLog: Failed to get stack trace element");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.RadioButton;
|
||||
@@ -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;
|
||||
Log.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;
|
||||
Log.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;
|
||||
Log.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;
|
||||
Log.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
|
||||
@@ -96,7 +113,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
|
||||
@Override
|
||||
public void receivedData(DeviceConnection devConn, byte[] data, int offset, int length) {
|
||||
Log.i(LOG_TAG, data.toString());
|
||||
QLog.i(LOG_TAG, data.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,96 +128,132 @@ 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 */
|
||||
Log.i(LOG_TAG,
|
||||
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)
|
||||
{
|
||||
Log.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) {
|
||||
Log.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
|
||||
QLog.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
|
||||
if(ADBConnected) {
|
||||
StringBuilder commandBuffer = new StringBuilder();
|
||||
|
||||
@@ -212,7 +265,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
/* Send it to the device */
|
||||
connection.queueCommand(commandBuffer.toString());
|
||||
} else {
|
||||
Log.e(LOG_TAG, "sendCommand ADB is not connected!");
|
||||
QLog.e(LOG_TAG, "sendCommand ADB is not connected!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
@@ -68,7 +68,7 @@ public class SDMChannelController {
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
Log.w(TAG, "Channel was already open");
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type. In
|
||||
// order for master (TX) channels and slave (RX) channels to connect, they
|
||||
@@ -99,7 +99,7 @@ public class SDMChannelController {
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
} catch (AntCommandFailedException e) {
|
||||
@@ -108,7 +108,7 @@ public class SDMChannelController {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "No channel available");
|
||||
QLog.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
@@ -117,7 +117,7 @@ public class SDMChannelController {
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
|
||||
Log.e(TAG, logString);
|
||||
QLog.e(TAG, logString);
|
||||
}
|
||||
|
||||
void channelError(String error, AntCommandFailedException e) {
|
||||
@@ -146,11 +146,11 @@ public class SDMChannelController {
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
Log.e(TAG, logString.toString());
|
||||
QLog.e(TAG, logString.toString());
|
||||
|
||||
mAntChannel.release();
|
||||
|
||||
Log.e(TAG, "ANT Command Failed");
|
||||
QLog.e(TAG, "ANT Command Failed");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@@ -164,7 +164,7 @@ public class SDMChannelController {
|
||||
mAntChannel = null;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Channel Closed");
|
||||
QLog.e(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,20 +186,20 @@ public class SDMChannelController {
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
Log.e(TAG, "Channel Death");
|
||||
QLog.e(TAG, "Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
Log.d(TAG, "Rx: " + antParcel);
|
||||
Log.d(TAG, "Message Type: " + messageType);
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
|
||||
if(carousalTimer == null) {
|
||||
carousalTimer = new Timer(); // At this line a new Thread will be created
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(TAG, "Tx Unsollicited");
|
||||
QLog.d(TAG, "Tx Unsollicited");
|
||||
long realtimeMillis = SystemClock.elapsedRealtime();
|
||||
double speedM_s = speed / 3.6;
|
||||
long deltaTime = (realtimeMillis - lastTime);
|
||||
@@ -243,7 +243,7 @@ public class SDMChannelController {
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
Log.d(TAG, "Event Code: " + code);
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
@@ -278,7 +278,7 @@ public class SDMChannelController {
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
// TODO May want to keep searching
|
||||
Log.e(TAG, "No Device Found");
|
||||
QLog.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
|
||||
@@ -18,7 +18,7 @@ import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.view.Display;
|
||||
import android.view.OrientationEventListener;
|
||||
import android.view.WindowManager;
|
||||
@@ -43,7 +43,7 @@ import android.graphics.Rect;
|
||||
import android.graphics.Point;
|
||||
|
||||
import androidx.core.util.Pair;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.os.Build;
|
||||
|
||||
public class ScreenCaptureService extends Service {
|
||||
@@ -137,7 +137,7 @@ public class ScreenCaptureService extends Service {
|
||||
int pixelStride = planes[0].getPixelStride();
|
||||
int rowStride = planes[0].getRowStride();
|
||||
int rowPadding = rowStride - pixelStride * mWidth;
|
||||
//Log.e(TAG, "Image reviewing");
|
||||
//QLog.e(TAG, "Image reviewing");
|
||||
|
||||
isRunning = true;
|
||||
|
||||
@@ -152,7 +152,7 @@ public class ScreenCaptureService extends Service {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
|
||||
|
||||
IMAGES_PRODUCED++;
|
||||
Log.e(TAG, "captured image: " + IMAGES_PRODUCED);
|
||||
QLog.e(TAG, "captured image: " + IMAGES_PRODUCED);
|
||||
*/
|
||||
|
||||
InputImage inputImage = InputImage.fromBitmap(bitmap, 0);
|
||||
@@ -169,7 +169,7 @@ public class ScreenCaptureService extends Service {
|
||||
public void onSuccess(Text result) {
|
||||
// Task completed successfully
|
||||
|
||||
//Log.e(TAG, "Image done!");
|
||||
//QLog.e(TAG, "Image done!");
|
||||
|
||||
String resultText = result.getText();
|
||||
lastText = resultText;
|
||||
@@ -204,12 +204,12 @@ public class ScreenCaptureService extends Service {
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
// Task failed with an exception
|
||||
//Log.e(TAG, "Image fail");
|
||||
//QLog.e(TAG, "Image fail");
|
||||
isRunning = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//Log.e(TAG, "Image ignored");
|
||||
//QLog.e(TAG, "Image ignored");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -246,7 +246,7 @@ public class ScreenCaptureService extends Service {
|
||||
private class MediaProjectionStopCallback extends MediaProjection.Callback {
|
||||
@Override
|
||||
public void onStop() {
|
||||
Log.e(TAG, "stopping projection.");
|
||||
QLog.e(TAG, "stopping projection.");
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -276,12 +276,12 @@ public class ScreenCaptureService extends Service {
|
||||
if (!storeDirectory.exists()) {
|
||||
boolean success = storeDirectory.mkdirs();
|
||||
if (!success) {
|
||||
Log.e(TAG, "failed to create file storage directory.");
|
||||
QLog.e(TAG, "failed to create file storage directory.");
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
|
||||
QLog.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ public class ScreenCaptureService extends Service {
|
||||
startForeground(notification.first, notification.second);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("ForegroundService", "Failed to start foreground service", e);
|
||||
QLog.e("ForegroundService", "Failed to start foreground service", e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
// start projection
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.content.IntentFilter;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.app.Service;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
@@ -35,7 +35,7 @@ public class Shortcuts {
|
||||
|
||||
List<ShortcutInfo> shortcuts = new ArrayList<>();
|
||||
|
||||
Log.d("Shortcuts", folder);
|
||||
QLog.d("Shortcuts", folder);
|
||||
File[] files = new File(folder, "profiles").listFiles();
|
||||
if (files != null) {
|
||||
for (int i = 0; i < files.length && i < 5; i++) { // Limit to 5 shortcuts
|
||||
@@ -45,7 +45,7 @@ public class Shortcuts {
|
||||
if (dotIndex > 0) { // Check if there is a dot, indicating an extension exists
|
||||
fileNameWithoutExtension = fileNameWithoutExtension.substring(0, dotIndex);
|
||||
}
|
||||
Log.d("Shortcuts", file.getAbsolutePath());
|
||||
QLog.d("Shortcuts", file.getAbsolutePath());
|
||||
Intent intent = new Intent(context, context.getClass());
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra("profile_path", file.getAbsolutePath());
|
||||
@@ -74,7 +74,7 @@ public class Shortcuts {
|
||||
for (String key : extras.keySet()) {
|
||||
Object value = extras.get(key);
|
||||
if("profile_path".equals(key)) {
|
||||
Log.d("Shortcuts", "profile_path: " + value.toString());
|
||||
QLog.d("Shortcuts", "profile_path: " + value.toString());
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public class Shortcuts {
|
||||
if (extras != null) {
|
||||
for (String key : extras.keySet()) {
|
||||
Object value = extras.get(key);
|
||||
Log.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
|
||||
QLog.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
@@ -67,7 +67,7 @@ public class SpeedChannelController {
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
Log.w(TAG, "Channel was already open");
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type. In
|
||||
// order for master (TX) channels and slave (RX) channels to connect, they
|
||||
@@ -98,7 +98,7 @@ public class SpeedChannelController {
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
} catch (AntCommandFailedException e) {
|
||||
@@ -107,7 +107,7 @@ public class SpeedChannelController {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "No channel available");
|
||||
QLog.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
@@ -116,7 +116,7 @@ public class SpeedChannelController {
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
|
||||
Log.e(TAG, logString);
|
||||
QLog.e(TAG, logString);
|
||||
}
|
||||
|
||||
void channelError(String error, AntCommandFailedException e) {
|
||||
@@ -145,11 +145,11 @@ public class SpeedChannelController {
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
Log.e(TAG, logString.toString());
|
||||
QLog.e(TAG, logString.toString());
|
||||
|
||||
mAntChannel.release();
|
||||
|
||||
Log.e(TAG, "ANT Command Failed");
|
||||
QLog.e(TAG, "ANT Command Failed");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@@ -163,7 +163,7 @@ public class SpeedChannelController {
|
||||
mAntChannel = null;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Channel Closed");
|
||||
QLog.e(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,20 +185,20 @@ public class SpeedChannelController {
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
Log.e(TAG, "Channel Death");
|
||||
QLog.e(TAG, "Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
Log.d(TAG, "Rx: " + antParcel);
|
||||
Log.d(TAG, "Message Type: " + messageType);
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
|
||||
if(carousalTimer == null) {
|
||||
carousalTimer = new Timer(); // At this line a new Thread will be created
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(TAG, "Tx Unsollicited");
|
||||
QLog.d(TAG, "Tx Unsollicited");
|
||||
long realtimeMillis = SystemClock.elapsedRealtime();
|
||||
|
||||
if (lastTime != 0) {
|
||||
@@ -252,7 +252,7 @@ public class SpeedChannelController {
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
Log.d(TAG, "Event Code: " + code);
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
@@ -296,7 +296,7 @@ public class SpeedChannelController {
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
// TODO May want to keep searching
|
||||
Log.e(TAG, "No Device Found");
|
||||
QLog.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.content.IntentFilter;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.app.Service;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
@@ -43,12 +43,12 @@ public class Usbserial {
|
||||
static int lastReadLen = 0;
|
||||
|
||||
public static void open(Context context) {
|
||||
Log.d("QZ","UsbSerial open");
|
||||
QLog.d("QZ","UsbSerial open");
|
||||
// Find all available drivers from attached devices.
|
||||
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
||||
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
|
||||
if (availableDrivers.isEmpty()) {
|
||||
Log.d("QZ","UsbSerial no available drivers");
|
||||
QLog.d("QZ","UsbSerial no available drivers");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class Usbserial {
|
||||
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
RingtoneManager.getRingtone(context, notification).play();
|
||||
|
||||
Log.d("QZ","USB permission ...");
|
||||
QLog.d("QZ","USB permission ...");
|
||||
final Boolean[] granted = {null};
|
||||
BroadcastReceiver usbReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@@ -85,12 +85,12 @@ public class Usbserial {
|
||||
// Do something here
|
||||
}
|
||||
}
|
||||
Log.d("QZ","USB permission "+granted[0]);
|
||||
QLog.d("QZ","USB permission "+granted[0]);
|
||||
}
|
||||
|
||||
UsbDeviceConnection connection = manager.openDevice(driver.getDevice());
|
||||
if (connection == null) {
|
||||
Log.d("QZ","UsbSerial no permissions");
|
||||
QLog.d("QZ","UsbSerial no permissions");
|
||||
// add UsbManager.requestPermission(driver.getDevice(), ..) handling here
|
||||
return;
|
||||
}
|
||||
@@ -104,14 +104,14 @@ public class Usbserial {
|
||||
// Do something here
|
||||
}
|
||||
|
||||
Log.d("QZ","UsbSerial port opened");
|
||||
QLog.d("QZ","UsbSerial port opened");
|
||||
}
|
||||
|
||||
public static void write (byte[] bytes) {
|
||||
if(port == null)
|
||||
return;
|
||||
|
||||
Log.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
|
||||
QLog.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
|
||||
try {
|
||||
port.write(bytes, 2000);
|
||||
}
|
||||
@@ -132,7 +132,7 @@ public class Usbserial {
|
||||
|
||||
try {
|
||||
lastReadLen = port.read(receiveData, 2000);
|
||||
Log.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
|
||||
QLog.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
|
||||
}
|
||||
catch (IOException e) {
|
||||
// Do something here
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -33,7 +33,7 @@ public class WearableController {
|
||||
_intent = new Intent(context, WearableMessageListenerService.class);
|
||||
// FloatingWindowGFG service is started
|
||||
context.startService(_intent);
|
||||
Log.v("WearableController", "started");
|
||||
QLog.v("WearableController", "started");
|
||||
}
|
||||
|
||||
public static int getHeart() {
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.google.android.gms.wearable.Wearable;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.wearable.DataItemBuffer;
|
||||
import com.google.android.gms.wearable.DataMap;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.gms.common.api.Status;
|
||||
import java.io.InputStream;
|
||||
@@ -31,7 +31,7 @@ public class WearableMessageListenerService extends Service implements
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.v("WearableMessageListenerService","onCreate");
|
||||
QLog.v("WearableMessageListenerService","onCreate");
|
||||
}
|
||||
|
||||
public static int getHeart() {
|
||||
@@ -55,7 +55,7 @@ public class WearableMessageListenerService extends Service implements
|
||||
mWearableClient.addListener(this);
|
||||
Wearable.getDataClient(this).addListener(this);
|
||||
|
||||
Log.v("WearableMessageListenerService","onStartCommand");
|
||||
QLog.v("WearableMessageListenerService","onStartCommand");
|
||||
|
||||
// Return START_STICKY to restart the service if it's killed by the system
|
||||
return START_STICKY;
|
||||
@@ -65,9 +65,9 @@ public class WearableMessageListenerService extends Service implements
|
||||
public void onDataChanged(DataEventBuffer dataEvents) {
|
||||
for (DataEvent event : dataEvents) {
|
||||
if (event.getType() == DataEvent.TYPE_DELETED) {
|
||||
Log.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
|
||||
QLog.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
|
||||
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
|
||||
Log.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
|
||||
QLog.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
|
||||
if(event.getDataItem().getUri().getPath().equals("/qz")) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
@@ -78,14 +78,14 @@ public class WearableMessageListenerService extends Service implements
|
||||
heart_rate = DataMap.fromByteArray(result.get(0).getData())
|
||||
.getInt("heart_rate", 0);
|
||||
} else {
|
||||
Log.e(TAG, "Unexpected number of DataItems found.\n"
|
||||
QLog.e(TAG, "Unexpected number of DataItems found.\n"
|
||||
+ "\tExpected: 1\n"
|
||||
+ "\tActual: " + result.getCount());
|
||||
}
|
||||
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onHandleIntent: failed to get current alarm state");
|
||||
} else {
|
||||
QLog.d(TAG, "onHandleIntent: failed to get current alarm state");
|
||||
}
|
||||
Log.d(TAG, "Heart: " + heart_rate);
|
||||
QLog.d(TAG, "Heart: " + heart_rate);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@@ -96,17 +96,17 @@ public class WearableMessageListenerService extends Service implements
|
||||
|
||||
@Override
|
||||
public void onConnected(Bundle bundle) {
|
||||
Log.v("WearableMessageListenerService","onConnected");
|
||||
QLog.v("WearableMessageListenerService","onConnected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionSuspended(int i) {
|
||||
Log.v("WearableMessageListenerService","onConnectionSuspended");
|
||||
QLog.v("WearableMessageListenerService","onConnectionSuspended");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailed(ConnectionResult connectionResult) {
|
||||
Log.v("WearableMessageListenerService","onConnectionFailed");
|
||||
QLog.v("WearableMessageListenerService","onConnectionFailed");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,8 +117,8 @@ public class WearableMessageListenerService extends Service implements
|
||||
// Handle the received message data here
|
||||
String messageData = new String(data); // Assuming it's a simple string message
|
||||
|
||||
Log.v("Wearable", path);
|
||||
Log.v("Wearable", messageData);
|
||||
QLog.v("Wearable", path);
|
||||
QLog.v("Wearable", messageData);
|
||||
|
||||
// You can then perform actions or update data in your service based on the received message
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IntentFilter;
|
||||
@@ -44,12 +44,12 @@ public class ZapClickLayer {
|
||||
}
|
||||
|
||||
public static int processCharacteristic(byte[] value) {
|
||||
Log.d(TAG, "processCharacteristic");
|
||||
QLog.d(TAG, "processCharacteristic");
|
||||
return device.processCharacteristic("QZ", value);
|
||||
}
|
||||
|
||||
public static byte[] buildHandshakeStart() {
|
||||
Log.d(TAG, "buildHandshakeStart");
|
||||
QLog.d(TAG, "buildHandshakeStart");
|
||||
return device.buildHandshakeStart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import com.garmin.android.connectiq.ConnectIQ;
|
||||
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
|
||||
import com.garmin.android.connectiq.IQApp;
|
||||
@@ -49,17 +49,17 @@ public class ZwiftAPI {
|
||||
// Ora puoi usare 'message' come un oggetto normale
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
// Gestisci l'eccezione se il messaggio non può essere parsato
|
||||
Log.e(TAG, e.toString());
|
||||
QLog.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static float getAltitude() {
|
||||
Log.d(TAG, "getAltitude " + playerState.getAltitude());
|
||||
QLog.d(TAG, "getAltitude " + playerState.getAltitude());
|
||||
return playerState.getAltitude();
|
||||
}
|
||||
|
||||
public static float getDistance() {
|
||||
Log.d(TAG, "getDistance " + playerState.getDistance());
|
||||
QLog.d(TAG, "getDistance " + playerState.getDistance());
|
||||
return playerState.getDistance();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import com.garmin.android.connectiq.ConnectIQ;
|
||||
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
|
||||
import com.garmin.android.connectiq.IQApp;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class ShellService extends Service implements DeviceConnectionListener {
|
||||
|
||||
@@ -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) {
|
||||
Log.e("ForegroundService", "Failed to start foreground service", 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
|
||||
|
||||
@@ -55,7 +55,7 @@ import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||
@@ -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;
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
@@ -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
|
||||
@@ -119,25 +122,28 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
}
|
||||
|
||||
public void initializeConnection(){
|
||||
Log.w(TAG, "initializeConnection start");
|
||||
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() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResult) {
|
||||
Log.w(TAG, "onBillingSetupFinished");
|
||||
QLog.w(TAG, "onBillingSetupFinished");
|
||||
if (billingResult.getResponseCode() == RESULT_OK) {
|
||||
purchasedProductsQueried(m_nativePointer);
|
||||
} else {
|
||||
Log.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
|
||||
QLog.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
Log.w(TAG, "Billing service disconnected");
|
||||
QLog.w(TAG, "Billing service disconnected");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -191,7 +197,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(BillingResult billingResult)
|
||||
{
|
||||
Log.d(TAG, "Purchase acknowledged ");
|
||||
QLog.d(TAG, "Purchase acknowledged ");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -199,9 +205,9 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
}
|
||||
|
||||
public void queryDetails(final String[] productIds) {
|
||||
Log.d(TAG, "queryDetails: start");
|
||||
QLog.d(TAG, "queryDetails: start");
|
||||
int index = 0;
|
||||
Log.d(TAG, "queryDetails: productIds.length " + productIds.length);
|
||||
QLog.d(TAG, "queryDetails: productIds.length " + productIds.length);
|
||||
while (index < productIds.length) {
|
||||
List<String> productIdList = new ArrayList<>();
|
||||
for (int i = index; i < Math.min(index + 20, productIds.length); ++i) {
|
||||
@@ -209,31 +215,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();
|
||||
Log.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
|
||||
QLog.d(TAG, "onProductDetailsResponse: responseCode " + responseCode);
|
||||
|
||||
if (responseCode != RESULT_OK) {
|
||||
Log.e(TAG, "queryDetails: Couldn't retrieve sku details.");
|
||||
QLog.e(TAG, "queryDetails: Couldn't retrieve product details.");
|
||||
return;
|
||||
}
|
||||
if (skuDetailsList == null) {
|
||||
Log.e(TAG, "queryDetails: No details list in response.");
|
||||
if (productDetailsList == null || productDetailsList.isEmpty()) {
|
||||
QLog.e(TAG, "queryDetails: No details list in response.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.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 +262,6 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -255,33 +273,41 @@ 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) {
|
||||
Log.e(TAG, "Unable to launch Google Play purchase screen");
|
||||
QLog.e(TAG, "Unable to launch Google Play purchase screen");
|
||||
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 productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails)
|
||||
.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,7 +317,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
@Override
|
||||
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||
if (billingResult.getResponseCode() != RESULT_OK) {
|
||||
Log.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
|
||||
QLog.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -312,7 +338,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
|
||||
if (billingResult.getResponseCode() != RESULT_OK){
|
||||
Log.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
|
||||
QLog.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -321,18 +347,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package org.qtproject.qt.android.purchasing;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -58,7 +58,7 @@ public class Security {
|
||||
*/
|
||||
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
|
||||
if (signedData == null) {
|
||||
Log.e(TAG, "data is null");
|
||||
QLog.e(TAG, "data is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public class Security {
|
||||
PublicKey key = Security.generatePublicKey(base64PublicKey);
|
||||
verified = Security.verify(key, signedData, signature);
|
||||
if (!verified) {
|
||||
Log.w(TAG, "signature does not match data.");
|
||||
QLog.w(TAG, "signature does not match data.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -89,10 +89,10 @@ public class Security {
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
Log.e(TAG, "Invalid key specification.");
|
||||
QLog.e(TAG, "Invalid key specification.");
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (Base64DecoderException e) {
|
||||
Log.e(TAG, "Base64 decoding failed.");
|
||||
QLog.e(TAG, "Base64 decoding failed.");
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
@@ -113,18 +113,18 @@ public class Security {
|
||||
sig.initVerify(publicKey);
|
||||
sig.update(signedData.getBytes());
|
||||
if (!sig.verify(Base64.decode(signature))) {
|
||||
Log.e(TAG, "Signature verification failed.");
|
||||
QLog.e(TAG, "Signature verification failed.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.e(TAG, "NoSuchAlgorithmException.");
|
||||
QLog.e(TAG, "NoSuchAlgorithmException.");
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.e(TAG, "Invalid key specification.");
|
||||
QLog.e(TAG, "Invalid key specification.");
|
||||
} catch (SignatureException e) {
|
||||
Log.e(TAG, "Signature exception.");
|
||||
QLog.e(TAG, "Signature exception.");
|
||||
} catch (Base64DecoderException e) {
|
||||
Log.e(TAG, "Base64 decoding failed.");
|
||||
QLog.e(TAG, "Base64 decoding failed.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
42
src/androidqlog.cpp
Normal file
42
src/androidqlog.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include <QDebug>
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QAndroidJniObject>
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_cagnulen_qdomyoszwift_QLog_sendToQt(JNIEnv *env, jclass clazz,
|
||||
jint level, jstring tag, jstring message) {
|
||||
const char *tagChars = env->GetStringUTFChars(tag, nullptr);
|
||||
const char *msgChars = env->GetStringUTFChars(message, nullptr);
|
||||
|
||||
QString tagStr = QString::fromUtf8(tagChars);
|
||||
QString msgStr = QString::fromUtf8(msgChars);
|
||||
|
||||
// Converti i livelli di log Android in livelli Qt
|
||||
switch (level) {
|
||||
case 2: // VERBOSE
|
||||
qDebug() << "[VERBOSE:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 3: // DEBUG
|
||||
qDebug() << "[DEBUG:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 4: // INFO
|
||||
qInfo() << "[INFO:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 5: // WARN
|
||||
qWarning() << "[WARN:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 6: // ERROR
|
||||
qCritical() << "[ERROR:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 7: // ASSERT/WTF
|
||||
qCritical() << "[ASSERT:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
default:
|
||||
qDebug() << "[LOG:" << tagStr << "(" << level << ")]" << msgStr;
|
||||
}
|
||||
|
||||
env->ReleaseStringUTFChars(tag, tagChars);
|
||||
env->ReleaseStringUTFChars(message, msgChars);
|
||||
}
|
||||
#endif
|
||||
23
src/characteristics/characteristicnotifier0002.cpp
Normal file
23
src/characteristics/characteristicnotifier0002.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "characteristicnotifier0002.h"
|
||||
#include "bike.h"
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
|
||||
CharacteristicNotifier0002::CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent)
|
||||
: CharacteristicNotifier(0x0002, parent) {
|
||||
Bike = bike;
|
||||
answerList = QList<QByteArray>(); // Initialize empty list
|
||||
}
|
||||
|
||||
void CharacteristicNotifier0002::addAnswer(const QByteArray &newAnswer) {
|
||||
answerList.append(newAnswer);
|
||||
}
|
||||
|
||||
int CharacteristicNotifier0002::notify(QByteArray &value) {
|
||||
if(!answerList.isEmpty()) {
|
||||
value.append(answerList.first()); // Get first item
|
||||
answerList.removeFirst(); // Remove it from list
|
||||
return CN_OK;
|
||||
}
|
||||
return CN_INVALID;
|
||||
}
|
||||
19
src/characteristics/characteristicnotifier0002.h
Normal file
19
src/characteristics/characteristicnotifier0002.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef CHARACTERISTICNOTIFIER0002_H
|
||||
#define CHARACTERISTICNOTIFIER0002_H
|
||||
|
||||
#include "bluetoothdevice.h"
|
||||
#include "characteristicnotifier.h"
|
||||
#include <QList>
|
||||
|
||||
class CharacteristicNotifier0002 : public CharacteristicNotifier {
|
||||
Q_OBJECT
|
||||
bluetoothdevice* Bike = nullptr;
|
||||
QList<QByteArray> answerList;
|
||||
|
||||
public:
|
||||
explicit CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent = nullptr);
|
||||
int notify(QByteArray &value) override;
|
||||
void addAnswer(const QByteArray &newAnswer);
|
||||
};
|
||||
|
||||
#endif // CHARACTERISTICNOTIFIER0002_H
|
||||
23
src/characteristics/characteristicnotifier0004.cpp
Normal file
23
src/characteristics/characteristicnotifier0004.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "characteristicnotifier0004.h"
|
||||
#include "bike.h"
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
|
||||
CharacteristicNotifier0004::CharacteristicNotifier0004(bluetoothdevice *bike, QObject *parent)
|
||||
: CharacteristicNotifier(0x0004, parent) {
|
||||
Bike = bike;
|
||||
answerList = QList<QByteArray>();
|
||||
}
|
||||
|
||||
void CharacteristicNotifier0004::addAnswer(const QByteArray &newAnswer) {
|
||||
answerList.append(newAnswer);
|
||||
}
|
||||
|
||||
int CharacteristicNotifier0004::notify(QByteArray &value) {
|
||||
if(!answerList.isEmpty()) {
|
||||
value.append(answerList.first());
|
||||
answerList.removeFirst();
|
||||
return CN_OK;
|
||||
}
|
||||
return CN_INVALID;
|
||||
}
|
||||
19
src/characteristics/characteristicnotifier0004.h
Normal file
19
src/characteristics/characteristicnotifier0004.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef CHARACTERISTICNOTIFIER0004_H
|
||||
#define CHARACTERISTICNOTIFIER0004_H
|
||||
|
||||
#include "bluetoothdevice.h"
|
||||
#include "characteristicnotifier.h"
|
||||
#include <QList>
|
||||
|
||||
class CharacteristicNotifier0004 : public CharacteristicNotifier {
|
||||
Q_OBJECT
|
||||
bluetoothdevice* Bike = nullptr;
|
||||
QList<QByteArray> answerList;
|
||||
|
||||
public:
|
||||
explicit CharacteristicNotifier0004(bluetoothdevice *bike, QObject *parent = nullptr);
|
||||
int notify(QByteArray &value) override;
|
||||
void addAnswer(const QByteArray &newAnswer);
|
||||
};
|
||||
|
||||
#endif // CHARACTERISTICNOTIFIER0004_H
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "characteristicnotifier2acd.h"
|
||||
#include "devices/treadmill.h"
|
||||
#include <qmath.h>
|
||||
#include <QTime> // Include QTime for Bike->elapsedTime()
|
||||
|
||||
CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QObject *parent)
|
||||
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
|
||||
@@ -9,7 +10,8 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
value.append(0x0C); // Inclination available and distance for peloton
|
||||
value.append((char)0x01); // heart rate available
|
||||
//value.append((char)0x01); // heart rate available
|
||||
value.append((char)0x05); // HeartRate(8) | ElapsedTime(10)
|
||||
|
||||
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
|
||||
char a = (normalizeSpeed >> 8) & 0XFF;
|
||||
@@ -61,6 +63,18 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
rampBytes.append(b);
|
||||
rampBytes.append(a);
|
||||
|
||||
// Get session elapsed time - makes Runna calculations work
|
||||
QTime sessionElapsedTime = Bike->elapsedTime();
|
||||
double elapsed_time_seconds =
|
||||
(double)sessionElapsedTime.hour() * 3600.0 +
|
||||
(double)sessionElapsedTime.minute() * 60.0 +
|
||||
(double)sessionElapsedTime.second() +
|
||||
(double)sessionElapsedTime.msec() / 1000.0;
|
||||
uint16_t ftms_elapsed_time_field = (uint16_t)qRound(elapsed_time_seconds);
|
||||
QByteArray elapsedBytes;
|
||||
elapsedBytes.append(static_cast<char>(ftms_elapsed_time_field & 0xFF));
|
||||
elapsedBytes.append(static_cast<char>((ftms_elapsed_time_field >> 8) & 0xFF));
|
||||
|
||||
value.append(speedBytes); // Actual value.
|
||||
|
||||
value.append(distanceBytes); // Actual value.
|
||||
@@ -70,6 +84,9 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
value.append(rampBytes); // ramp angle
|
||||
|
||||
value.append(Bike->currentHeart().value()); // current heart rate
|
||||
|
||||
value.append(elapsedBytes); // Elapsed Time
|
||||
|
||||
return CN_OK;
|
||||
} else
|
||||
return CN_INVALID;
|
||||
|
||||
316
src/characteristics/characteristicwriteprocessor0003.cpp
Normal file
316
src/characteristics/characteristicwriteprocessor0003.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#include "characteristicwriteprocessor0003.h"
|
||||
#include <QDebug>
|
||||
#include "bike.h"
|
||||
|
||||
CharacteristicWriteProcessor0003::CharacteristicWriteProcessor0003(double bikeResistanceGain,
|
||||
int8_t bikeResistanceOffset,
|
||||
bluetoothdevice *bike,
|
||||
CharacteristicNotifier0002 *notifier0002,
|
||||
CharacteristicNotifier0004 *notifier0004,
|
||||
QObject *parent)
|
||||
: CharacteristicWriteProcessor(bikeResistanceGain, bikeResistanceOffset, bike, parent), notifier0002(notifier0002), notifier0004(notifier0004) {
|
||||
}
|
||||
|
||||
CharacteristicWriteProcessor0003::VarintResult CharacteristicWriteProcessor0003::decodeVarint(const QByteArray& bytes, int startIndex) {
|
||||
qint64 result = 0;
|
||||
int shift = 0;
|
||||
int bytesRead = 0;
|
||||
|
||||
for (int i = startIndex; i < bytes.size(); i++) {
|
||||
quint8 byte = static_cast<quint8>(bytes.at(i));
|
||||
result |= static_cast<qint64>(byte & 0x7F) << shift;
|
||||
bytesRead++;
|
||||
|
||||
if ((byte & 0x80) == 0) {
|
||||
break;
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
return {result, bytesRead};
|
||||
}
|
||||
|
||||
double CharacteristicWriteProcessor0003::currentGear() {
|
||||
if(zwiftGearReceived)
|
||||
return currentZwiftGear;
|
||||
else
|
||||
return ((bike*)Bike)->gears();
|
||||
}
|
||||
|
||||
qint32 CharacteristicWriteProcessor0003::decodeSInt(const QByteArray& bytes) {
|
||||
if (static_cast<quint8>(bytes.at(0)) != 0x22) {
|
||||
qFatal("Invalid field header");
|
||||
}
|
||||
|
||||
int length = static_cast<quint8>(bytes.at(1));
|
||||
|
||||
if (static_cast<quint8>(bytes.at(2)) != 0x10) {
|
||||
qFatal("Invalid inner header");
|
||||
}
|
||||
|
||||
VarintResult varint = decodeVarint(bytes, 3);
|
||||
|
||||
qint32 decoded = (varint.value >> 1) ^ -(varint.value & 1);
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
void CharacteristicWriteProcessor0003::handleZwiftGear(const QByteArray &array) {
|
||||
uint8_t g = 0;
|
||||
if (array.size() >= 2) {
|
||||
if ((uint8_t)array[0] == (uint8_t)0xCC && (uint8_t)array[1] == (uint8_t)0x3A) g = 1;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xFC && (uint8_t)array[1] == (uint8_t)0x43) g = 2;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xAC && (uint8_t)array[1] == (uint8_t)0x4D) g = 3;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0x56) g = 4;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0x8C && (uint8_t)array[1] == (uint8_t)0x60) g = 5;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xE8 && (uint8_t)array[1] == (uint8_t)0x6B) g = 6;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xC4 && (uint8_t)array[1] == (uint8_t)0x77) g = 7;
|
||||
else if (array.size() >= 3) {
|
||||
if ((uint8_t)array[0] == (uint8_t)0xA0 && (uint8_t)array[1] == (uint8_t)0x83 && (uint8_t)array[2] == (uint8_t)0x01) g = 8;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xA8 && (uint8_t)array[1] == (uint8_t)0x91 && (uint8_t)array[2] == (uint8_t)0x01) g = 9;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xB0 && (uint8_t)array[1] == (uint8_t)0x9F && (uint8_t)array[2] == (uint8_t)0x01) g = 10;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xB8 && (uint8_t)array[1] == (uint8_t)0xAD && (uint8_t)array[2] == (uint8_t)0x01) g = 11;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xC0 && (uint8_t)array[1] == (uint8_t)0xBB && (uint8_t)array[2] == (uint8_t)0x01) g = 12;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xF3 && (uint8_t)array[1] == (uint8_t)0xCB && (uint8_t)array[2] == (uint8_t)0x01) g = 13;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xA8 && (uint8_t)array[1] == (uint8_t)0xDC && (uint8_t)array[2] == (uint8_t)0x01) g = 14;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0xEC && (uint8_t)array[2] == (uint8_t)0x01) g = 15;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0x90 && (uint8_t)array[1] == (uint8_t)0xFD && (uint8_t)array[2] == (uint8_t)0x01) g = 16;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xD4 && (uint8_t)array[1] == (uint8_t)0x90 && (uint8_t)array[2] == (uint8_t)0x02) g = 17;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0x98 && (uint8_t)array[1] == (uint8_t)0xA4 && (uint8_t)array[2] == (uint8_t)0x02) g = 18;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0xB7 && (uint8_t)array[2] == (uint8_t)0x02) g = 19;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0x9F && (uint8_t)array[1] == (uint8_t)0xCB && (uint8_t)array[2] == (uint8_t)0x02) g = 20;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xD8 && (uint8_t)array[1] == (uint8_t)0xE2 && (uint8_t)array[2] == (uint8_t)0x02) g = 21;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0x90 && (uint8_t)array[1] == (uint8_t)0xFA && (uint8_t)array[2] == (uint8_t)0x02) g = 22;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xC8 && (uint8_t)array[1] == (uint8_t)0x91 && (uint8_t)array[2] == (uint8_t)0x03) g = 23;
|
||||
else if ((uint8_t)array[0] == (uint8_t)0xF3 && (uint8_t)array[1] == (uint8_t)0xAC && (uint8_t)array[2] == (uint8_t)0x03) g = 24;
|
||||
else { return; }
|
||||
}
|
||||
else { return; }
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
if(settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool()) {
|
||||
int actGear = ((bike*)Bike)->gears();
|
||||
if (g < actGear) {
|
||||
for (int i = 0; i < actGear - g; i++) {
|
||||
((bike*)Bike)->gearDown();
|
||||
}
|
||||
} else if (g > actGear) {
|
||||
for (int i = 0; i < g - actGear; i++) {
|
||||
((bike*)Bike)->gearUp();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (g < currentZwiftGear) {
|
||||
for (int i = 0; i < currentZwiftGear - g; ++i) {
|
||||
((bike*)Bike)->gearDown();
|
||||
}
|
||||
} else if (g > currentZwiftGear) {
|
||||
for (int i = 0; i < g - currentZwiftGear; ++i) {
|
||||
((bike*)Bike)->gearUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
currentZwiftGear = g;
|
||||
zwiftGearReceived = true;
|
||||
}
|
||||
|
||||
QByteArray CharacteristicWriteProcessor0003::encodeHubRidingData(
|
||||
uint32_t power,
|
||||
uint32_t cadence,
|
||||
uint32_t speedX100,
|
||||
uint32_t hr,
|
||||
uint32_t unknown1,
|
||||
uint32_t unknown2
|
||||
) {
|
||||
QByteArray buffer;
|
||||
buffer.append(char(0x03));
|
||||
|
||||
auto encodeVarInt32 = [](QByteArray& buf, uint32_t value) {
|
||||
do {
|
||||
uint8_t byte = value & 0x7F;
|
||||
value >>= 7;
|
||||
if (value) byte |= 0x80;
|
||||
buf.append(char(byte));
|
||||
} while (value);
|
||||
};
|
||||
|
||||
encodeVarInt32(buffer, (1 << 3) | 0);
|
||||
encodeVarInt32(buffer, power);
|
||||
|
||||
encodeVarInt32(buffer, (2 << 3) | 0);
|
||||
encodeVarInt32(buffer, cadence);
|
||||
|
||||
encodeVarInt32(buffer, (3 << 3) | 0);
|
||||
encodeVarInt32(buffer, speedX100);
|
||||
|
||||
encodeVarInt32(buffer, (4 << 3) | 0);
|
||||
encodeVarInt32(buffer, hr);
|
||||
|
||||
encodeVarInt32(buffer, (5 << 3) | 0);
|
||||
encodeVarInt32(buffer, unknown1);
|
||||
|
||||
encodeVarInt32(buffer, (6 << 3) | 0);
|
||||
encodeVarInt32(buffer, unknown2);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static uint32_t lastUnknown1 = 836; // Starting value from logs
|
||||
static uint32_t baseValue = 19000; // Base value from original code
|
||||
|
||||
uint32_t CharacteristicWriteProcessor0003::calculateUnknown1(uint16_t power) {
|
||||
// Increment by a value between 400-800 based on current power
|
||||
uint32_t increment = 400 + (power * 2);
|
||||
if (increment > 800) increment = 800;
|
||||
|
||||
// Adjust based on power changes
|
||||
if (power > 0) {
|
||||
lastUnknown1 += increment;
|
||||
} else {
|
||||
// For zero power, larger increments
|
||||
lastUnknown1 += 600;
|
||||
}
|
||||
|
||||
// Keep within observed range (800-24000)
|
||||
if (lastUnknown1 > 24000) lastUnknown1 = baseValue;
|
||||
|
||||
return lastUnknown1;
|
||||
}
|
||||
|
||||
int CharacteristicWriteProcessor0003::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
|
||||
static const QByteArray expectedHexArray = QByteArray::fromHex("52696465 4F6E02");
|
||||
static const QByteArray expectedHexArray2 = QByteArray::fromHex("410805");
|
||||
static const QByteArray expectedHexArray3 = QByteArray::fromHex("00088804");
|
||||
static const QByteArray expectedHexArray4 = QByteArray::fromHex("042A0A10 C0BB0120");
|
||||
static const QByteArray expectedHexArray4b = QByteArray::fromHex("042A0A10 A0830120");
|
||||
static const QByteArray expectedHexArray5 = QByteArray::fromHex("0422");
|
||||
static const QByteArray expectedHexArray6 = QByteArray::fromHex("042A0410");
|
||||
static const QByteArray expectedHexArray7 = QByteArray::fromHex("042A0310");
|
||||
static const QByteArray expectedHexArray8 = QByteArray::fromHex("0418");
|
||||
static const QByteArray expectedHexArray9 = QByteArray::fromHex("042a0810");
|
||||
static const QByteArray expectedHexArray10 = QByteArray::fromHex("000800");
|
||||
|
||||
QByteArray receivedData = data;
|
||||
|
||||
if (receivedData.startsWith(expectedHexArray)) {
|
||||
qDebug() << "Zwift Play Processor: Initial connection request";
|
||||
reply = QByteArray::fromHex("2a08031211220f4154582030342c2053545820303400");
|
||||
notifier0002->addAnswer(reply);
|
||||
reply = QByteArray::fromHex("2a0803120d220b524944455f4f4e28322900");
|
||||
notifier0002->addAnswer(reply);
|
||||
reply = QByteArray::fromHex("526964654f6e0200");
|
||||
notifier0004->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray2)) {
|
||||
qDebug() << "Zwift Play Processor: Device info request";
|
||||
reply = QByteArray::fromHex("3c080012320a3008800412040500050"
|
||||
"11a0b4b49434b5220434f524500320f"
|
||||
"3430323431383030393834000000003a01314204080110140");
|
||||
notifier0004->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray3)) {
|
||||
qDebug() << "Zwift Play Processor: Status request";
|
||||
reply = QByteArray::fromHex("3c0888041206 0a0440c0bb01");
|
||||
notifier0004->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray4) || receivedData.startsWith(expectedHexArray4b)) {
|
||||
qDebug() << "Zwift Play Ask 4";
|
||||
|
||||
reply = QByteArray::fromHex("0308001000185920002800309bed01");
|
||||
notifier0002->addAnswer(reply);
|
||||
|
||||
reply = QByteArray::fromHex("2a08031227222567"
|
||||
"61705f706172616d735f6368616e6765"
|
||||
"2832293a2037322c2037322c20302c20"
|
||||
"36303000");
|
||||
notifier0002->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray5)) {
|
||||
qDebug() << "Zwift Play Processor: Slope change request";
|
||||
double slopefloat = decodeSInt(receivedData.mid(1));
|
||||
QByteArray slope(2, 0);
|
||||
slope[0] = quint8(qint16(slopefloat) & 0xFF);
|
||||
slope[1] = quint8((qint16(slopefloat) >> 8) & 0x00FF);
|
||||
|
||||
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(),
|
||||
QByteArray::fromHex("116901") + slope + QByteArray::fromHex("3228"));
|
||||
|
||||
changeSlope(slopefloat, 0 /* TODO */, 0 /* TODO */);
|
||||
|
||||
reply = encodeHubRidingData(
|
||||
Bike->wattsMetric().value(),
|
||||
Bike->currentCadence().value(),
|
||||
0,
|
||||
Bike->wattsMetric().value(),
|
||||
calculateUnknown1(Bike->wattsMetric().value()),
|
||||
0
|
||||
);
|
||||
notifier0002->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray6)) {
|
||||
qDebug() << "Zwift Play Ask 6";
|
||||
|
||||
reply = QByteArray::fromHex("3c0888041206 0a0440c0bb01");
|
||||
reply[9] = receivedData[4];
|
||||
reply[10] = receivedData[5];
|
||||
reply[11] = receivedData[6];
|
||||
handleZwiftGear(receivedData.mid(4));
|
||||
notifier0004->addAnswer(reply);
|
||||
|
||||
reply = QByteArray::fromHex("03080010001827e7 20002896143093ed01");
|
||||
notifier0002->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray7)) {
|
||||
qDebug() << "Zwift Play Ask 7";
|
||||
|
||||
reply = QByteArray::fromHex("03080010001827e7 2000 28 00 3093ed01");
|
||||
notifier0002->addAnswer(reply);
|
||||
|
||||
reply = QByteArray::fromHex("3c088804120503408c60");
|
||||
reply[8] = receivedData[4];
|
||||
reply[9] = receivedData[5];
|
||||
handleZwiftGear(receivedData.mid(4));
|
||||
notifier0004->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray8)) {
|
||||
qDebug() << "Zwift Play Processor: Power request";
|
||||
VarintResult Power = decodeVarint(receivedData, 2);
|
||||
QByteArray power(2, 0);
|
||||
power[0] = quint8(qint16(Power.value) & 0xFF);
|
||||
power[1] = quint8((qint16(Power.value) >> 8) & 0x00FF);
|
||||
|
||||
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(),
|
||||
QByteArray::fromHex("05") + power);
|
||||
|
||||
reply = encodeHubRidingData(
|
||||
Bike->wattsMetric().value(),
|
||||
Bike->currentCadence().value(),
|
||||
0,
|
||||
Bike->wattsMetric().value(),
|
||||
calculateUnknown1(Bike->wattsMetric().value()),
|
||||
0
|
||||
);
|
||||
notifier0002->addAnswer(reply);
|
||||
|
||||
changePower(Power.value);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray9)) {
|
||||
qDebug() << "Zwift Play Ask 9";
|
||||
|
||||
reply = QByteArray::fromHex("050a08400058b60560fc26");
|
||||
notifier0004->addAnswer(reply);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray10)) {
|
||||
qDebug() << "Zwift Play Ask 10";
|
||||
|
||||
reply = QByteArray::fromHex("3c0800122408800412040004000c1a00320f42412d4534333732443932374244453a00420408011053");
|
||||
notifier0004->addAnswer(reply);
|
||||
}
|
||||
else {
|
||||
qDebug() << "Zwift Play Processor: Unhandled request:" << receivedData.toHex();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
45
src/characteristics/characteristicwriteprocessor0003.h
Normal file
45
src/characteristics/characteristicwriteprocessor0003.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef CHARACTERISTICWRITEPROCESSOR0003_H
|
||||
#define CHARACTERISTICWRITEPROCESSOR0003_H
|
||||
|
||||
#include "characteristicnotifier0002.h"
|
||||
#include "characteristicnotifier0004.h"
|
||||
#include "characteristicwriteprocessor.h"
|
||||
|
||||
class CharacteristicWriteProcessor0003 : public CharacteristicWriteProcessor {
|
||||
Q_OBJECT
|
||||
CharacteristicNotifier0002 *notifier0002 = nullptr;
|
||||
CharacteristicNotifier0004 *notifier0004 = nullptr;
|
||||
|
||||
public:
|
||||
explicit CharacteristicWriteProcessor0003(double bikeResistanceGain, int8_t bikeResistanceOffset,
|
||||
bluetoothdevice *bike, CharacteristicNotifier0002 *notifier0002,
|
||||
CharacteristicNotifier0004 *notifier0004,
|
||||
QObject *parent = nullptr);
|
||||
int writeProcess(quint16 uuid, const QByteArray &data, QByteArray &out) override;
|
||||
static QByteArray encodeHubRidingData(uint32_t power,
|
||||
uint32_t cadence,
|
||||
uint32_t speedX100,
|
||||
uint32_t hr,
|
||||
uint32_t unknown1,
|
||||
uint32_t unknown2);
|
||||
static uint32_t calculateUnknown1(uint16_t power);
|
||||
void handleZwiftGear(const QByteArray &array);
|
||||
double currentGear();
|
||||
|
||||
|
||||
private:
|
||||
struct VarintResult {
|
||||
qint64 value;
|
||||
int bytesRead;
|
||||
};
|
||||
|
||||
VarintResult decodeVarint(const QByteArray& bytes, int startIndex);
|
||||
qint32 decodeSInt(const QByteArray& bytes);
|
||||
int currentZwiftGear = 8;
|
||||
bool zwiftGearReceived = false;
|
||||
|
||||
signals:
|
||||
void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
};
|
||||
|
||||
#endif // CHARACTERISTICWRITEPROCESSOR0003_H
|
||||
187
src/devices/android_antbike/android_antbike.cpp
Normal file
187
src/devices/android_antbike/android_antbike.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "android_antbike.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
#include <math.h>
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#include <QLowEnergyConnectionParameters>
|
||||
#endif
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
android_antbike::android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
this->noVirtualDevice = noVirtualDevice;
|
||||
initDone = false;
|
||||
connect(refresh, &QTimer::timeout, this, &android_antbike::update);
|
||||
refresh->start(200ms);
|
||||
}
|
||||
|
||||
void android_antbike::update() {
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
Heart = KeepAwakeHelper::antObject(true)->callMethod<int>("getHeart", "()I");
|
||||
Cadence = KeepAwakeHelper::antObject(true)->callMethod<int>("getBikeCadence", "()I");
|
||||
m_watt = KeepAwakeHelper::antObject(true)->callMethod<int>("getBikePower", "()I");
|
||||
if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
m_watt.value(), 0, Speed.value(), fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0),
|
||||
speedLimit());
|
||||
} else {
|
||||
Speed = KeepAwakeHelper::antObject(true)->callMethod<double>("getBikeSpeed", "()D");
|
||||
}
|
||||
bool bikeConnected = KeepAwakeHelper::antObject(true)->callMethod<jboolean>("isBikeConnected", "()Z");
|
||||
qDebug() << QStringLiteral("Current ANT Cadence: ") << QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current ANT Speed: ") << QString::number(Speed.value());
|
||||
qDebug() << QStringLiteral("Current ANT Power: ") << QString::number(m_watt.value());
|
||||
qDebug() << QStringLiteral("Current ANT Heart: ") << QString::number(Heart.value());
|
||||
qDebug() << QStringLiteral("ANT Bike Connected: ") << bikeConnected;
|
||||
#endif
|
||||
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
}
|
||||
|
||||
if (requestInclination != -100) {
|
||||
Inclination = requestInclination;
|
||||
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
|
||||
requestInclination = -100;
|
||||
}
|
||||
|
||||
update_metrics(false, watts());
|
||||
|
||||
Distance += ((Speed.value() / (double)3600.0) /
|
||||
((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))));
|
||||
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
|
||||
// ******************************************* virtual bike init *************************************
|
||||
if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
&& !h
|
||||
#endif
|
||||
#endif
|
||||
) {
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence) {
|
||||
qDebug() << "ios_peloton_workaround activated!";
|
||||
h = new lockscreen();
|
||||
h->virtualbike_ios();
|
||||
} else
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
emit debug(QStringLiteral("creating virtual bike interface..."));
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &android_antbike::changeInclinationRequested);
|
||||
connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &android_antbike::ftmsCharacteristicChanged);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
if (!firstStateChanged)
|
||||
emit connectedAndDiscovered();
|
||||
firstStateChanged = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
if (!noVirtualDevice) {
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
|
||||
Heart = (uint8_t)KeepAwakeHelper::heart();
|
||||
debug("Current Heart: " + QString::number(Heart.value()));
|
||||
}
|
||||
#endif
|
||||
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
|
||||
update_hr_from_external();
|
||||
}
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
if (Heart.value()) {
|
||||
static double lastKcal = 0;
|
||||
if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator
|
||||
lastKcal = abs(KCal.value());
|
||||
KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal;
|
||||
}
|
||||
|
||||
if (requestResistance != -1 && requestResistance != currentResistance().value()) {
|
||||
Resistance = requestResistance;
|
||||
m_pelotonResistance = requestResistance;
|
||||
}
|
||||
}
|
||||
|
||||
void android_antbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QByteArray b = newValue;
|
||||
qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' ');
|
||||
}
|
||||
|
||||
void android_antbike::changeInclinationRequested(double grade, double percentage) {
|
||||
if (percentage < 0)
|
||||
percentage = 0;
|
||||
changeInclination(grade, percentage);
|
||||
}
|
||||
|
||||
uint16_t android_antbike::wattsFromResistance(double resistance) {
|
||||
return _ergTable.estimateWattage(Cadence.value(), 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;
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
uint16_t android_antbike::watts() { return m_watt.value(); }
|
||||
bool android_antbike::connected() { return true; }
|
||||
81
src/devices/android_antbike/android_antbike.h
Normal file
81
src/devices/android_antbike/android_antbike.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#ifndef ANDROID_ANTBIKE_H
|
||||
#define ANDROID_ANTBIKE_H
|
||||
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "devices/bike.h"
|
||||
#include "ergtable.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class android_antbike : public bike {
|
||||
Q_OBJECT
|
||||
public:
|
||||
android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice);
|
||||
bool connected() override;
|
||||
uint16_t watts() override;
|
||||
resistance_t maxResistance() override { return 100; }
|
||||
resistance_t resistanceFromPowerRequest(uint16_t power) override;
|
||||
|
||||
private:
|
||||
QTimer *refresh;
|
||||
|
||||
uint8_t sec1Update = 0;
|
||||
QByteArray lastPacket;
|
||||
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
QDateTime lastGoodCadence = QDateTime::currentDateTime();
|
||||
uint8_t firstStateChanged = 0;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
bool noVirtualDevice = false;
|
||||
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
|
||||
private slots:
|
||||
void changeInclinationRequested(double grade, double percentage);
|
||||
void update();
|
||||
|
||||
void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
};
|
||||
#endif // ANDROID_ANTBIKE_H
|
||||
@@ -57,7 +57,13 @@ void apexbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
} else {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
}
|
||||
|
||||
|
||||
if (!disable_log) {
|
||||
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
|
||||
@@ -62,17 +62,28 @@ void bike::changePower(int32_t power) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestPower = power; // used by some bikes that have ERG mode builtin
|
||||
QSettings settings;
|
||||
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"));
|
||||
double erg_filter_upper =
|
||||
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();
|
||||
|
||||
requestPower = power; // used by some bikes that have ERG mode builtin
|
||||
|
||||
if(power_sensor && ergModeSupported && m_rawWatt.value() > 0 && m_watt.value() > 0 && fabs(requestPower - m_watt.average5s()) < qMax(erg_filter_upper, erg_filter_lower)) {
|
||||
qDebug() << "applying delta watt to power request m_rawWatt" << m_rawWatt.average5s() << "watt" << m_watt.average5s() << "req" << requestPower;
|
||||
// the concept here is to trying to add or decrease the delta from the power sensor
|
||||
requestPower += (requestPower - m_watt.average5s());
|
||||
}
|
||||
|
||||
bool force_resistance =
|
||||
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
|
||||
.toBool();
|
||||
// bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); //Not used
|
||||
// anywhere in code
|
||||
double erg_filter_upper =
|
||||
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();
|
||||
double deltaDown = wattsMetric().value() - ((double)power);
|
||||
double deltaUp = ((double)power) - wattsMetric().value();
|
||||
qDebug() << QStringLiteral("filter ") + QString::number(deltaUp) + " " + QString::number(deltaDown) + " " +
|
||||
@@ -176,6 +187,7 @@ void bike::clearStats() {
|
||||
m_jouls.clear(true);
|
||||
elevationAcc = 0;
|
||||
m_watt.clear(false);
|
||||
m_rawWatt.clear(false);
|
||||
WeightLoss.clear(false);
|
||||
|
||||
RequestedPelotonResistance.clear(false);
|
||||
@@ -203,6 +215,7 @@ void bike::setPaused(bool p) {
|
||||
Heart.setPaused(p);
|
||||
m_jouls.setPaused(p);
|
||||
m_watt.setPaused(p);
|
||||
m_rawWatt.setPaused(p);
|
||||
WeightLoss.setPaused(p);
|
||||
m_pelotonResistance.setPaused(p);
|
||||
Cadence.setPaused(p);
|
||||
@@ -228,6 +241,7 @@ void bike::setLap() {
|
||||
Heart.setLap(false);
|
||||
m_jouls.setLap(true);
|
||||
m_watt.setLap(false);
|
||||
m_rawWatt.setLap(false);
|
||||
WeightLoss.setLap(false);
|
||||
WattKg.setLap(false);
|
||||
|
||||
|
||||
@@ -109,6 +109,8 @@ void bluetooth::finished() {
|
||||
QSettings settings;
|
||||
bool antbike =
|
||||
settings.value(QZSettings::antbike, QZSettings::default_antbike).toBool();
|
||||
bool android_antbike =
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
|
||||
QString nordictrack_2950_ip =
|
||||
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();
|
||||
@@ -121,7 +123,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) {
|
||||
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || antbike || android_antbike) {
|
||||
// faking a bluetooth device
|
||||
qDebug() << "faking a bluetooth device for nordictrack_2950_ip";
|
||||
deviceDiscovered(QBluetoothDeviceInfo());
|
||||
@@ -462,6 +464,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
settings.value(QZSettings::proformtreadmillip, QZSettings::default_proformtreadmillip).toString();
|
||||
bool antbike_setting =
|
||||
settings.value(QZSettings::antbike, QZSettings::default_antbike).toBool();
|
||||
bool android_antbike_setting =
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
|
||||
QString nordictrack_2950_ip =
|
||||
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();
|
||||
@@ -817,6 +821,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(antBike);
|
||||
} else if (android_antbike_setting && !android_antBike) {
|
||||
this->stopDiscovery();
|
||||
android_antBike = new android_antbike(noWriteResistance, noHeartService, false);
|
||||
emit deviceConnected(b);
|
||||
connect(android_antBike, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
// connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
connect(android_antBike, &android_antbike::debug, this, &bluetooth::debug);
|
||||
// connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop())); //NOTE: Commented due to #358
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(android_antBike);
|
||||
} else if (!proformtreadmillip.isEmpty() && !proformWifiTreadmill) {
|
||||
this->stopDiscovery();
|
||||
proformWifiTreadmill = new proformwifitreadmill(noWriteResistance, noHeartService, bikeResistanceOffset,
|
||||
@@ -1334,7 +1351,27 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(soleF80);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("LF")) && b.name().length() == 18) &&
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM01")) && !speraXTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
speraXTreadmill = new speraxtreadmill(this->pollDeviceTime, noConsole, noHeartService);
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
stateFileRead();
|
||||
#endif
|
||||
emit deviceConnected(b);
|
||||
connect(speraXTreadmill, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
// connect(speraXTreadmill, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
connect(speraXTreadmill, &speraxtreadmill::debug, this, &bluetooth::debug);
|
||||
connect(speraXTreadmill, &speraxtreadmill::speedChanged, this, &bluetooth::speedChanged);
|
||||
connect(speraXTreadmill, &speraxtreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
speraXTreadmill->deviceDiscovered(b);
|
||||
connect(this, &bluetooth::searchingStop, speraXTreadmill, &speraxtreadmill::searchingStop);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(speraXTreadmill);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("LF")) && (b.name().length() == 18 || b.name().length() == 15)) &&
|
||||
!lifefitnessTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1396,21 +1433,27 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BFX_T9_")) ||
|
||||
(b.name().toUpper().startsWith("3G PRO ")) ||
|
||||
(b.name().toUpper().startsWith("3G ELITE ")) ||
|
||||
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("LJJ-")) || // LJJ-02351A
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
|
||||
(b.name().toUpper().startsWith("SCHWINN 810")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
|
||||
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TT8")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ST90")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("XT485")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI WMTP")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("LB600")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T60-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T90-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
|
||||
@@ -1516,6 +1559,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
#endif
|
||||
} else if ((b.name().toUpper().startsWith("TACX ") ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("THINK X")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("THINK-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("THINK_")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("53997-")) ||
|
||||
(b.name().toUpper().startsWith("VANRYSEL-HT")) ||
|
||||
b.address() == QBluetoothAddress("C1:14:D9:9C:FB:01") || // specific TACX NEO 2 #1707
|
||||
(b.name().toUpper().startsWith("TACX SMART BIKE"))) &&
|
||||
@@ -1534,7 +1580,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// connect(tacxneo2Bike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
|
||||
tacxneo2Bike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(tacxneo2Bike);
|
||||
} else if ((b.name().toUpper().startsWith("INDOORCYCLE")) &&
|
||||
} else if (((b.name().toUpper().startsWith("INDOORCYCLE")) ||
|
||||
((b.name().toUpper().startsWith("MAGNUS ")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) &&
|
||||
!cycleopsphantomBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1580,7 +1627,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25
|
||||
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
|
||||
(b.name().toUpper().startsWith("3G CARDIO ")) ||
|
||||
(b.name().toUpper().startsWith("ZWIFT HUB")) || (b.name().toUpper().startsWith("MAGNUS ")) ||
|
||||
(b.name().toUpper().startsWith("ZWIFT HUB")) ||
|
||||
((b.name().toUpper().startsWith("MAGNUS ")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith("HAMMER ") && !power_as_bike && !saris_trainer) || // HAMMER 64123
|
||||
(b.name().toUpper().startsWith("FLXCY-")) || // Pro FlexBike
|
||||
(b.name().toUpper().startsWith("QB-WC01")) || // Nexgim QB-C01 smart bike
|
||||
@@ -1626,13 +1674,13 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("FELVON V2")) ||
|
||||
(b.name().toUpper().startsWith("JUSTO")) ||
|
||||
(b.name().toUpper().startsWith("MYCYCLE ")) ||
|
||||
(b.name().toUpper().startsWith("T2 ")) ||
|
||||
(b.name().toUpper().startsWith("DR") && b.name().length() == 2) ||
|
||||
(b.name().toUpper().startsWith("T2 ")) ||
|
||||
(b.name().toUpper().startsWith("RC-MAX-")) ||
|
||||
(b.name().toUpper().startsWith("TPS-SPBIKE-2.0")) ||
|
||||
(b.name().toUpper().startsWith("NEO BIKE SMART")) ||
|
||||
(b.name().toUpper().startsWith("ZDRIVE")) ||
|
||||
(b.name().toUpper().startsWith("TUNTURI E60-")) ||
|
||||
(b.name().toUpper().startsWith("TUNTURI F40-")) ||
|
||||
(b.name().toUpper().startsWith("JFBK5.0")) ||
|
||||
(b.name().toUpper().startsWith("NEO 3M ")) ||
|
||||
(b.name().toUpper().startsWith("JFBK7.0")) ||
|
||||
@@ -1640,14 +1688,24 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("POOBOO")) ||
|
||||
(b.name().toUpper().startsWith("ZYCLE ZPRO")) ||
|
||||
(b.name().toUpper().startsWith("SM720I")) ||
|
||||
(b.name().toUpper().startsWith("H9115 LYON")) ||
|
||||
(b.name().toUpper().startsWith("AVANTI")) ||
|
||||
(b.name().toUpper().startsWith("T300P_")) ||
|
||||
(b.name().toUpper().startsWith("T200_")) ||
|
||||
(b.name().toUpper().startsWith("BZ9110 ")) ||
|
||||
(b.name().toUpper().startsWith("CFC") && b.name().length() == 14) || // CFC31231004349
|
||||
(b.name().toUpper().startsWith("TITAN 7000")) ||
|
||||
(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("MRK-S26S-")) ||
|
||||
(b.name().toUpper().startsWith("ROBX")) ||
|
||||
(b.name().toUpper().startsWith("XCX-")) ||
|
||||
(b.name().toUpper().startsWith("NEO BIKE PLUS ")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) && !b.name().toUpper().endsWith(QStringLiteral("SKI")) && !b.name().toUpper().endsWith(QStringLiteral("ROW"))) ||
|
||||
(b.name().toUpper().startsWith("L-") && b.name().length() == 11) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) ||
|
||||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
|
||||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
@@ -1658,7 +1716,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("DIRETO X")) || (b.name().toUpper().startsWith("MERACH-667-")) ||
|
||||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) &&
|
||||
!ftmsBike && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
ftms_rower.contains(QZSettings::default_ftms_rower) &&
|
||||
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
ftmsBike = new ftmsbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
@@ -1733,7 +1792,10 @@ 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")) &&
|
||||
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
|
||||
@@ -1766,7 +1828,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
smartrowRower->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(smartrowRower);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("PM5")) &&
|
||||
!b.name().toUpper().endsWith(QStringLiteral("ROW"))) &&
|
||||
b.name().toUpper().endsWith(QStringLiteral("SKI"))) &&
|
||||
!concept2Skierg && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1790,6 +1852,8 @@ 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("ROWER ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DFIT-L-R")) ||
|
||||
!b.name().compare(ftms_rower, Qt::CaseInsensitive) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) &&
|
||||
@@ -1807,13 +1871,28 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// connect(ftmsRower, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
|
||||
ftmsRower->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(ftmsRower);
|
||||
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-EC-SPT"))) &&
|
||||
!echelonStairclimber && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
echelonStairclimber = new echelonstairclimber(this->pollDeviceTime, noConsole, noHeartService);
|
||||
// stateFileRead();
|
||||
emit deviceConnected(b);
|
||||
connect(echelonStairclimber, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
// connect(echelonRower, SIGNAL(disconnected()), this, SLOT(restart())); connect(echelonStride,
|
||||
connect(echelonStairclimber, &echelonstairclimber::debug, this, &bluetooth::debug);
|
||||
connect(echelonStairclimber, &echelonstairclimber::speedChanged, this, &bluetooth::speedChanged);
|
||||
connect(echelonStairclimber, &echelonstairclimber::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
echelonStairclimber->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(echelonStairclimber);
|
||||
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("STRIDE")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("STRIDE6S-")) ||
|
||||
b.name().toUpper().startsWith(QLatin1String("ECH-SD-SPT"))) &&
|
||||
!echelonStride && filter) {
|
||||
!echelonStride && !echelonStairclimber && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
echelonStride = new echelonstride(this->pollDeviceTime, noConsole, noHeartService);
|
||||
@@ -1903,7 +1982,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// connect(echelonRower, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
|
||||
echelonRower->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(echelonRower);
|
||||
} else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride && !ftmsBike &&
|
||||
} else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride && !echelonStairclimber && !ftmsBike &&
|
||||
!echelonConnectSport && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2149,6 +2228,21 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// SLOT(inclinationChanged(double)));
|
||||
pafersTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(pafersTreadmill);
|
||||
|
||||
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("MOXY6")) && !moxy5Sensor) {
|
||||
// *** SPECIAL DEVICE ****
|
||||
moxy5Sensor = new moxy5sensor();
|
||||
connect(moxy5Sensor, &moxy5sensor::debug, this, &bluetooth::debug);
|
||||
moxy5Sensor->deviceDiscovered(b);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("CORE ")) && !coreSensor) {
|
||||
// *** SPECIAL DEVICE ****
|
||||
coreSensor = new coresensor();
|
||||
connect(coreSensor, &coresensor::debug, this, &bluetooth::debug);
|
||||
coreSensor->deviceDiscovered(b);
|
||||
connect(coreSensor, &coresensor::coreBodyTemperatureChanged, this->device(), &bluetoothdevice::coreBodyTemperature);
|
||||
connect(coreSensor, &coresensor::skinTemperatureChanged, this->device(), &bluetoothdevice::skinTemperature);
|
||||
connect(coreSensor, &coresensor::heatStrainIndexChanged, this->device(), &bluetoothdevice::heatStrainIndex);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("BOWFLEX T")) && !bowflexT216Treadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2321,10 +2415,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
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("IBIKING+")) ||
|
||||
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"))) ||
|
||||
@@ -2759,6 +2853,8 @@ void bluetooth::connectedAndDiscovered() {
|
||||
|
||||
connect(powerSensor, &stagesbike::debug, this, &bluetooth::debug);
|
||||
connect(powerSensor, &bluetoothdevice::powerChanged, this->device(), &bluetoothdevice::powerSensor);
|
||||
connect(powerSensor, &bluetoothdevice::cadenceChanged, this->device(),
|
||||
&bluetoothdevice::cadenceSensor);
|
||||
powerSensor->deviceDiscovered(b);
|
||||
} else if (device() && device()->deviceType() == bluetoothdevice::TREADMILL) {
|
||||
powerSensorRun = new strydrunpowersensor(false, false, true);
|
||||
@@ -2880,6 +2976,25 @@ 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) {
|
||||
|
||||
eliteSquareController = new elitesquarecontroller(this->device());
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
|
||||
connect(eliteSquareController, &elitesquarecontroller::debug, this, &bluetooth::debug);
|
||||
connect(eliteSquareController, &elitesquarecontroller::plus, (bike*)this->device(), &bike::gearUp);
|
||||
connect(eliteSquareController, &elitesquarecontroller::minus, (bike*)this->device(), &bike::gearDown);
|
||||
eliteSquareController->deviceDiscovered(b);
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Elite Square Connected!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
|
||||
bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool();
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
@@ -2921,12 +3036,13 @@ void bluetooth::connectedAndDiscovered() {
|
||||
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
|
||||
"activity", "()Landroid/app/Activity;");
|
||||
KeepAwakeHelper::antObject(true)->callMethod<void>(
|
||||
"antStart", "(Landroid/app/Activity;ZZZZ)V", activity.object<jobject>(),
|
||||
"antStart", "(Landroid/app/Activity;ZZZZZ)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);
|
||||
device()->deviceType() == bluetoothdevice::ELLIPTICAL,
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool());
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) {
|
||||
@@ -3074,6 +3190,11 @@ void bluetooth::restart() {
|
||||
delete horizonTreadmill;
|
||||
horizonTreadmill = nullptr;
|
||||
}
|
||||
if (speraXTreadmill) {
|
||||
|
||||
delete speraXTreadmill;
|
||||
speraXTreadmill = nullptr;
|
||||
}
|
||||
if (lifefitnessTreadmill) {
|
||||
|
||||
delete lifefitnessTreadmill;
|
||||
@@ -3179,6 +3300,11 @@ void bluetooth::restart() {
|
||||
delete antBike;
|
||||
antBike = nullptr;
|
||||
}
|
||||
if (android_antBike) {
|
||||
|
||||
delete android_antBike;
|
||||
android_antBike = nullptr;
|
||||
}
|
||||
if (proformTelnetBike) {
|
||||
|
||||
delete proformTelnetBike;
|
||||
@@ -3335,6 +3461,11 @@ void bluetooth::restart() {
|
||||
delete echelonStride;
|
||||
echelonStride = nullptr;
|
||||
}
|
||||
if (echelonStairclimber) {
|
||||
|
||||
delete echelonStairclimber;
|
||||
echelonStairclimber = nullptr;
|
||||
}
|
||||
if (octaneTreadmill) {
|
||||
|
||||
delete octaneTreadmill;
|
||||
@@ -3669,6 +3800,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return proformWifiTreadmill;
|
||||
} else if (antBike) {
|
||||
return antBike;
|
||||
} else if (android_antBike) {
|
||||
return android_antBike;
|
||||
} else if (nordictrackifitadbTreadmill) {
|
||||
return nordictrackifitadbTreadmill;
|
||||
} else if (nordictrackifitadbBike) {
|
||||
@@ -3727,6 +3860,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return horizonTreadmill;
|
||||
} else if (lifefitnessTreadmill) {
|
||||
return lifefitnessTreadmill;
|
||||
} else if (speraXTreadmill) {
|
||||
return speraXTreadmill;
|
||||
} else if (technogymmyrunTreadmill) {
|
||||
return technogymmyrunTreadmill;
|
||||
#ifndef Q_OS_IOS
|
||||
@@ -3751,6 +3886,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return echelonRower;
|
||||
} else if (echelonStride) {
|
||||
return echelonStride;
|
||||
} else if (echelonStairclimber) {
|
||||
return echelonStairclimber;
|
||||
} else if (octaneTreadmill) {
|
||||
return octaneTreadmill;
|
||||
} else if (ziproTreadmill) {
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
#include "qzsettings.h"
|
||||
|
||||
#include "devices/activiotreadmill/activiotreadmill.h"
|
||||
#include "devices/speraxtreadmill/speraxtreadmill.h"
|
||||
#include "devices/antbike/antbike.h"
|
||||
#include "devices/android_antbike/android_antbike.h"
|
||||
#include "devices/apexbike/apexbike.h"
|
||||
#include "devices/bhfitnesselliptical/bhfitnesselliptical.h"
|
||||
#include "devices/bkoolbike/bkoolbike.h"
|
||||
@@ -29,6 +31,7 @@
|
||||
#include "devices/bowflext216treadmill/bowflext216treadmill.h"
|
||||
#include "devices/bowflextreadmill/bowflextreadmill.h"
|
||||
#include "devices/chronobike/chronobike.h"
|
||||
#include "devices/coresensor/coresensor.h"
|
||||
#ifndef Q_OS_IOS
|
||||
#include "devices/computrainerbike/computrainerbike.h"
|
||||
#include "devices/csaferower/csaferower.h"
|
||||
@@ -46,8 +49,10 @@
|
||||
|
||||
#include "devices/echelonconnectsport/echelonconnectsport.h"
|
||||
#include "devices/echelonrower/echelonrower.h"
|
||||
#include "devices/echelonstairclimber/echelonstairclimber.h"
|
||||
#include "devices/eliteariafan/eliteariafan.h"
|
||||
#include "devices/eliterizer/eliterizer.h"
|
||||
#include "devices/elitesquarecontroller/elitesquarecontroller.h"
|
||||
#include "devices/elitesterzosmart/elitesterzosmart.h"
|
||||
#include "devices/eslinkertreadmill/eslinkertreadmill.h"
|
||||
#include "devices/fakebike/fakebike.h"
|
||||
@@ -77,6 +82,7 @@
|
||||
#include "devices/m3ibike/m3ibike.h"
|
||||
#include "devices/mcfbike/mcfbike.h"
|
||||
#include "devices/mepanelbike/mepanelbike.h"
|
||||
#include "devices/moxy5sensor/moxy5sensor.h"
|
||||
#include "devices/nautilusbike/nautilusbike.h"
|
||||
#include "devices/nautiluselliptical/nautiluselliptical.h"
|
||||
#include "devices/nautilustreadmill/nautilustreadmill.h"
|
||||
@@ -173,11 +179,13 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
QFile *debugCommsLog = nullptr;
|
||||
QBluetoothDeviceDiscoveryAgent *discoveryAgent = nullptr;
|
||||
antbike *antBike = nullptr;
|
||||
android_antbike *android_antBike = nullptr;
|
||||
apexbike *apexBike = nullptr;
|
||||
bkoolbike *bkoolBike = nullptr;
|
||||
bhfitnesselliptical *bhFitnessElliptical = nullptr;
|
||||
bowflextreadmill *bowflexTreadmill = nullptr;
|
||||
bowflext216treadmill *bowflexT216Treadmill = nullptr;
|
||||
coresensor* coreSensor = nullptr;
|
||||
crossrope *crossRope = nullptr;
|
||||
fitshowtreadmill *fitshowTreadmill = nullptr;
|
||||
focustreadmill *focusTreadmill = nullptr;
|
||||
@@ -199,6 +207,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
trxappgateusbtreadmill *trxappgateusb = nullptr;
|
||||
spirittreadmill *spiritTreadmill = nullptr;
|
||||
activiotreadmill *activioTreadmill = nullptr;
|
||||
speraxtreadmill *speraXTreadmill = nullptr;
|
||||
nautilusbike *nautilusBike = nullptr;
|
||||
nautiluselliptical *nautilusElliptical = nullptr;
|
||||
nautilustreadmill *nautilusTreadmill = nullptr;
|
||||
@@ -208,6 +217,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
echelonconnectsport *echelonConnectSport = nullptr;
|
||||
yesoulbike *yesoulBike = nullptr;
|
||||
flywheelbike *flywheelBike = nullptr;
|
||||
moxy5sensor *moxy5Sensor = nullptr;
|
||||
nordictrackelliptical *nordictrackElliptical = nullptr;
|
||||
nordictrackifitadbtreadmill *nordictrackifitadbTreadmill = nullptr;
|
||||
nordictrackifitadbbike *nordictrackifitadbBike = nullptr;
|
||||
@@ -256,6 +266,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
ftmsrower *ftmsRower = nullptr;
|
||||
smartrowrower *smartrowRower = nullptr;
|
||||
echelonstride *echelonStride = nullptr;
|
||||
echelonstairclimber *echelonStairclimber = nullptr;
|
||||
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
|
||||
lifespantreadmill *lifespanTreadmill = nullptr;
|
||||
keepbike *keepBike = nullptr;
|
||||
@@ -292,6 +303,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
QList<zwiftclickremote* > zwiftPlayDevice;
|
||||
zwiftclickremote* zwiftClickRemote = nullptr;
|
||||
sramaxscontroller* sramAXSController = nullptr;
|
||||
elitesquarecontroller* eliteSquareController = nullptr;
|
||||
QString filterDevice = QLatin1String("");
|
||||
|
||||
bool testResistance = false;
|
||||
|
||||
@@ -108,6 +108,7 @@ 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::jouls() { return m_jouls; }
|
||||
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
|
||||
@@ -132,6 +133,9 @@ bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
|
||||
bool bluetoothdevice::connected() { return false; }
|
||||
metric bluetoothdevice::elevationGain() { return elevationAcc; }
|
||||
void bluetoothdevice::heartRate(uint8_t heart) { Heart.setValue(heart); }
|
||||
void bluetoothdevice::coreBodyTemperature(double coreBodyTemperature) { CoreBodyTemperature.setValue(coreBodyTemperature); }
|
||||
void bluetoothdevice::skinTemperature(double skinTemperature) { SkinTemperature.setValue(skinTemperature); }
|
||||
void bluetoothdevice::heatStrainIndex(double heatStrainIndex) { HeatStrainIndex.setValue(heatStrainIndex); }
|
||||
void bluetoothdevice::disconnectBluetooth() {
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
@@ -281,11 +285,15 @@ void bluetoothdevice::clearStats() {
|
||||
m_jouls.clear(true);
|
||||
elevationAcc = 0;
|
||||
m_watt.clear(false);
|
||||
m_rawWatt.clear(false);
|
||||
WeightLoss.clear(false);
|
||||
WattKg.clear(false);
|
||||
Cadence.clear(false);
|
||||
for(int i=0; i<maxHeartZone(); i++) {
|
||||
hrZonesSeconds[i].clear(false);
|
||||
}
|
||||
for(int i=0; i<maxHeatZone(); i++) {
|
||||
heatZonesSeconds[i].clear(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,11 +309,15 @@ void bluetoothdevice::setPaused(bool p) {
|
||||
Heart.setPaused(p);
|
||||
m_jouls.setPaused(p);
|
||||
m_watt.setPaused(p);
|
||||
m_rawWatt.setPaused(p);
|
||||
WeightLoss.setPaused(p);
|
||||
WattKg.setPaused(p);
|
||||
Cadence.setPaused(p);
|
||||
for(int i=0; i<maxHeartZone(); i++) {
|
||||
hrZonesSeconds[i].setPaused(p);
|
||||
}
|
||||
for(int i=0; i<maxHeatZone(); i++) {
|
||||
heatZonesSeconds[i].setPaused(p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,11 +332,15 @@ void bluetoothdevice::setLap() {
|
||||
Heart.setLap(false);
|
||||
m_jouls.setLap(true);
|
||||
m_watt.setLap(false);
|
||||
m_rawWatt.setLap(false);
|
||||
WeightLoss.setLap(false);
|
||||
WattKg.setLap(false);
|
||||
Cadence.setLap(false);
|
||||
for(int i=0; i<maxHeartZone(); i++) {
|
||||
hrZonesSeconds[i].setLap(false);
|
||||
}
|
||||
for(int i=0; i<maxHeatZone(); i++) {
|
||||
heatZonesSeconds[i].setLap(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,9 +491,9 @@ void bluetoothdevice::setGPXFile(QString filename) {
|
||||
}
|
||||
}
|
||||
|
||||
void bluetoothdevice::setHeartZone(double hz) {
|
||||
void bluetoothdevice::setHeartZone(double hz) {
|
||||
HeartZone = hz;
|
||||
if(isPaused() == false) {
|
||||
if(isPaused() == false && currentHeart().value() > 0) {
|
||||
hz = hz - 1;
|
||||
if(hz >= maxHeartZone() ) {
|
||||
hrZonesSeconds[maxHeartZone() - 1].setValue(hrZonesSeconds[maxHeartZone() - 1].value() + 1);
|
||||
@@ -489,9 +505,39 @@ void bluetoothdevice::setHeartZone(double hz) {
|
||||
}
|
||||
}
|
||||
|
||||
void bluetoothdevice::setHeatZone(double heatStrainIndex) {
|
||||
// Determine heat zone based on Heat Strain Index values
|
||||
uint8_t zone;
|
||||
if (heatStrainIndex >= 0 && heatStrainIndex <= 1.99) {
|
||||
zone = 1;
|
||||
} else if (heatStrainIndex < 3.0) {
|
||||
zone = 2;
|
||||
} else if (heatStrainIndex < 7.0) {
|
||||
zone = 3;
|
||||
} else {
|
||||
zone = 4;
|
||||
}
|
||||
|
||||
HeatZone = zone;
|
||||
if(isPaused() == false && heatStrainIndex > 0) {
|
||||
// Convert to 0-based index for array access
|
||||
uint8_t zoneIndex = zone - 1;
|
||||
if(zoneIndex < maxHeatZone()) {
|
||||
heatZonesSeconds[zoneIndex].setValue(heatZonesSeconds[zoneIndex].value() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t bluetoothdevice::secondsForHeartZone(uint8_t zone) {
|
||||
if(zone < maxHeartZone()) {
|
||||
return hrZonesSeconds[zone].value();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t bluetoothdevice::secondsForHeatZone(uint8_t zone) {
|
||||
if(zone < maxHeatZone()) {
|
||||
return heatZonesSeconds[zone].value();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -146,6 +146,11 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
virtual QTime lapElapsedTime();
|
||||
|
||||
/**
|
||||
* @brief lapOdometer Gets the distance elapsed on the current lap.
|
||||
*/
|
||||
virtual double lapOdometer();
|
||||
|
||||
/**
|
||||
* @brief connected Gets a value to indicate if the device is connected.
|
||||
*/
|
||||
@@ -365,6 +370,24 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
uint32_t secondsForHeartZone(uint8_t zone);
|
||||
|
||||
/**
|
||||
* @brief currentHeatZone Gets a metric object to get or set the current heat zone. Units: depends on
|
||||
* implementation (based on Heat Strain Index: Zone 1: 0-1.99, Zone 2: 2-2.99, Zone 3: 3-6.99, Zone 4: 7+)
|
||||
*/
|
||||
metric currentHeatZone() { return HeatZone; }
|
||||
|
||||
/**
|
||||
* @brief maxHeatZone Gets the maximum number of heat zones.
|
||||
*/
|
||||
uint8_t maxHeatZone() { return maxheatzone; }
|
||||
|
||||
/**
|
||||
* @brief secondsForHeatZone Gets the number of seconds in the current heat zone.
|
||||
*
|
||||
* @param zone The heat zone.
|
||||
*/
|
||||
uint32_t secondsForHeatZone(uint8_t zone);
|
||||
|
||||
/**
|
||||
* @brief currentPowerZone Gets a metric object to get or set the current power zome. Units: depends on
|
||||
* implementation.
|
||||
@@ -399,6 +422,13 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
void setHeartZone(double hz);
|
||||
|
||||
/**
|
||||
* @brief setHeatZone Set the current heat zone based on Heat Strain Index.
|
||||
* This is equivalent to currentHeatZone().setvalue(hz)
|
||||
* @param heatStrainIndex The heat strain index to determine zone. Unit: depends on implementation.
|
||||
*/
|
||||
void setHeatZone(double heatStrainIndex);
|
||||
|
||||
/**
|
||||
* @brief setPowerZone Set the current power zone.
|
||||
* This is equivalent to currentPowerZone().setvalue(pz)
|
||||
@@ -413,7 +443,7 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
void setTargetPowerZone(double pz) { TargetPowerZone = pz; }
|
||||
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE };
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
|
||||
enum WORKOUT_EVENT_STATE { STARTED = 0, PAUSED = 1, RESUMED = 2, STOPPED = 3 };
|
||||
|
||||
/**
|
||||
@@ -438,6 +468,11 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
virtual resistance_t maxResistance();
|
||||
|
||||
// Metrics for core temperature data
|
||||
metric CoreBodyTemperature; // Core body temperature in °C or °F
|
||||
metric SkinTemperature; // Skin temperature in °C or °F
|
||||
metric HeatStrainIndex; // Heat Strain Index (0-25.4, scaled by 10)
|
||||
|
||||
public Q_SLOTS:
|
||||
virtual void start();
|
||||
virtual void stop(bool pause);
|
||||
@@ -445,6 +480,9 @@ class bluetoothdevice : public QObject {
|
||||
virtual void cadenceSensor(uint8_t cadence);
|
||||
virtual void powerSensor(uint16_t power);
|
||||
virtual void speedSensor(double speed);
|
||||
virtual void coreBodyTemperature(double coreBodyTemperature);
|
||||
virtual void skinTemperature(double skinTemperature);
|
||||
virtual void heatStrainIndex(double heatStrainIndex);
|
||||
virtual void inclinationSensor(double grade, double inclination);
|
||||
virtual void changeResistance(resistance_t res);
|
||||
virtual void changePower(int32_t power);
|
||||
@@ -581,9 +619,14 @@ class bluetoothdevice : public QObject {
|
||||
metric elevationAcc;
|
||||
|
||||
/**
|
||||
* @brief m_watt Metric to get and set the power expended in the session. Unit: watts
|
||||
* @brief m_watt Metric to get and set the power read from the trainer or from the power sensor Unit: watts
|
||||
*/
|
||||
metric m_watt;
|
||||
|
||||
/**
|
||||
* @brief m_rawWatt Metric to get and set the power from the trainer only. Unit: watts
|
||||
*/
|
||||
metric m_rawWatt;
|
||||
|
||||
/**
|
||||
* @brief WattKg Metric to get and set the watt kg for the session (what's this?). Unit: watt kg
|
||||
@@ -663,6 +706,11 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
metric HeartZone;
|
||||
|
||||
/**
|
||||
* @brief HeatZone A metric to get and set the current heat zone. Unit: depends on implementation
|
||||
*/
|
||||
metric HeatZone;
|
||||
|
||||
/**
|
||||
* @brief PowerZone A metric to get and set the current power zone. Unit: depends on implementation
|
||||
*/
|
||||
@@ -689,6 +737,12 @@ class bluetoothdevice : public QObject {
|
||||
static const uint8_t maxhrzone = 5;
|
||||
metric hrZonesSeconds[maxhrzone];
|
||||
|
||||
/**
|
||||
* @brief Collect the number of seconds in each zone for the current heat strain index
|
||||
*/
|
||||
static const uint8_t maxheatzone = 4;
|
||||
metric heatZonesSeconds[maxheatzone];
|
||||
|
||||
bluetoothdevice::WORKOUT_EVENT_STATE lastState;
|
||||
|
||||
/**
|
||||
|
||||
@@ -199,10 +199,20 @@ void bowflext216treadmill::characteristicChanged(const QLowEnergyCharacteristic
|
||||
else if ((newValue.length() != 12) && bowflex_t8j == true)
|
||||
return;
|
||||
|
||||
if (bowflex_t6 == true && characteristic.uuid() != QBluetoothUuid(QStringLiteral("a46a4a80-9803-11e3-8f3c-0002a5d5c51b")))
|
||||
if(bowflex_T128 && characteristic.uuid() != QBluetoothUuid(QStringLiteral("a46a4a80-9803-11e3-8f3c-0002a5d5c51b"))) {
|
||||
double incline = GetInclinationFromPacket(value);
|
||||
qDebug() << QStringLiteral("Current incline: ") << incline;
|
||||
if (Inclination.value() != incline) {
|
||||
emit inclinationChanged(0.0, incline);
|
||||
}
|
||||
Inclination = incline;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((bowflex_t6 == true || bowflex_T128 == true) && characteristic.uuid() != QBluetoothUuid(QStringLiteral("a46a4a80-9803-11e3-8f3c-0002a5d5c51b")))
|
||||
return;
|
||||
|
||||
double speed = GetSpeedFromPacket(value);
|
||||
double speed = GetSpeedFromPacket(value);
|
||||
double incline = GetInclinationFromPacket(value);
|
||||
// double kcal = GetKcalFromPacket(value);
|
||||
// double distance = GetDistanceFromPacket(value);
|
||||
@@ -226,10 +236,13 @@ void bowflext216treadmill::characteristicChanged(const QLowEnergyCharacteristic
|
||||
emit speedChanged(speed);
|
||||
}
|
||||
Speed = speed;
|
||||
if (Inclination.value() != incline) {
|
||||
emit inclinationChanged(0.0, incline);
|
||||
|
||||
if(bowflex_T128 == false) {
|
||||
if (Inclination.value() != incline) {
|
||||
emit inclinationChanged(0.0, incline);
|
||||
}
|
||||
Inclination = incline;
|
||||
}
|
||||
Inclination = incline;
|
||||
|
||||
// KCal = kcal;
|
||||
// Distance = distance;
|
||||
@@ -271,6 +284,10 @@ double bowflext216treadmill::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(3)) + ((uint16_t)((uint8_t)packet.at(4)) << 8);
|
||||
double data = (double)convertedData / 100.0f;
|
||||
return data * 1.60934;
|
||||
} else if(bowflex_T128) {
|
||||
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 * 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;
|
||||
@@ -294,6 +311,12 @@ double bowflext216treadmill::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
}
|
||||
|
||||
double bowflext216treadmill::GetInclinationFromPacket(const QByteArray &packet) {
|
||||
if (bowflex_T128) {
|
||||
uint16_t convertedData = packet.at(7);
|
||||
double data = convertedData;
|
||||
|
||||
return data;
|
||||
}
|
||||
if (bowflex_t8j) {
|
||||
uint16_t convertedData = packet.at(6);
|
||||
double data = convertedData;
|
||||
@@ -447,6 +470,12 @@ void bowflext216treadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
|
||||
device.address().toString() + ')');
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
|
||||
if(bluetoothDevice.name().toUpper().startsWith("BOWFLEX T128")) {
|
||||
qDebug() << "BOWFLEX T128 workaround found!";
|
||||
bowflex_T128 = true;
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &bowflext216treadmill::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &bowflext216treadmill::serviceScanDone);
|
||||
|
||||
@@ -75,6 +75,7 @@ class bowflext216treadmill : public treadmill {
|
||||
bool bowflex_t6 = false;
|
||||
bool bowflex_btx116 = false;
|
||||
bool bowflex_t8j = false;
|
||||
bool bowflex_T128 = false;
|
||||
|
||||
Q_SIGNALS:
|
||||
void disconnected();
|
||||
|
||||
456
src/devices/coresensor/coresensor.cpp
Normal file
456
src/devices/coresensor/coresensor.cpp
Normal file
@@ -0,0 +1,456 @@
|
||||
#include "coresensor.h"
|
||||
#include "homeform.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
|
||||
coresensor::coresensor() :
|
||||
m_sensorHeartRate(0),
|
||||
m_dataQuality(0),
|
||||
m_heartRateState(0),
|
||||
m_isCelsius(true) {
|
||||
}
|
||||
|
||||
coresensor::~coresensor() {
|
||||
disconnectBluetooth();
|
||||
}
|
||||
|
||||
bool coresensor::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
metric coresensor::currentHeart() {
|
||||
// If we have a valid value from the sensor, create a metric with that value
|
||||
if (m_sensorHeartRate > 0) {
|
||||
metric heartMetric;
|
||||
heartMetric.setValue(m_sensorHeartRate);
|
||||
return heartMetric;
|
||||
}
|
||||
|
||||
// Otherwise return the value from the base class
|
||||
return bluetoothdevice::currentHeart();
|
||||
}
|
||||
|
||||
void coresensor::update() {
|
||||
// This method can be used for periodic tasks or calculations
|
||||
QSettings settings;
|
||||
// Future implementation if needed
|
||||
}
|
||||
|
||||
void coresensor::disconnectBluetooth() {
|
||||
qDebug() << QStringLiteral("coresensor::disconnect") << m_control;
|
||||
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void coresensor::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
QSettings settings;
|
||||
|
||||
qDebug() << QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')';
|
||||
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested(device.name() + QStringLiteral(" connected!"));
|
||||
|
||||
// We might filter the device name if needed
|
||||
// For example: if (device.name().contains("CORE") || device.name().contains("CoreTemp"))
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &coresensor::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &coresensor::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &coresensor::handleError);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &coresensor::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("Cannot connect to remote device.");
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("Controller connected. Search services...");
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("LowEnergy controller disconnected");
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void coresensor::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
|
||||
}
|
||||
|
||||
void coresensor::serviceScanDone() {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
qDebug() << QStringLiteral("coresensor services ") << s.toString();
|
||||
|
||||
// Look for the specific CORE sensor service
|
||||
if (s == QBluetoothUuid(QString(CORE_SERVICE_UUID))) {
|
||||
QBluetoothUuid coreServiceId(QString(CORE_SERVICE_UUID));
|
||||
m_coreService = m_control->createServiceObject(coreServiceId);
|
||||
connect(m_coreService, &QLowEnergyService::stateChanged, this,
|
||||
&coresensor::serviceStateChanged);
|
||||
connect(m_coreService,
|
||||
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &coresensor::handleServiceError);
|
||||
m_coreService->discoverDetails();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, we didn't find the CORE service
|
||||
qDebug() << QStringLiteral("CORE service not found!");
|
||||
}
|
||||
|
||||
void coresensor::serviceStateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
auto characteristics_list = m_coreService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("characteristic ") + c.uuid().toString();
|
||||
}
|
||||
|
||||
// Find the Core Body Temperature characteristic
|
||||
m_coreTemperatureCharacteristic =
|
||||
m_coreService->characteristic(QBluetoothUuid(QString(CORE_TEMPERATURE_CHAR_UUID)));
|
||||
|
||||
if (!m_coreTemperatureCharacteristic.isValid()) {
|
||||
qDebug() << "Core Body Temperature characteristic not valid";
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the Control Point characteristic
|
||||
m_coreControlPointCharacteristic =
|
||||
m_coreService->characteristic(QBluetoothUuid(QString(CORE_CONTROL_POINT_UUID)));
|
||||
|
||||
if (!m_coreControlPointCharacteristic.isValid()) {
|
||||
qDebug() << "Core Control Point characteristic not valid";
|
||||
// We can continue without control point if only reading temperature
|
||||
}
|
||||
|
||||
// Set up notifications and handle characteristic changes
|
||||
connect(m_coreService, &QLowEnergyService::characteristicChanged, this,
|
||||
&coresensor::characteristicChanged);
|
||||
connect(m_coreService, &QLowEnergyService::characteristicWritten, this,
|
||||
&coresensor::characteristicWritten);
|
||||
connect(m_coreService, &QLowEnergyService::descriptorWritten, this,
|
||||
&coresensor::descriptorWritten);
|
||||
|
||||
// Enable notifications for Core Body Temperature characteristic
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
m_coreService->writeDescriptor(
|
||||
m_coreTemperatureCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration),
|
||||
descriptor);
|
||||
|
||||
// For Control Point, we need to enable indications (not notifications)
|
||||
if (m_coreControlPointCharacteristic.isValid()) {
|
||||
QByteArray cpDescriptor;
|
||||
cpDescriptor.append((char)0x02); // 0x02 for indications (different from notifications)
|
||||
cpDescriptor.append((char)0x00);
|
||||
m_coreService->writeDescriptor(
|
||||
m_coreControlPointCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration),
|
||||
cpDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void coresensor::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
emit packetReceived();
|
||||
|
||||
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
|
||||
|
||||
// Handle Core Body Temperature characteristic updates
|
||||
if (characteristic.uuid() == QBluetoothUuid(QString(CORE_TEMPERATURE_CHAR_UUID))) {
|
||||
if (newValue.length() < 1) {
|
||||
qDebug() << "Received invalid data packet (too short)";
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse flags field (first byte)
|
||||
uint8_t flags = newValue.at(0);
|
||||
bool hasSkinTemp = (flags & 0x01);
|
||||
bool hasCoreReserved = (flags & 0x02);
|
||||
bool hasQualityState = (flags & 0x04);
|
||||
bool isFahrenheit = (flags & 0x08);
|
||||
bool hasHeartRate = (flags & 0x10);
|
||||
bool hasHeatStrainIndex = (flags & 0x20);
|
||||
|
||||
// Update temperature unit flag
|
||||
bool newIsCelsius = !isFahrenheit;
|
||||
if (m_isCelsius != newIsCelsius) {
|
||||
m_isCelsius = newIsCelsius;
|
||||
emit isCelsiusChanged(m_isCelsius);
|
||||
}
|
||||
|
||||
int offset = 1; // Start after flags byte
|
||||
|
||||
// Core body temperature (mandatory)
|
||||
if (newValue.length() >= (offset + 2)) {
|
||||
int16_t tempRaw = (int16_t)((uint8_t)newValue[offset] | ((uint8_t)newValue[offset+1] << 8));
|
||||
|
||||
// Special value for 'Data not available'
|
||||
if (tempRaw == 0x7FFF) {
|
||||
qDebug() << "Core body temperature: Data not available";
|
||||
} else {
|
||||
double temp = tempRaw / 100.0; // Convert from 0.01°C/F to °C/F
|
||||
CoreBodyTemperature.setValue(temp);
|
||||
emit coreBodyTemperatureChanged(temp);
|
||||
}
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
// Skin temperature (optional)
|
||||
if (hasSkinTemp && newValue.length() >= (offset + 2)) {
|
||||
int16_t tempRaw = (int16_t)((uint8_t)newValue[offset] | ((uint8_t)newValue[offset+1] << 8));
|
||||
double temp = tempRaw / 100.0; // Convert from 0.01°C/F to °C/F
|
||||
SkinTemperature.setValue(temp);
|
||||
emit skinTemperatureChanged(temp);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
// Core reserved (optional)
|
||||
if (hasCoreReserved && newValue.length() >= (offset + 2)) {
|
||||
int16_t tempRaw = (int16_t)((uint8_t)newValue[offset] | ((uint8_t)newValue[offset+1] << 8));
|
||||
double temp = tempRaw / 100.0; // Convert from 0.01°C/F to °C/F
|
||||
emit coreReservedChanged(temp);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
// Quality and state (optional)
|
||||
if (hasQualityState && newValue.length() >= (offset + 1)) {
|
||||
uint8_t qualityState = newValue.at(offset);
|
||||
int newDataQuality = qualityState & 0x0F; // Lower 4 bits, actually using only 3 bits
|
||||
int newHeartRateState = (qualityState >> 4) & 0x03; // Bits 4-5
|
||||
|
||||
if (m_dataQuality != newDataQuality) {
|
||||
m_dataQuality = newDataQuality;
|
||||
emit dataQualityChanged(m_dataQuality);
|
||||
}
|
||||
|
||||
if (m_heartRateState != newHeartRateState) {
|
||||
m_heartRateState = newHeartRateState;
|
||||
emit heartRateStateChanged(m_heartRateState);
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// Heart rate (optional)
|
||||
if (hasHeartRate && newValue.length() >= (offset + 1)) {
|
||||
uint8_t hr = newValue.at(offset);
|
||||
if (m_sensorHeartRate != hr) {
|
||||
m_sensorHeartRate = hr;
|
||||
emit heartRateChanged(m_sensorHeartRate);
|
||||
|
||||
// Forward heart rate to base class if it's a valid value
|
||||
if (hr > 0) {
|
||||
bluetoothdevice::heartRate(hr);
|
||||
}
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// Heat Strain Index (optional)
|
||||
if (hasHeatStrainIndex && newValue.length() >= (offset + 1)) {
|
||||
uint8_t hsiRaw = newValue.at(offset);
|
||||
double hsi = hsiRaw / 10.0; // Convert from 0.1 units to actual value
|
||||
if (HeatStrainIndex.value() != hsi) {
|
||||
HeatStrainIndex = hsi;
|
||||
emit heatStrainIndexChanged(HeatStrainIndex.value());
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// Log data for debugging
|
||||
QString logMsg = QStringLiteral("CORE Sensor: ");
|
||||
logMsg += QStringLiteral("Temperature: ") + QString::number(CoreBodyTemperature.value(), 'f', 2) +
|
||||
(m_isCelsius ? "°C" : "°F");
|
||||
|
||||
if (hasSkinTemp) {
|
||||
logMsg += QStringLiteral(" Skin: ") + QString::number(SkinTemperature.value(), 'f', 2) +
|
||||
(m_isCelsius ? "°C" : "°F");
|
||||
}
|
||||
|
||||
if (hasHeartRate) {
|
||||
logMsg += QStringLiteral(" HR: ") + QString::number(m_sensorHeartRate) + QStringLiteral(" BPM");
|
||||
}
|
||||
|
||||
if (hasHeatStrainIndex) {
|
||||
logMsg += QStringLiteral(" HSI: ") + QString::number(HeatStrainIndex.value(), 'f', 1);
|
||||
}
|
||||
|
||||
qDebug() << logMsg;
|
||||
|
||||
} else if (characteristic.uuid() == QBluetoothUuid(QString(CORE_CONTROL_POINT_UUID))) {
|
||||
// Handle Control Point responses
|
||||
handleControlPointResponse(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
void coresensor::handleControlPointResponse(const QByteArray &value) {
|
||||
if (value.length() < 3) {
|
||||
qDebug() << "Invalid Control Point response (too short)";
|
||||
return;
|
||||
}
|
||||
|
||||
// First byte should be 0x80 (response code)
|
||||
if ((uint8_t)value.at(0) != 0x80) {
|
||||
qDebug() << "Invalid Control Point response (expected 0x80 response code)";
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t requestOpCode = value.at(1);
|
||||
uint8_t resultCode = value.at(2);
|
||||
|
||||
QString result;
|
||||
switch (resultCode) {
|
||||
case 0x01: result = "Success"; break;
|
||||
case 0x02: result = "Op Code not supported"; break;
|
||||
case 0x03: result = "Invalid Parameter"; break;
|
||||
case 0x04: result = "Operation Failed"; break;
|
||||
default: result = "Unknown result code"; break;
|
||||
}
|
||||
|
||||
qDebug() << "Control Point response for OpCode" << QString("0x%1").arg(requestOpCode, 2, 16, QChar('0'))
|
||||
<< "Result:" << result;
|
||||
|
||||
m_operationInProgress = false;
|
||||
}
|
||||
|
||||
void coresensor::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
qDebug() << QStringLiteral("characteristicWritten ") + newValue.toHex(' ');
|
||||
}
|
||||
|
||||
void coresensor::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
qDebug() << QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ');
|
||||
}
|
||||
|
||||
void coresensor::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void coresensor::handleError(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
qDebug() << QStringLiteral("coresensor::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString();
|
||||
}
|
||||
|
||||
void coresensor::handleServiceError(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
qDebug() << QStringLiteral("coresensor::serviceError ") + QString::fromLocal8Bit(metaEnum.valueToKey(err));
|
||||
}
|
||||
|
||||
bool coresensor::setExternalHeartRate(uint8_t bpm) {
|
||||
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray request;
|
||||
request.append((char)0x13); // OpCode for external heart rate
|
||||
request.append((char)bpm); // Heart rate value
|
||||
|
||||
m_operationInProgress = true;
|
||||
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool coresensor::disableExternalHeartRate() {
|
||||
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray request;
|
||||
request.append((char)0x13); // OpCode for external heart rate
|
||||
// No additional data means disable external heart rate
|
||||
|
||||
m_operationInProgress = true;
|
||||
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool coresensor::scanForBLEHeartRateMonitors() {
|
||||
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray request;
|
||||
request.append((char)0x0D); // OpCode for scan BLE HRMs
|
||||
request.append((char)0xFF); // Parameter for starting scan
|
||||
|
||||
m_operationInProgress = true;
|
||||
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool coresensor::scanForANTHeartRateMonitors() {
|
||||
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray request;
|
||||
request.append((char)0x0A); // OpCode for scan ANT+ HRMs
|
||||
request.append((char)0xFF); // Parameter for starting scan
|
||||
|
||||
m_operationInProgress = true;
|
||||
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int coresensor::getNumberOfPairedHeartRateMonitors() {
|
||||
// This is a simple implementation that requests the number of paired BLE HRMs
|
||||
// In a complete implementation, we would make an async request and store the result
|
||||
|
||||
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
|
||||
return -1; // Error condition
|
||||
}
|
||||
|
||||
// Request total number of paired BLE heart rate monitors
|
||||
QByteArray request;
|
||||
request.append((char)0x08); // OpCode for get total number of paired BLE HRMs
|
||||
|
||||
m_operationInProgress = true;
|
||||
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
|
||||
|
||||
// Note: in a real implementation, we would need to handle the response asynchronously
|
||||
// and store the result for a getter to access. This would require additional state tracking.
|
||||
|
||||
return 0; // Placeholder value - in reality, would return cached value from last request
|
||||
}
|
||||
196
src/devices/coresensor/coresensor.h
Normal file
196
src/devices/coresensor/coresensor.h
Normal file
@@ -0,0 +1,196 @@
|
||||
#ifndef CORESENSOR_H
|
||||
#define CORESENSOR_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "bluetoothdevice.h"
|
||||
|
||||
// UUID costanti per CORE Sensor secondo le specifiche
|
||||
#define CORE_SERVICE_UUID "00002100-5B1E-4347-B07C-97B514DAE121"
|
||||
#define CORE_TEMPERATURE_CHAR_UUID "00002101-5B1E-4347-B07C-97B514DAE121"
|
||||
#define CORE_CONTROL_POINT_UUID "00002102-5B1E-4347-B07C-97B514DAE121"
|
||||
|
||||
// Flag bit definitions for Core Body Temperature characteristic
|
||||
namespace CoreSensorFlags {
|
||||
const uint8_t SKIN_TEMPERATURE_PRESENT = 0x01;
|
||||
const uint8_t CORE_RESERVED_PRESENT = 0x02;
|
||||
const uint8_t QUALITY_STATE_PRESENT = 0x04;
|
||||
const uint8_t TEMPERATURE_UNIT_FAHRENHEIT = 0x08;
|
||||
const uint8_t HEART_RATE_PRESENT = 0x10;
|
||||
const uint8_t HEAT_STRAIN_INDEX_PRESENT = 0x20;
|
||||
}
|
||||
|
||||
// Data quality values
|
||||
namespace CoreSensorQuality {
|
||||
const uint8_t INVALID = 0;
|
||||
const uint8_t POOR = 1;
|
||||
const uint8_t FAIR = 2;
|
||||
const uint8_t GOOD = 3;
|
||||
const uint8_t EXCELLENT = 4;
|
||||
const uint8_t NOT_AVAILABLE = 7;
|
||||
}
|
||||
|
||||
// Heart rate state values
|
||||
namespace CoreSensorHRState {
|
||||
const uint8_t NOT_SUPPORTED = 0;
|
||||
const uint8_t SUPPORTED_NOT_RECEIVING = 1;
|
||||
const uint8_t SUPPORTED_RECEIVING = 2;
|
||||
const uint8_t NOT_AVAILABLE = 3;
|
||||
}
|
||||
|
||||
// Control Point OpCodes
|
||||
namespace CoreControlPoint {
|
||||
// ANT+ Related OpCodes
|
||||
const uint8_t CLEAR_ANT_PAIRED_HRM_LIST = 0x01;
|
||||
const uint8_t ADD_ANT_HRM = 0x02;
|
||||
const uint8_t REMOVE_ANT_HRM = 0x03;
|
||||
const uint8_t GET_TOTAL_ANT_PAIRED_HRMS = 0x04;
|
||||
const uint8_t GET_ANT_HRM_ID_AT_INDEX = 0x05;
|
||||
const uint8_t SCAN_ANT_HRMS = 0x0A;
|
||||
const uint8_t GET_TOTAL_SCANNED_ANT_HRMS = 0x0B;
|
||||
const uint8_t GET_SCANNED_ANT_HRM_ID = 0x0C;
|
||||
|
||||
// BLE Related OpCodes
|
||||
const uint8_t ADD_BLE_HRM = 0x06;
|
||||
const uint8_t REMOVE_BLE_HRM = 0x07;
|
||||
const uint8_t GET_TOTAL_BLE_PAIRED_HRMS = 0x08;
|
||||
const uint8_t GET_BLE_HRM_NAME_STATE = 0x09;
|
||||
const uint8_t SCAN_BLE_HRMS = 0x0D;
|
||||
const uint8_t GET_TOTAL_SCANNED_BLE_HRMS = 0x0E;
|
||||
const uint8_t GET_SCANNED_BLE_HRM_NAME = 0x0F;
|
||||
const uint8_t GET_SCANNED_BLE_HRM_MAC = 0x10;
|
||||
const uint8_t CLEAR_BLE_PAIRED_HRM_LIST = 0x11;
|
||||
const uint8_t GET_BLE_HRM_MAC_STATE = 0x12;
|
||||
|
||||
// External heart rate input
|
||||
const uint8_t EXTERNAL_HEART_RATE = 0x13;
|
||||
|
||||
// Response code (sent by sensor)
|
||||
const uint8_t RESPONSE_CODE = 0x80;
|
||||
|
||||
// Scan parameters
|
||||
const uint8_t START_SCAN = 0xFF;
|
||||
const uint8_t START_PROXIMITY_PAIRING = 0xFE;
|
||||
|
||||
// Result codes
|
||||
const uint8_t SUCCESS = 0x01;
|
||||
const uint8_t OP_CODE_NOT_SUPPORTED = 0x02;
|
||||
const uint8_t INVALID_PARAMETER = 0x03;
|
||||
const uint8_t OPERATION_FAILED = 0x04;
|
||||
}
|
||||
|
||||
// Struttura per la qualità e lo stato
|
||||
struct QualityAndState {
|
||||
enum DataQuality {
|
||||
INVALID = 0,
|
||||
POOR = 1,
|
||||
FAIR = 2,
|
||||
GOOD = 3,
|
||||
EXCELLENT = 4,
|
||||
NOT_AVAILABLE = 7
|
||||
};
|
||||
|
||||
enum HeartRateState {
|
||||
NOT_SUPPORTED = 0,
|
||||
SUPPORTED_NOT_RECEIVING = 1,
|
||||
SUPPORTED_RECEIVING = 2,
|
||||
NOT_AVAILABLE_STATE = 3
|
||||
};
|
||||
|
||||
DataQuality quality;
|
||||
HeartRateState hrState;
|
||||
};
|
||||
|
||||
class coresensor : public bluetoothdevice {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
coresensor();
|
||||
~coresensor();
|
||||
bool connected() override;
|
||||
|
||||
// Override from bluetoothdevice
|
||||
metric currentHeart() override;
|
||||
|
||||
private:
|
||||
QLowEnergyService *m_coreService = nullptr;
|
||||
QLowEnergyCharacteristic m_coreTemperatureCharacteristic;
|
||||
QLowEnergyCharacteristic m_coreControlPointCharacteristic;
|
||||
|
||||
// Other data from Core sensor
|
||||
uint8_t m_sensorHeartRate; // Heart rate in BPM
|
||||
int m_dataQuality; // Quality level (0=Invalid, 1=Poor, 2=Fair, 3=Good, 4=Excellent)
|
||||
int m_heartRateState; // Heart rate state
|
||||
bool m_isCelsius; // Temperature unit (true=°C, false=°F)
|
||||
|
||||
// Control point state tracking
|
||||
bool m_operationInProgress = false;
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void packetReceived();
|
||||
|
||||
// Signals for property change notifications
|
||||
void coreBodyTemperatureChanged(double value);
|
||||
void skinTemperatureChanged(double value);
|
||||
void coreReservedChanged(double value);
|
||||
void heartRateChanged(double value);
|
||||
void heatStrainIndexChanged(double value);
|
||||
void dataQualityChanged(int value);
|
||||
void heartRateStateChanged(int value);
|
||||
void isCelsiusChanged(bool value);
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
void disconnectBluetooth();
|
||||
|
||||
// Control point operations
|
||||
bool setExternalHeartRate(uint8_t bpm);
|
||||
bool disableExternalHeartRate();
|
||||
|
||||
// Heart rate monitor operations - these depend on implementation
|
||||
bool scanForBLEHeartRateMonitors();
|
||||
bool scanForANTHeartRateMonitors();
|
||||
int getNumberOfPairedHeartRateMonitors();
|
||||
|
||||
private slots:
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void serviceStateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void handleError(QLowEnergyController::Error err);
|
||||
void handleServiceError(QLowEnergyService::ServiceError err);
|
||||
|
||||
// Handle control point responses
|
||||
void handleControlPointResponse(const QByteArray &value);
|
||||
};
|
||||
|
||||
#endif // CORESENSOR_H
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "devices/dircon/dirconmanager.h"
|
||||
#include "devices/bike.h"
|
||||
#include <QNetworkInterface>
|
||||
#include <QSettings>
|
||||
#include <chrono>
|
||||
@@ -8,13 +9,17 @@ using namespace std::chrono_literals;
|
||||
#define DM_MACHINE_TYPE_BIKE 1
|
||||
#define DM_MACHINE_TYPE_TREADMILL 2
|
||||
|
||||
#define DM_SERV_OP(OP, P1, P2, P3) \
|
||||
#define DM_SERV_OP(OP, P1, P2, P3, ZWIFT_ENABLED) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x1826, WAHOO_KICKR, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_TREADMILL, 0x1826, WAHOO_TREADMILL, P1, P2, P3) \
|
||||
OP(CYCLING_POWER, 0x1818, WAHOO_KICKR, P1, P2, P3) \
|
||||
OP(CYCLING_SPEED_AND_CADENCE, 0x1816, WAHOO_KICKR, P1, P2, P3) \
|
||||
OP(RUNNING_SPEED_AND_CADENCE, 0x1814, WAHOO_TREADMILL, P1, P2, P3) \
|
||||
OP(HEART_RATE, 0x180D, WAHOO_BLUEHR, P1, P2, P3)
|
||||
OP(HEART_RATE, 0x180D, WAHOO_BLUEHR, P1, P2, P3) \
|
||||
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, ZWIFT_PLAY_ENUM_VALUE, WAHOO_KICKR, P1, P2, P3)
|
||||
|
||||
#define ZWIFT_ENABLED_OP(OP, DESC, UUID, MACHINE, P1, P2, P3) OP(DESC, UUID, MACHINE, P1, P2, P3)
|
||||
#define ZWIFT_DISABLED_OP(OP, DESC, UUID, MACHINE, P1, P2, P3)
|
||||
|
||||
#define DM_MACHINE_OP(OP, P1, P2, P3) \
|
||||
OP(WAHOO_KICKR, "Wahoo KICKR $uuid_hex$", DM_MACHINE_TYPE_TREADMILL | DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
|
||||
@@ -22,6 +27,7 @@ using namespace std::chrono_literals;
|
||||
OP(WAHOO_RPM_SPEED, "Wahoo SPEED $uuid_hex$", DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
|
||||
OP(WAHOO_TREADMILL, "Wahoo TREAD $uuid_hex$", DM_MACHINE_TYPE_TREADMILL, P1, P2, P3)
|
||||
|
||||
#define DP_PROCESS_WRITE_0003() (zwift_play_emulator ? writeP0003 : 0)
|
||||
#define DP_PROCESS_WRITE_2AD9() writeP2AD9
|
||||
#define DP_PROCESS_WRITE_2AD9T() writeP2AD9
|
||||
#define DP_PROCESS_WRITE_E005() writePE005
|
||||
@@ -32,7 +38,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
#define DM_BT(A) QByteArrayLiteral(A)
|
||||
|
||||
#define DM_CHAR_OP(OP, P1, P2, P3) \
|
||||
#define DM_CHAR_OP(OP, P1, P2, P3, ZWIFT_ENABLED) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x2ACC, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x83\x14\x00\x00\x0C\xE0\x00\x00"), \
|
||||
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
|
||||
@@ -71,7 +77,15 @@ using namespace std::chrono_literals;
|
||||
P3) \
|
||||
OP(RUNNING_SPEED_AND_CADENCE, 0x2A53, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, \
|
||||
P3) \
|
||||
OP(HEART_RATE, 0x2A37, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3)
|
||||
OP(HEART_RATE, 0x2A37, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, 0x0003, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_0003, P1, P2, P3) \
|
||||
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, 0x0002, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, 0x0004, DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x02\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3)
|
||||
|
||||
#define ZWIFT_CHAR_ENABLED_OP(OP, DESC, UUID, TYPE, READV, WRITEP, P1, P2, P3) \
|
||||
OP(DESC, UUID, TYPE, READV, WRITEP, P1, P2, P3)
|
||||
|
||||
#define ZWIFT_CHAR_DISABLED_OP(OP, DESC, UUID, TYPE, READV, WRITEP, P1, P2, P3)
|
||||
|
||||
#define DM_MACHINE_ENUM_OP(DESC, NAME, TYPE, P1, P2, P3) DM_MACHINE_##DESC,
|
||||
|
||||
@@ -79,15 +93,22 @@ enum { DM_MACHINE_OP(DM_MACHINE_ENUM_OP, 0, 0, 0) };
|
||||
|
||||
#define DM_SERV_ENUMU_OP(DESC, UUID, MACHINE, P1, P2, P3) DM_SERV_U_##DESC = UUID,
|
||||
|
||||
enum { DM_SERV_OP(DM_SERV_ENUMU_OP, 0, 0, 0) };
|
||||
enum {
|
||||
DM_SERV_OP(DM_SERV_ENUMU_OP, 0, 0, 0, ZWIFT_ENABLED_OP)
|
||||
};
|
||||
|
||||
#define DM_SERV_ENUMM_OP(DESC, UUID, MACHINE, P1, P2, P3) DM_SERV_M_##DESC = DM_MACHINE_##MACHINE,
|
||||
|
||||
enum { DM_SERV_OP(DM_SERV_ENUMM_OP, 0, 0, 0) };
|
||||
enum {
|
||||
DM_SERV_OP(DM_SERV_ENUMM_OP, 0, 0, 0, ZWIFT_ENABLED_OP)
|
||||
};
|
||||
|
||||
#define DM_SERV_ENUMI_OP(DESC, UUID, MACHINE, P1, P2, P3) DM_SERV_I_##DESC,
|
||||
|
||||
enum { DM_SERV_OP(DM_SERV_ENUMI_OP, 0, 0, 0) DM_SERV_I_NUM };
|
||||
enum {
|
||||
DM_SERV_OP(DM_SERV_ENUMI_OP, 0, 0, 0, ZWIFT_ENABLED_OP)
|
||||
DM_SERV_I_NUM
|
||||
};
|
||||
|
||||
#define DM_CHAR_INIT_OP(SDESC, UUID, TYPE, READV, WRITEP, P1, P2, P3) \
|
||||
if (P1.size() <= DM_SERV_I_##SDESC) { \
|
||||
@@ -150,35 +171,55 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
|
||||
QSettings settings;
|
||||
DirconProcessorService *service;
|
||||
QList<DirconProcessorService *> services, proc_services;
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
bt = Bike;
|
||||
uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
|
||||
: DM_MACHINE_TYPE_BIKE;
|
||||
qDebug() << "Building Dircom Manager";
|
||||
uint16_t server_base_port =
|
||||
settings.value(QZSettings::dircon_server_base_port, QZSettings::default_dircon_server_base_port).toUInt();
|
||||
bool bike_wheel_revs = settings.value(QZSettings::bike_wheel_revs, QZSettings::default_bike_wheel_revs).toBool();
|
||||
bool zwift_play_emulator = settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool();
|
||||
|
||||
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_BUILD_OP, Bike, 0, 0)
|
||||
|
||||
writeP2AD9 = new CharacteristicWriteProcessor2AD9(bikeResistanceGain, bikeResistanceOffset, Bike, notif2AD9, this);
|
||||
writePE005 = new CharacteristicWriteProcessorE005(bikeResistanceGain, bikeResistanceOffset, Bike, this);
|
||||
DM_CHAR_OP(DM_CHAR_INIT_OP, services, service, 0)
|
||||
|
||||
writeP0003 = new CharacteristicWriteProcessor0003(bikeResistanceGain, bikeResistanceOffset, Bike, notif0002, notif0004, this);
|
||||
|
||||
if (zwift_play_emulator) {
|
||||
DM_CHAR_OP(DM_CHAR_INIT_OP, services, service, 0, ZWIFT_CHAR_ENABLED_OP);
|
||||
} else {
|
||||
DM_CHAR_OP(DM_CHAR_INIT_OP, services, service, 0, ZWIFT_CHAR_DISABLED_OP);
|
||||
}
|
||||
|
||||
connect(writeP2AD9, SIGNAL(changeInclination(double, double)), this, SIGNAL(changeInclination(double, double)));
|
||||
connect(writeP2AD9, SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)), this,
|
||||
SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)));
|
||||
connect(writePE005, SIGNAL(changeInclination(double, double)), this, SIGNAL(changeInclination(double, double)));
|
||||
connect(writePE005, SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)), this,
|
||||
SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)));
|
||||
|
||||
if (zwift_play_emulator && writeP0003) {
|
||||
connect(writeP0003, SIGNAL(changeInclination(double, double)), this, SIGNAL(changeInclination(double, double)));
|
||||
connect(writeP0003, SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)), this,
|
||||
SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)));
|
||||
}
|
||||
|
||||
QObject::connect(&bikeTimer, &QTimer::timeout, this, &DirconManager::bikeProvider);
|
||||
QString mac = getMacAddress();
|
||||
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
|
||||
if (settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool())
|
||||
bikeTimer.start(100ms);
|
||||
|
||||
if (zwift_play_emulator || settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool())
|
||||
bikeTimer.start(50ms);
|
||||
else
|
||||
bikeTimer.start(1s);
|
||||
}
|
||||
|
||||
#define DM_CHAR_NOTIF_NOTIF1_OP(UUID, P1, P2, P3) \
|
||||
QByteArray all##UUID; \
|
||||
int rv##UUID = notif##UUID->notify(all##UUID);
|
||||
int rv##UUID = notif##UUID ? notif##UUID->notify(all##UUID) : 0;
|
||||
|
||||
#define DM_CHAR_NOTIF_NOTIF2_OP(UUID, P1, P2, P3) \
|
||||
if (rv##UUID == CN_OK) \
|
||||
@@ -186,5 +227,16 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
|
||||
|
||||
void DirconManager::bikeProvider() {
|
||||
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_NOTIF1_OP, 0, 0, 0)
|
||||
foreach (DirconProcessor *processor, processors) { DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_NOTIF2_OP, processor, 0, 0) }
|
||||
foreach (DirconProcessor *processor, processors) {
|
||||
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_NOTIF2_OP, processor, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return ((bike*)bt)->gears();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "characteristics/characteristicnotifier2ad9.h"
|
||||
#include "characteristics/characteristicwriteprocessor2ad9.h"
|
||||
#include "characteristics/characteristicwriteprocessore005.h"
|
||||
#include "characteristics/characteristicwriteprocessor0003.h"
|
||||
#include "devices/dircon/dirconpacket.h"
|
||||
#include "devices/dircon/dirconprocessor.h"
|
||||
#include <QObject>
|
||||
@@ -20,7 +21,8 @@
|
||||
OP(2AD2, P1, P2, P3) \
|
||||
OP(2A63, P1, P2, P3) \
|
||||
OP(2A37, P1, P2, P3) OP(2A5B, P1, P2, P3) OP(2A53, P1, P2, P3) OP(2ACD, P1, P2, P3) OP(2ACC, P1, P2, P3) \
|
||||
OP(2AD9, P1, P2, P3)
|
||||
OP(2AD9, P1, P2, P3) \
|
||||
OP(0002, P1, P2, P3) OP(0004, P1, P2, P3)
|
||||
|
||||
#define DM_CHAR_NOTIF_DEFINE_OP(UUID, P1, P2, P3) CharacteristicNotifier##UUID *notif##UUID = 0;
|
||||
|
||||
@@ -28,12 +30,15 @@ class DirconManager : public QObject {
|
||||
Q_OBJECT
|
||||
QTimer bikeTimer;
|
||||
CharacteristicWriteProcessor2AD9 *writeP2AD9 = 0;
|
||||
CharacteristicWriteProcessor0003 *writeP0003 = 0;
|
||||
CharacteristicWriteProcessorE005 *writePE005 = 0;
|
||||
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_DEFINE_OP, 0, 0, 0)
|
||||
QList<DirconProcessor *> processors;
|
||||
static QString getMacAddress();
|
||||
bluetoothdevice* bt = 0;
|
||||
|
||||
public:
|
||||
double currentGear();
|
||||
explicit DirconManager(bluetoothdevice *t, int8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0,
|
||||
QObject *parent = nullptr);
|
||||
private slots:
|
||||
|
||||
@@ -171,22 +171,40 @@ QByteArray DirconPacket::encode(int last_seq_number) {
|
||||
this->Length = this->uuids.size() * 16;
|
||||
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
|
||||
foreach (u, this->uuids) {
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
if(u >= 1 && u <= 4) {
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
|
||||
} else {
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this->Identifier == DPKT_MSGID_DISCOVER_CHARACTERISTICS && !this->isRequest) {
|
||||
this->Length = 16 + this->uuids.size() * 17;
|
||||
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
|
||||
u = this->uuid;
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
foreach (u, this->uuids) {
|
||||
if(u >= 1 && u <= 4) {
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
|
||||
} else {
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
}
|
||||
foreach (u, this->uuids) {
|
||||
if(u >= 1 && u <= 4) {
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
|
||||
} else {
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
}
|
||||
byteout.append(this->additional_data.at(i++));
|
||||
}
|
||||
} else if (((this->Identifier == DPKT_MSGID_READ_CHARACTERISTIC ||
|
||||
@@ -196,9 +214,15 @@ QByteArray DirconPacket::encode(int last_seq_number) {
|
||||
this->Length = 16;
|
||||
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
|
||||
u = this->uuid;
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
if(u >= 1 && u <= 4) {
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
|
||||
} else {
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
}
|
||||
} else if (this->Identifier == DPKT_MSGID_WRITE_CHARACTERISTIC ||
|
||||
this->Identifier == DPKT_MSGID_UNSOLICITED_CHARACTERISTIC_NOTIFICATION ||
|
||||
(this->Identifier == DPKT_MSGID_READ_CHARACTERISTIC && !this->isRequest) ||
|
||||
@@ -206,9 +230,15 @@ QByteArray DirconPacket::encode(int last_seq_number) {
|
||||
this->Length = 16 + this->additional_data.size();
|
||||
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
|
||||
u = this->uuid;
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
if(u >= 1 && u <= 4) {
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
|
||||
} else {
|
||||
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
|
||||
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
|
||||
byteout.append((char *)this->uuid_bytes, 16);
|
||||
}
|
||||
byteout.append(this->additional_data);
|
||||
}
|
||||
return byteout;
|
||||
|
||||
@@ -4,10 +4,20 @@
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
|
||||
#define ZWIFT_PLAY_ENUM_VALUE 0x0001
|
||||
#define ZWIFT_PLAY_UUID_STRING "00000001-19CA-4651-86E5-FA29DCDD09D1"
|
||||
#define ZWIFT_PLAY_CHAR1_ENUM_VALUE 0x0002
|
||||
#define ZWIFT_PLAY_CHAR2_ENUM_VALUE 0x0003
|
||||
#define ZWIFT_PLAY_CHAR3_ENUM_VALUE 0x0004
|
||||
#define ZWIFT_PLAY_CHAR1_UUID_STRING "00000002-19CA-4651-86E5-FA29DCDD09D1"
|
||||
#define ZWIFT_PLAY_CHAR2_UUID_STRING "00000003-19CA-4651-86E5-FA29DCDD09D1"
|
||||
#define ZWIFT_PLAY_CHAR3_UUID_STRING "00000004-19CA-4651-86E5-FA29DCDD09D1"
|
||||
|
||||
#define DPKT_MESSAGE_HEADER_LENGTH 6
|
||||
#define DPKT_CHAR_PROP_FLAG_READ 0x01
|
||||
#define DPKT_CHAR_PROP_FLAG_WRITE 0x02
|
||||
#define DPKT_CHAR_PROP_FLAG_NOTIFY 0x04
|
||||
#define DPKT_CHAR_PROP_FLAG_INDICATE 0x08
|
||||
#define DPKT_MSGID_ERROR 0xFF
|
||||
#define DPKT_MSGID_DISCOVER_SERVICES 0x01
|
||||
#define DPKT_MSGID_DISCOVER_CHARACTERISTICS 0x02
|
||||
@@ -51,6 +61,8 @@ class DirconPacket {
|
||||
private:
|
||||
quint8 uuid_bytes[16] = {0x00, 0x00, 0x18, 0x26, 0x00, 0x00, 0x10, 0x00,
|
||||
0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB};
|
||||
quint8 uuid_bytes_zwift_play[16] = {0x00, 0x00, 0x00, 0x04, 0x19, 0xCA, 0x46, 0x51,
|
||||
0x86, 0xE5, 0xFA, 0x29, 0xDC, 0xDD, 0x09, 0xD1};
|
||||
bool checkIsRequest(int last_seq_number);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,16 @@ DirconProcessor::DirconProcessor(const QList<DirconProcessorService *> &my_servi
|
||||
|
||||
DirconProcessor::~DirconProcessor() {}
|
||||
|
||||
QString DirconProcessor::convertUUIDFromUINT16ToString (quint16 uuid) {
|
||||
if(uuid == ZWIFT_PLAY_CHAR1_ENUM_VALUE)
|
||||
return ZWIFT_PLAY_CHAR1_UUID_STRING;
|
||||
if(uuid == ZWIFT_PLAY_CHAR2_ENUM_VALUE)
|
||||
return ZWIFT_PLAY_CHAR2_UUID_STRING;
|
||||
if(uuid == ZWIFT_PLAY_CHAR3_ENUM_VALUE)
|
||||
return ZWIFT_PLAY_CHAR3_UUID_STRING;
|
||||
return "";
|
||||
}
|
||||
|
||||
bool DirconProcessor::initServer() {
|
||||
qDebug() << "Initializing dircon tcp server for" << serverName;
|
||||
if (!server) {
|
||||
@@ -55,10 +65,16 @@ void DirconProcessor::initAdvertising() {
|
||||
mdnsService.addAttribute(QByteArrayLiteral("serial-number"), serialN.toUtf8());
|
||||
QString ble_uuids;
|
||||
int i = 0;
|
||||
foreach (DirconProcessorService *service, services)
|
||||
ble_uuids += QString(QStringLiteral(DP_BASE_UUID))
|
||||
.replace("u", QString(QStringLiteral("%1")).arg(service->uuid, 4, 16, QLatin1Char('0'))) +
|
||||
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
|
||||
foreach (DirconProcessorService *service, services) {
|
||||
if(service->uuid == ZWIFT_PLAY_ENUM_VALUE) {
|
||||
ble_uuids += ZWIFT_PLAY_UUID_STRING +
|
||||
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
|
||||
} else {
|
||||
ble_uuids += QString(QStringLiteral(DP_BASE_UUID))
|
||||
.replace("u", QString(QStringLiteral("%1")).arg(service->uuid, 4, 16, QLatin1Char('0'))) +
|
||||
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
|
||||
}
|
||||
}
|
||||
mdnsService.addAttribute(QByteArrayLiteral("ble-service-uuids"), ble_uuids.toUtf8());
|
||||
mdnsService.setPort(serverPort);
|
||||
mdnsProvider->update(mdnsService);
|
||||
@@ -209,7 +225,7 @@ bool DirconProcessor::sendCharacteristicNotification(quint16 uuid, const QByteAr
|
||||
pkt.uuid = uuid;
|
||||
for (QHash<QTcpSocket *, DirconProcessorClient *>::iterator i = clientsMap.begin(); i != clientsMap.end(); ++i) {
|
||||
client = i.value();
|
||||
if (client->char_notify.indexOf(uuid) >= 0 || !settings.value(QZSettings::wahoo_rgt_dircon, QZSettings::default_wahoo_rgt_dircon).toBool()) {
|
||||
/*if (client->char_notify.indexOf(uuid) >= 0 || !settings.value(QZSettings::wahoo_rgt_dircon, QZSettings::default_wahoo_rgt_dircon).toBool())*/ {
|
||||
socket = i.key();
|
||||
rvs = socket->write(pkt.encode(0)) < 0;
|
||||
if (rvs)
|
||||
@@ -252,7 +268,7 @@ void DirconProcessor::tcpDataAvailable() {
|
||||
qDebug() << "Sending resp for uuid" << serverName << ":" << resp;
|
||||
if (resp.Identifier != DPKT_MSGID_ERROR) {
|
||||
QByteArray byteout = resp.encode(pkt.SequenceNumber);
|
||||
if (byteout.size())
|
||||
if (byteout.size() && client && client->sock)
|
||||
client->sock->write(byteout);
|
||||
}
|
||||
} else if (rembuf >= 0) {
|
||||
@@ -261,7 +277,7 @@ void DirconProcessor::tcpDataAvailable() {
|
||||
resp.ResponseCode = DPKT_RESPCODE_UNEXPECTED_ERROR;
|
||||
resp.Identifier = pkt.Identifier;
|
||||
QByteArray byteout = resp.encode(pkt.SequenceNumber);
|
||||
if (byteout.size())
|
||||
if (byteout.size() && client && client->sock)
|
||||
client->sock->write(byteout);
|
||||
} else
|
||||
break;
|
||||
|
||||
@@ -84,6 +84,7 @@ class DirconProcessor : public QObject {
|
||||
bool initServer();
|
||||
void initAdvertising();
|
||||
DirconPacket processPacket(DirconProcessorClient *client, const DirconPacket &pkt);
|
||||
QString convertUUIDFromUINT16ToString (quint16 uuid);
|
||||
|
||||
public:
|
||||
~DirconProcessor();
|
||||
|
||||
@@ -353,20 +353,16 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
|
||||
and speed status return;*/
|
||||
|
||||
double speed = GetSpeedFromPacket(value);
|
||||
double kcal;
|
||||
if (!settings.value(QZSettings::kcal_ignore_builtin, QZSettings::default_kcal_ignore_builtin).toBool())
|
||||
kcal = GetKcalFromPacket(value);
|
||||
KCal = GetKcalFromPacket(value);
|
||||
else {
|
||||
if (watts())
|
||||
kcal =
|
||||
KCal.value() + ((((0.048 * ((double)watts()) + 1.19) *
|
||||
KCal += ((((0.048 * ((double)watts()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
|
||||
now)))); //(( (0.048* Output in watts +1.19) * body
|
||||
// weight in kg * 3.5) / 200 ) / 60
|
||||
else
|
||||
kcal = KCal.value();
|
||||
}
|
||||
|
||||
if(!firstCharacteristicChanged) {
|
||||
@@ -439,7 +435,7 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
|
||||
qDebug() << QStringLiteral("Current cadence: ") + QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current resistance: ") + QString::number(Resistance.value());
|
||||
qDebug() << QStringLiteral("Current heart: ") + QString::number(Heart.value());
|
||||
qDebug() << QStringLiteral("Current KCal: ") + QString::number(kcal);
|
||||
qDebug() << QStringLiteral("Current KCal: ") + QString::number(KCal.value());
|
||||
qDebug() << QStringLiteral("Current Distance: ") + QString::number(distance);
|
||||
qDebug() << QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs);
|
||||
qDebug() << QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime);
|
||||
@@ -456,7 +452,6 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
KCal = kcal;
|
||||
firstCharacteristicChanged = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@ void domyostreadmill::update() {
|
||||
if (requestInclination != -100 && lastInclinationChanged.secsTo(QDateTime::currentDateTime()) > inclination_delay_seconds) {
|
||||
lastInclinationChanged = QDateTime::currentDateTime();
|
||||
// only 0.5 steps ara available
|
||||
requestInclination = treadmillInclinationOverrideReverse(requestInclination);
|
||||
requestInclination = qRound(requestInclination * 2.0) / 2.0;
|
||||
inc = requestInclination;
|
||||
requestInclination = -100;
|
||||
@@ -641,7 +642,7 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
|
||||
and speed status return;*/
|
||||
|
||||
double speed = GetSpeedFromPacket(value);
|
||||
double incline = GetInclinationFromPacket(value);
|
||||
double incline = treadmillInclinationOverride(GetInclinationFromPacket(value));
|
||||
double kcal = GetKcalFromPacket(value);
|
||||
double distance = GetDistanceFromPacket(value);
|
||||
bool disable_hr_frommachinery =
|
||||
|
||||
@@ -37,28 +37,37 @@ echelonconnectsport::echelonconnectsport(bool noWriteResistance, bool noHeartSer
|
||||
|
||||
void echelonconnectsport::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response) {
|
||||
QSettings settings;
|
||||
bool useNativeIOS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
#endif
|
||||
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
// if there are some crash here, maybe it's better to use 2 separate event for the characteristicChanged.
|
||||
// one for the resistance changed event (spontaneous), and one for the other ones.
|
||||
if (wait_for_response) {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
if (!useNativeIOS) {
|
||||
// if there are some crash here, maybe it's better to use 2 separate event for the characteristicChanged.
|
||||
// one for the resistance changed event (spontaneous), and one for the other ones.
|
||||
if (wait_for_response) {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
qDebug() << QStringLiteral("writeCharacteristic error because the connection is closed");
|
||||
return;
|
||||
}
|
||||
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
qDebug() << QStringLiteral("writeCharacteristic error because the connection is closed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gattWriteCharacteristic.isValid()) {
|
||||
qDebug() << QStringLiteral("gattWriteCharacteristic is invalid");
|
||||
return;
|
||||
if (!gattWriteCharacteristic.isValid()) {
|
||||
qDebug() << QStringLiteral("gattWriteCharacteristic is invalid");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
@@ -66,14 +75,25 @@ void echelonconnectsport::writeCharacteristic(uint8_t *data, uint8_t data_len, c
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
#ifdef Q_OS_IOS
|
||||
if (useNativeIOS) {
|
||||
#ifndef IO_UNDER_QT
|
||||
iOS_echelonConnectSport->echelonConnectSport_WriteCharacteristic((unsigned char*)writeBuffer->data(), data_len);
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
}
|
||||
|
||||
if (!disable_log) {
|
||||
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info;
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
if (!useNativeIOS) {
|
||||
loop.exec();
|
||||
}
|
||||
}
|
||||
|
||||
void echelonconnectsport::forceResistance(resistance_t requestResistance) {
|
||||
@@ -105,7 +125,14 @@ void echelonconnectsport::sendPoll() {
|
||||
}
|
||||
|
||||
void echelonconnectsport::update() {
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
QSettings settings;
|
||||
bool useNativeIOS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
#endif
|
||||
|
||||
if (!useNativeIOS && m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
@@ -113,12 +140,17 @@ void echelonconnectsport::update() {
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
btinit();
|
||||
} else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
|
||||
gattCommunicationChannelService && gattWriteCharacteristic.isValid() &&
|
||||
gattNotify1Characteristic.isValid() && gattNotify2Characteristic.isValid() && initDone) {
|
||||
} else if ((useNativeIOS ||
|
||||
(bluetoothDevice.isValid() &&
|
||||
m_control->state() == QLowEnergyController::DiscoveredState &&
|
||||
gattCommunicationChannelService &&
|
||||
gattWriteCharacteristic.isValid() &&
|
||||
gattNotify1Characteristic.isValid() &&
|
||||
gattNotify2Characteristic.isValid())) &&
|
||||
initDone) {
|
||||
update_metrics(true, watts());
|
||||
|
||||
// sending poll every 2 seconds
|
||||
// sending poll every 2 seconds
|
||||
if (sec1Update++ >= (2000 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
sendPoll();
|
||||
@@ -140,7 +172,7 @@ void echelonconnectsport::update() {
|
||||
if (requestStart != -1) {
|
||||
qDebug() << QStringLiteral("starting...");
|
||||
|
||||
// btinit();
|
||||
// btinit();
|
||||
|
||||
requestStart = -1;
|
||||
emit bikeStarted();
|
||||
@@ -206,19 +238,24 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
bool useNativeIOS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
#endif
|
||||
|
||||
qDebug() << " << " + newValue.toHex(' ');
|
||||
|
||||
lastPacket = newValue;
|
||||
|
||||
// resistance value is in another frame
|
||||
// resistance value is in another frame
|
||||
if (newValue.length() == 5 && ((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd2) {
|
||||
resistance_t res = newValue.at(3);
|
||||
if (settings.value(QZSettings::gears_from_bike, QZSettings::default_gears_from_bike).toBool()) {
|
||||
qDebug() << QStringLiteral("gears_from_bike") << res << Resistance.value() << gears()
|
||||
<< lastRawRequestedResistanceValue << lastRequestedResistance().value();
|
||||
if (
|
||||
// if the resistance is different from the previous one
|
||||
// if the resistance is different from the previous one
|
||||
res != qRound(Resistance.value()) &&
|
||||
// and the last target resistance is different from the current one or there is no any pending last
|
||||
// requested resistance
|
||||
@@ -294,18 +331,18 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// these useless lines are needed to calculate the AVG resistance and AVG peloton resistance since
|
||||
// echelon just send the resistance values when it changes
|
||||
// these useless lines are needed to calculate the AVG resistance and AVG peloton resistance since
|
||||
// echelon just send the resistance values when it changes
|
||||
Resistance = Resistance.value();
|
||||
m_pelotonResistance = m_pelotonResistance.value();
|
||||
|
||||
@@ -318,7 +355,7 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
|
||||
qDebug() << QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime);
|
||||
qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts());
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError) {
|
||||
if (!useNativeIOS && m_control && m_control->error() != QLowEnergyController::NoError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
}
|
||||
}
|
||||
@@ -366,79 +403,90 @@ void echelonconnectsport::btinit() {
|
||||
}
|
||||
|
||||
void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66"));
|
||||
QBluetoothUuid _gattNotify2CharacteristicId(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66"));
|
||||
QSettings settings;
|
||||
bool useNativeIOS = false;
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
#endif
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
// qDebug() << gattCommunicationChannelService->characteristics();
|
||||
if (!useNativeIOS) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66"));
|
||||
QBluetoothUuid _gattNotify2CharacteristicId(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66"));
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
gattNotify2Characteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId);
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
Q_ASSERT(gattNotify2Characteristic.isValid());
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&echelonconnectsport::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&echelonconnectsport::characteristicWritten);
|
||||
connect(gattCommunicationChannelService,
|
||||
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &echelonconnectsport::errorService);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&echelonconnectsport::descriptorWritten);
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
// qDebug() << gattCommunicationChannelService->characteristics();
|
||||
|
||||
// ******************************************* virtual bike init *************************************
|
||||
if (!firstStateChanged && !this->hasVirtualDevice()
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
gattNotify2Characteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId);
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
Q_ASSERT(gattNotify2Characteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&echelonconnectsport::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&echelonconnectsport::characteristicWritten);
|
||||
connect(gattCommunicationChannelService,
|
||||
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &echelonconnectsport::errorService);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&echelonconnectsport::descriptorWritten);
|
||||
}
|
||||
}
|
||||
|
||||
// ******************************************* virtual bike init *************************************
|
||||
if (!firstStateChanged && !this->hasVirtualDevice()
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
&& !h
|
||||
&& !h
|
||||
#endif
|
||||
#endif
|
||||
) {
|
||||
QSettings settings;
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
QSettings settings;
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence) {
|
||||
qDebug() << "ios_peloton_workaround activated!";
|
||||
h = new lockscreen();
|
||||
h->virtualbike_ios();
|
||||
} else
|
||||
bool cadence =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence) {
|
||||
qDebug() << "ios_peloton_workaround activated!";
|
||||
h = new lockscreen();
|
||||
h->virtualbike_ios();
|
||||
} else
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
if (virtual_device_rower) {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("creating virtual bike interface...");
|
||||
auto virtualBike =
|
||||
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
if (virtual_device_enabled) {
|
||||
if (virtual_device_rower) {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("creating virtual bike interface...");
|
||||
auto virtualBike =
|
||||
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
// ********************************************************************************************************
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
if (!useNativeIOS && state == QLowEnergyService::ServiceDiscovered) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
@@ -465,17 +513,26 @@ void echelonconnectsport::characteristicWritten(const QLowEnergyCharacteristic &
|
||||
void echelonconnectsport::serviceScanDone(void) {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0bf669f1-45f2-11e7-9598-0800200c9a66"));
|
||||
QSettings settings;
|
||||
bool useNativeIOS = false;
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
|
||||
&echelonconnectsport::stateChanged);
|
||||
if(gattCommunicationChannelService != nullptr) {
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
} else {
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Bluetooth Service Error! Restart the bike!");
|
||||
m_control->disconnectFromDevice();
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
#endif
|
||||
|
||||
if (!useNativeIOS) {
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0bf669f1-45f2-11e7-9598-0800200c9a66"));
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
|
||||
&echelonconnectsport::stateChanged);
|
||||
if(gattCommunicationChannelService != nullptr) {
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
} else {
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("Bluetooth Service Error! Restart the bike!");
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +551,21 @@ void echelonconnectsport::error(QLowEnergyController::Error err) {
|
||||
void echelonconnectsport::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')';
|
||||
|
||||
QSettings settings;
|
||||
bool useNativeIOS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
if (useNativeIOS) {
|
||||
#ifndef IO_UNDER_QT
|
||||
iOS_echelonConnectSport = new lockscreen();
|
||||
iOS_echelonConnectSport->echelonConnectSport(device.name().toStdString().c_str(), this);
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
if (device.name().startsWith(QStringLiteral("ECH"))) {
|
||||
bluetoothDevice = device;
|
||||
|
||||
@@ -524,13 +596,23 @@ void echelonconnectsport::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool echelonconnectsport::connected() {
|
||||
QSettings settings;
|
||||
bool useNativeIOS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
if (useNativeIOS) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
@@ -657,10 +739,21 @@ uint16_t echelonconnectsport::wattsFromResistance(double resistance) {
|
||||
|
||||
void echelonconnectsport::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
QSettings settings;
|
||||
bool useNativeIOS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
|
||||
#endif
|
||||
|
||||
if (state == QLowEnergyController::UnconnectedState
|
||||
&& (m_control || useNativeIOS)
|
||||
) {
|
||||
lastResistanceBeforeDisconnection = Resistance.value();
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
if(!useNativeIOS)
|
||||
m_control->connectToDevice();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ class echelonconnectsport : public bike {
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
lockscreen* iOS_echelonConnectSport = nullptr;
|
||||
#endif
|
||||
|
||||
Q_SIGNALS:
|
||||
@@ -89,14 +90,14 @@ class echelonconnectsport : public bike {
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
private slots:
|
||||
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
|
||||
474
src/devices/echelonstairclimber/echelonstairclimber.cpp
Normal file
474
src/devices/echelonstairclimber/echelonstairclimber.cpp
Normal file
@@ -0,0 +1,474 @@
|
||||
#include "echelonstairclimber.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualtreadmill.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
echelonstairclimber::echelonstairclimber(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
refresh = new QTimer(this);
|
||||
initDone = false;
|
||||
connect(refresh, &QTimer::timeout, this, &echelonstairclimber::update);
|
||||
refresh->start(pollDeviceTime);
|
||||
}
|
||||
|
||||
void echelonstairclimber::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
|
||||
bool wait_for_response) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
if (wait_for_response) {
|
||||
connect(this, &echelonstairclimber::packetReceived, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
} else {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit debug(QStringLiteral("writeCharacteristic error because the connection is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
void echelonstairclimber::updateDisplay(uint16_t elapsed) {}
|
||||
|
||||
void echelonstairclimber::forceSpeed(double requestSpeed) {
|
||||
uint8_t noOpData[] = {0xf0, 0xb2, 0x02, 0x00, 0x00, 0x00};
|
||||
|
||||
noOpData[3] = (uint8_t)(((uint16_t)(requestSpeed * 1000.0)) >> 8);
|
||||
noOpData[4] = ((uint8_t)(requestSpeed * 1000.0));
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
|
||||
noOpData[5] += noOpData[i]; // the last byte is a sort of a checksum
|
||||
}
|
||||
|
||||
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("force speed"), false, true);
|
||||
}
|
||||
|
||||
void echelonstairclimber::forceIncline(double requestIncline) {
|
||||
uint8_t noOpData[] = {0xf0, 0xb1, 0x01, 0x00, 0x00};
|
||||
|
||||
noOpData[3] = requestIncline;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
|
||||
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
|
||||
}
|
||||
|
||||
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("force incline"), false, true);
|
||||
}
|
||||
|
||||
void echelonstairclimber::sendPoll() {
|
||||
uint8_t noOpData[] = {0xf0, 0xa0, 0x01, 0x00, 0x00};
|
||||
|
||||
noOpData[3] = counterPoll;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
|
||||
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
|
||||
}
|
||||
|
||||
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true);
|
||||
|
||||
counterPoll++;
|
||||
if (!counterPoll) {
|
||||
counterPoll = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void echelonstairclimber::changeInclinationRequested(double grade, double percentage) {
|
||||
if (percentage < 0)
|
||||
percentage = 0;
|
||||
changeInclination(grade, percentage);
|
||||
}
|
||||
|
||||
void echelonstairclimber::update() {
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
btinit();
|
||||
} else if (/*bluetoothDevice.isValid() &&*/
|
||||
m_control->state() == QLowEnergyController::DiscoveredState && gattCommunicationChannelService &&
|
||||
gattWriteCharacteristic.isValid() && gattNotify1Characteristic.isValid() &&
|
||||
gattNotify2Characteristic.isValid() && initDone) {
|
||||
QSettings settings;
|
||||
// ******************************************* virtual treadmill init *************************************
|
||||
if (!firstInit && !this->hasVirtualDevice()) {
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_force_bike =
|
||||
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike)
|
||||
.toBool();
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_force_bike) {
|
||||
debug("creating virtual treadmill interface...");
|
||||
auto virtualTreadMill = new virtualtreadmill(this, noHeartService);
|
||||
connect(virtualTreadMill, &virtualtreadmill::debug, this, &echelonstairclimber::debug);
|
||||
connect(virtualTreadMill, &virtualtreadmill::changeInclination, this,
|
||||
&echelonstairclimber::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
debug("creating virtual bike interface...");
|
||||
auto virtualBike = new virtualbike(this);
|
||||
connect(virtualBike, &virtualbike::changeInclination, this,
|
||||
&echelonstairclimber::changeInclinationRequested);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
|
||||
}
|
||||
firstInit = 1;
|
||||
}
|
||||
}
|
||||
// ********************************************************************************************************
|
||||
|
||||
update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
// updating the treadmill console every second
|
||||
if (sec1Update++ >= (2000 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
sendPoll();
|
||||
}
|
||||
/*
|
||||
if (requestSpeed != -1) {
|
||||
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
|
||||
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
|
||||
forceSpeed(requestSpeed);
|
||||
}
|
||||
requestSpeed = -1;
|
||||
}
|
||||
if (requestInclination != -100) {
|
||||
if (requestInclination < 0)
|
||||
requestInclination = 0;
|
||||
if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
|
||||
requestInclination <= 15) {
|
||||
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
|
||||
forceIncline(requestInclination);
|
||||
}
|
||||
requestInclination = -100;
|
||||
}
|
||||
if (requestStart != -1) {
|
||||
emit debug(QStringLiteral("starting..."));
|
||||
if (lastSpeed == 0.0) {
|
||||
lastSpeed = 0.5;
|
||||
}
|
||||
uint8_t initData3[] = {0xf0, 0xb0, 0x01, 0x01, 0xa2};
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("start"), false, true);
|
||||
|
||||
if(stride4) {
|
||||
uint8_t initData0[] = {0xf0, 0xa5, 0x00, 0x95};
|
||||
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("start"), false, false);
|
||||
}
|
||||
|
||||
uint8_t initData4[] = {0xf0, 0xd0, 0x01, 0x00, 0xc1};
|
||||
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("start"), false, false);
|
||||
|
||||
uint8_t initData5[] = {0xf0, 0xd0, 0x01, 0x11, 0xd2};
|
||||
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("start"), false, false);
|
||||
|
||||
if(stride4) {
|
||||
uint8_t initData0[] = {0xf0, 0xd3, 0x02, 0x01, 0xf4, 0xba};
|
||||
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("start"), false, false);
|
||||
}
|
||||
|
||||
lastStart = QDateTime::currentMSecsSinceEpoch();
|
||||
requestStart = -1;
|
||||
emit tapeStarted();
|
||||
}
|
||||
if (requestStop != -1) {
|
||||
emit debug(QStringLiteral("stopping..."));
|
||||
uint8_t initData3[] = {0xf0, 0xb0, 0x01, 0x00, 0xa1};
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("stop"), false, true);
|
||||
requestStop = -1;
|
||||
lastStop = QDateTime::currentMSecsSinceEpoch();
|
||||
}
|
||||
if (requestFanSpeed != -1) {
|
||||
emit debug(QStringLiteral("changing fan speed..."));
|
||||
// sendChangeFanSpeed(requestFanSpeed);
|
||||
requestFanSpeed = -1;
|
||||
}
|
||||
if (requestIncreaseFan != -1) {
|
||||
emit debug(QStringLiteral("increasing fan speed..."));
|
||||
// sendChangeFanSpeed(FanSpeed + 1);
|
||||
requestIncreaseFan = -1;
|
||||
} else if (requestDecreaseFan != -1) {
|
||||
emit debug(QStringLiteral("decreasing fan speed..."));
|
||||
// sendChangeFanSpeed(FanSpeed - 1);
|
||||
requestDecreaseFan = -1;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
void echelonstairclimber::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void echelonstairclimber::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
// qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
Q_UNUSED(characteristic);
|
||||
QByteArray value = newValue;
|
||||
|
||||
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
|
||||
|
||||
lastPacket = newValue;
|
||||
|
||||
if (((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd1) {
|
||||
|
||||
uint16_t convertedData = (newValue.at(5) << 8) | ((uint8_t)newValue.at(6));
|
||||
StepCount = convertedData;
|
||||
convertedData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
|
||||
elevationAcc = convertedData;
|
||||
Cadence = (uint8_t)newValue.at(9);
|
||||
Speed = Cadence.value() / 3.2;
|
||||
|
||||
qDebug() << QStringLiteral("Current Floors: ") + QString::number(StepCount.value() / 10.0);
|
||||
qDebug() << QStringLiteral("Current Cadence: ") + QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current StepCount: ") + QString::number(StepCount.value());
|
||||
qDebug() << QStringLiteral("Current elevationAcc: ") + QString::number(elevationAcc.value());
|
||||
return;
|
||||
}
|
||||
|
||||
/*if (newValue.length() != 21)
|
||||
return;*/
|
||||
|
||||
/*if ((uint8_t)(newValue.at(0)) != 0xf0 && (uint8_t)(newValue.at(1)) != 0xd1)
|
||||
return;*/
|
||||
|
||||
if (!firstCharacteristicChanged) {
|
||||
if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()))
|
||||
KCal +=
|
||||
((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) +
|
||||
1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo(
|
||||
now)))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
Distance += ((Speed.value() / 3600.0) /
|
||||
(1000.0 / (lastTimeCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
|
||||
if ((uint8_t)newValue.at(1) == 0xD1 && newValue.length() > 11)
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
Heart = (uint8_t)KeepAwakeHelper::heart();
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
|
||||
|
||||
uint8_t heart = ((uint8_t)newValue.at(11));
|
||||
if (heart == 0) {
|
||||
update_hr_from_external();
|
||||
} else {
|
||||
Heart = heart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Current Heart: ") + QString::number(Heart.value());
|
||||
qDebug() << QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value());
|
||||
qDebug() << QStringLiteral("Current Watt: ") +
|
||||
QString::number(watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError)
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
|
||||
lastTimeCharacteristicChanged = now;
|
||||
firstCharacteristicChanged = false;
|
||||
}
|
||||
|
||||
void echelonstairclimber::btinit() {
|
||||
uint8_t initData0[] = {0xf0, 0xa4, 0x00, 0x94};
|
||||
uint8_t initData1[] = {0xf0, 0xe0, 0x85, 0xf8, 0x03, 0x68, 0xd2, 0x8a};
|
||||
uint8_t initData2[] = {0xf0, 0xe0, 0x38, 0x04, 0x03, 0xb5, 0x14, 0xd8};
|
||||
uint8_t initData3[] = {0xf0, 0xa1, 0x00, 0x91};
|
||||
uint8_t initData4[] = {0xf0, 0xa3, 0x00, 0x93};
|
||||
uint8_t initData5[] = {0xf0, 0xb0, 0x01, 0x01, 0xa2};
|
||||
uint8_t initData6[] = {0xf0, 0xa5, 0x00, 0x95};
|
||||
|
||||
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, true);
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void echelonstairclimber::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66"));
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66"));
|
||||
QBluetoothUuid _gattNotify2CharacteristicId(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66"));
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
// qDebug() << gattCommunicationChannelService->characteristics();
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
|
||||
gattNotify2Characteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId);
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotify1Characteristic.isValid());
|
||||
Q_ASSERT(gattNotify2Characteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&echelonstairclimber::characteristicChanged);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
|
||||
&echelonstairclimber::characteristicWritten);
|
||||
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)), this,
|
||||
SLOT(errorService(QLowEnergyService::ServiceError)));
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&echelonstairclimber::descriptorWritten);
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotify2Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
void echelonstairclimber::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
|
||||
initRequest = true;
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void echelonstairclimber::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void echelonstairclimber::serviceScanDone(void) {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
qDebug() << s << "service found!";
|
||||
}
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0bf669f1-45f2-11e7-9598-0800200c9a66"));
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
if(gattCommunicationChannelService == nullptr) {
|
||||
qDebug() << "invalid service";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &echelonstairclimber::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void echelonstairclimber::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("echelonstairclimber::errorService ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void echelonstairclimber::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("echelonstairclimber::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void echelonstairclimber::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &echelonstairclimber::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &echelonstairclimber::serviceScanDone);
|
||||
connect(m_control, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(error(QLowEnergyController::Error)));
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &echelonstairclimber::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to remote device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Controller connected. Search services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("LowEnergy controller disconnected"));
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void echelonstairclimber::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
bool echelonstairclimber::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
104
src/devices/echelonstairclimber/echelonstairclimber.h
Normal file
104
src/devices/echelonstairclimber/echelonstairclimber.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#ifndef ECHELONSTAIRCLIMBER_H
|
||||
#define ECHELONSTAIRCLIMBER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "stairclimber.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class echelonstairclimber : public stairclimber {
|
||||
Q_OBJECT
|
||||
public:
|
||||
echelonstairclimber(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false,
|
||||
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
double GetSpeedFromPacket(QByteArray packet);
|
||||
double GetInclinationFromPacket(QByteArray packet);
|
||||
double GetKcalFromPacket(QByteArray packet);
|
||||
double GetDistanceFromPacket(QByteArray packet);
|
||||
void forceSpeed(double requestSpeed);
|
||||
void forceIncline(double requestIncline);
|
||||
void updateDisplay(uint16_t elapsed);
|
||||
void btinit();
|
||||
void sendPoll();
|
||||
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
|
||||
bool wait_for_response = false);
|
||||
void startDiscover();
|
||||
bool noConsole = false;
|
||||
bool noHeartService = false;
|
||||
uint32_t pollDeviceTime = 200;
|
||||
uint8_t sec1Update = 0;
|
||||
uint8_t firstInit = 0;
|
||||
uint8_t counterPoll = 1;
|
||||
QByteArray lastPacket;
|
||||
QDateTime lastTimeCharacteristicChanged;
|
||||
bool firstCharacteristicChanged = true;
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotify1Characteristic;
|
||||
QLowEnergyCharacteristic gattNotify2Characteristic;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void speedChanged(double speed);
|
||||
void packetReceived();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
void changeInclinationRequested(double grade, double percentage);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // ECHELONSTAIRCLIMBER_H
|
||||
304
src/devices/elitesquarecontroller/elitesquarecontroller.cpp
Normal file
304
src/devices/elitesquarecontroller/elitesquarecontroller.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
#include "homeform.h"
|
||||
#include "elitesquarecontroller.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
extern quint8 QZ_EnableDiscoveryCharsAndDescripttors;
|
||||
#endif
|
||||
|
||||
// Define static constants
|
||||
const QString elitesquarecontroller::DEVICE_NAME = "SQUARE";
|
||||
const QBluetoothUuid elitesquarecontroller::SERVICE_UUID = QBluetoothUuid(QStringLiteral("347b0001-7635-408b-8918-8ff3949ce592"));
|
||||
const QBluetoothUuid elitesquarecontroller::CHARACTERISTIC_UUID = QBluetoothUuid(QStringLiteral("347b0045-7635-408b-8918-8ff3949ce592"));
|
||||
|
||||
elitesquarecontroller::elitesquarecontroller(bluetoothdevice *parentDevice) {
|
||||
#ifdef Q_OS_IOS
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
this->parentDevice = parentDevice;
|
||||
|
||||
// Initialize button state vector (24 buttons from 0-23)
|
||||
buttonState.resize(24);
|
||||
buttonState.fill(0);
|
||||
|
||||
// Set up refresh timer
|
||||
refresh = new QTimer(this);
|
||||
connect(refresh, &QTimer::timeout, this, &elitesquarecontroller::update);
|
||||
refresh->start(1000ms);
|
||||
}
|
||||
|
||||
void elitesquarecontroller::update() {
|
||||
// Just a simple heartbeat check - no handshake needed for Elite Square
|
||||
if (m_control && m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
// Try to reconnect if disconnected
|
||||
qDebug() << QStringLiteral("Elite Square disconnected, attempting to reconnect...");
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void elitesquarecontroller::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
|
||||
// Check if the discovered service is the Elite Square service
|
||||
if (gatt == SERVICE_UUID) {
|
||||
qDebug() << QStringLiteral("Elite Square service discovered");
|
||||
}
|
||||
}
|
||||
|
||||
void elitesquarecontroller::disconnectBluetooth() {
|
||||
qDebug() << QStringLiteral("elitesquarecontroller::disconnect") << m_control;
|
||||
|
||||
if (m_control) {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void elitesquarecontroller::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
|
||||
const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
emit packetReceived();
|
||||
|
||||
qDebug() << QStringLiteral(" << ") << newValue.toHex(' ') << QString(newValue);
|
||||
|
||||
// Process the Elite Square button data
|
||||
if (characteristic.uuid() == CHARACTERISTIC_UUID) {
|
||||
// Process the raw bytes directly
|
||||
parseButtonData(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
void elitesquarecontroller::parseButtonData(const QByteArray &data) {
|
||||
// The data comes as raw bytes, with 11 bytes total
|
||||
if (data.size() < 11) {
|
||||
qDebug() << QStringLiteral("Invalid button data size: ") << data.size();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the hex data for better debugging
|
||||
qDebug() << QStringLiteral("Processing button data: ") << data.toHex(' ');
|
||||
|
||||
// Extract the important bytes (based on the provided Android code)
|
||||
uint8_t leftByte = data[5];
|
||||
uint8_t rightByte = data[10];
|
||||
|
||||
// Extract the nibbles from these bytes
|
||||
uint8_t leftLargeValue = (leftByte >> 4) & 0xF; // Left Shift Button 1
|
||||
uint8_t leftSmallValue = leftByte & 0xF; // Left Shift Button 2
|
||||
uint8_t rightLargeValue = (rightByte >> 4) & 0xF; // Right Shift Button 1
|
||||
uint8_t rightSmallValue = rightByte & 0xF; // Right Shift Button 2
|
||||
|
||||
qDebug() << QStringLiteral("Button values: leftLarge=") << leftLargeValue
|
||||
<< QStringLiteral(" leftSmall=") << leftSmallValue
|
||||
<< QStringLiteral(" rightLarge=") << rightLargeValue
|
||||
<< QStringLiteral(" rightSmall=") << rightSmallValue;
|
||||
|
||||
// Check if the left large shift button (Left Shift Button 1) is pressed
|
||||
// According to the code, odd values indicate press events
|
||||
if (leftLargeValue != buttonState[BUTTON_LEFT_SHIFT_1] && leftLargeValue % 2 == 1) {
|
||||
qDebug() << QStringLiteral("Left Shift Button 1 pressed (shift down 1)");
|
||||
emit minus();
|
||||
}
|
||||
buttonState[BUTTON_LEFT_SHIFT_1] = leftLargeValue;
|
||||
|
||||
// Check if the left small shift button (Left Shift Button 2) is pressed
|
||||
if (leftSmallValue != buttonState[BUTTON_LEFT_SHIFT_2] && leftSmallValue % 2 == 1) {
|
||||
qDebug() << QStringLiteral("Left Shift Button 2 pressed (shift down 3)");
|
||||
// Emit minus three times for triple downshift
|
||||
emit minus();
|
||||
emit minus();
|
||||
emit minus();
|
||||
}
|
||||
buttonState[BUTTON_LEFT_SHIFT_2] = leftSmallValue;
|
||||
|
||||
// Check if the right large shift button (Right Shift Button 1) is pressed
|
||||
if (rightLargeValue != buttonState[BUTTON_RIGHT_SHIFT_1] && rightLargeValue % 2 == 1) {
|
||||
qDebug() << QStringLiteral("Right Shift Button 1 pressed (shift up 1)");
|
||||
emit plus();
|
||||
}
|
||||
buttonState[BUTTON_RIGHT_SHIFT_1] = rightLargeValue;
|
||||
|
||||
// Check if the right small shift button (Right Shift Button 2) is pressed
|
||||
if (rightSmallValue != buttonState[BUTTON_RIGHT_SHIFT_2] && rightSmallValue % 2 == 1) {
|
||||
qDebug() << QStringLiteral("Right Shift Button 2 pressed (shift up 3)");
|
||||
// Emit plus three times for triple upshift
|
||||
emit plus();
|
||||
emit plus();
|
||||
emit plus();
|
||||
}
|
||||
buttonState[BUTTON_RIGHT_SHIFT_2] = rightSmallValue;
|
||||
|
||||
// Check for steering buttons and other controls
|
||||
// Assuming byte 3 might contain steering information as in our previous implementation
|
||||
uint8_t controlByte = data[3];
|
||||
|
||||
// These conditions would need to be adjusted based on actual behavior
|
||||
if (controlByte == 0x60 && data[3] != buttonState[BUTTON_X]) {
|
||||
emit steeringLeft(true);
|
||||
buttonState[BUTTON_X] = controlByte;
|
||||
} else if (controlByte == 0x20 && data[3] != buttonState[BUTTON_CIRCLE]) {
|
||||
// Need to make sure this doesn't conflict with shift button detection
|
||||
emit steeringRight(true);
|
||||
buttonState[BUTTON_CIRCLE] = controlByte;
|
||||
} else if (controlByte == 0x00) {
|
||||
emit steeringLeft(false);
|
||||
emit steeringRight(false);
|
||||
buttonState[BUTTON_X] = 0;
|
||||
buttonState[BUTTON_CIRCLE] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void elitesquarecontroller::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state();
|
||||
if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) {
|
||||
qDebug() << QStringLiteral("not all services discovered");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (state != QLowEnergyService::ServiceState::ServiceDiscovered) {
|
||||
qDebug() << QStringLiteral("ignoring this state");
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("all services discovered!");
|
||||
|
||||
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
|
||||
if (s->state() == QLowEnergyService::ServiceDiscovered) {
|
||||
// establish hook into notifications
|
||||
connect(s, &QLowEnergyService::characteristicChanged, this, &elitesquarecontroller::characteristicChanged);
|
||||
connect(
|
||||
s, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &elitesquarecontroller::errorService);
|
||||
connect(s, &QLowEnergyService::descriptorWritten, this, &elitesquarecontroller::descriptorWritten);
|
||||
|
||||
qDebug() << s->serviceUuid() << QStringLiteral("connected!");
|
||||
|
||||
// Check if this is the Elite Square service
|
||||
if (s->serviceUuid() == SERVICE_UUID) {
|
||||
gattService = s;
|
||||
|
||||
// Find the notification characteristic
|
||||
auto characteristics_list = s->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
if (c.uuid() == CHARACTERISTIC_UUID) {
|
||||
gattNotifyCharacteristic = c;
|
||||
|
||||
// Subscribe to notifications
|
||||
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
|
||||
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
connectionEstablished = true;
|
||||
qDebug() << QStringLiteral("Elite Square notification subscribed!");
|
||||
} else {
|
||||
qDebug() << QStringLiteral("ClientCharacteristicConfiguration is not valid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void elitesquarecontroller::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void elitesquarecontroller::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
if (gattCommunicationChannelService.constLast()) {
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&elitesquarecontroller::stateChanged);
|
||||
gattCommunicationChannelService.constLast()->discoverDetails();
|
||||
} else {
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void elitesquarecontroller::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
emit debug(QStringLiteral("elitesquarecontroller::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void elitesquarecontroller::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
emit debug(QStringLiteral("elitesquarecontroller::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString());
|
||||
}
|
||||
|
||||
void elitesquarecontroller::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
|
||||
// Check if this is the Elite Square device
|
||||
if (device.name() == DEVICE_NAME) {
|
||||
bluetoothDevice = device;
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &elitesquarecontroller::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &elitesquarecontroller::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &elitesquarecontroller::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &elitesquarecontroller::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Cannot connect to Elite Square device."));
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Elite Square controller connected. Searching services..."));
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
emit debug(QStringLiteral("Elite Square controller disconnected"));
|
||||
connectionEstablished = false;
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect to the device
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
bool elitesquarecontroller::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return connectionEstablished && (m_control->state() == QLowEnergyController::DiscoveredState);
|
||||
}
|
||||
|
||||
void elitesquarecontroller::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
connectionEstablished = false;
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
120
src/devices/elitesquarecontroller/elitesquarecontroller.h
Normal file
120
src/devices/elitesquarecontroller/elitesquarecontroller.h
Normal file
@@ -0,0 +1,120 @@
|
||||
#ifndef ELITESQUARECONTROLLER_H
|
||||
#define ELITESQUARECONTROLLER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
|
||||
#include "devices/bluetoothdevice.h"
|
||||
#include "zwift_play/abstractZapDevice.h"
|
||||
|
||||
// Button positions on Elite Square controller
|
||||
enum EliteSquareButton {
|
||||
BUTTON_NONE = 0,
|
||||
// 1 and 2 are N/A
|
||||
BUTTON_UP = 3,
|
||||
BUTTON_LEFT = 4,
|
||||
BUTTON_DOWN = 5,
|
||||
BUTTON_RIGHT = 6,
|
||||
BUTTON_X = 7,
|
||||
BUTTON_SQUARE = 8,
|
||||
BUTTON_LEFT_CAMPAGNOLO = 9,
|
||||
BUTTON_LEFT_BRAKE = 10,
|
||||
BUTTON_LEFT_SHIFT_1 = 11,
|
||||
BUTTON_LEFT_SHIFT_2 = 12,
|
||||
BUTTON_Y = 13,
|
||||
BUTTON_A = 14,
|
||||
BUTTON_B = 15,
|
||||
BUTTON_Z = 16,
|
||||
BUTTON_CIRCLE = 17,
|
||||
BUTTON_TRIANGLE = 18,
|
||||
// 19 is N/A
|
||||
BUTTON_RIGHT_CAMPAGNOLO = 20,
|
||||
BUTTON_RIGHT_BRAKE = 21,
|
||||
BUTTON_RIGHT_SHIFT_1 = 22,
|
||||
BUTTON_RIGHT_SHIFT_2 = 23
|
||||
};
|
||||
|
||||
// Button state
|
||||
enum EliteSquareButtonState {
|
||||
RELEASED = 0,
|
||||
PRESSED = 1
|
||||
};
|
||||
|
||||
class elitesquarecontroller : public bluetoothdevice {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Constants for Elite Square device
|
||||
static const QString DEVICE_NAME;
|
||||
static const QBluetoothUuid SERVICE_UUID;
|
||||
static const QBluetoothUuid CHARACTERISTIC_UUID;
|
||||
|
||||
elitesquarecontroller(bluetoothdevice *parentDevice);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
QList<QLowEnergyService *> gattCommunicationChannelService;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
QLowEnergyService *gattService;
|
||||
|
||||
bluetoothdevice *parentDevice = nullptr;
|
||||
|
||||
bool connectionEstablished = false;
|
||||
QTimer *refresh;
|
||||
|
||||
// Last known state of each button (0-23)
|
||||
QVector<int> buttonState;
|
||||
|
||||
// Helper function to parse the button data
|
||||
void parseButtonData(const QByteArray &data);
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void debug(QString string);
|
||||
void packetReceived();
|
||||
|
||||
// Match AbstractZapDevice signals
|
||||
void plus(); // Gear up/increase
|
||||
void minus(); // Gear down/decrease
|
||||
void steeringLeft(bool active); // X button and Left Campagnolo
|
||||
void steeringRight(bool active); // Circle button and Right Campagnolo
|
||||
void buttonActivated(int buttonId, bool pressed); // General button signal
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
void disconnectBluetooth();
|
||||
|
||||
private slots:
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
};
|
||||
|
||||
#endif // ELITESQUARECONTROLLER_H
|
||||
@@ -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);
|
||||
|
||||
@@ -48,10 +48,13 @@ void fakebike::update() {
|
||||
|
||||
if (requestPower != -1) {
|
||||
// bepo70: don't know if this conversion is really needed, i would do it anyway.
|
||||
m_watt = (double)requestPower;
|
||||
Cadence = requestPower;
|
||||
m_watt = (double)requestPower * (1.0 + (((double)rand() / RAND_MAX) * 0.4 - 0.2));
|
||||
if(requestPower)
|
||||
Cadence = 50 + (static_cast<double>(rand()) / RAND_MAX) * 50;
|
||||
else
|
||||
Cadence = 0;
|
||||
emit debug(QStringLiteral("writing power ") + QString::number(requestPower));
|
||||
requestPower = -1;
|
||||
//requestPower = -1;
|
||||
// bepo70: Disregard the current inclination for calculating speed. When the video
|
||||
// has a high inclination you have to give many power to get the desired playback speed,
|
||||
// if inclination is very low little more power gives a quite high speed jump.
|
||||
|
||||
@@ -512,7 +512,7 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index)))) / 10.0;
|
||||
emit resistanceRead(Resistance.value());
|
||||
m_pelotonResistance = Resistance.value();
|
||||
m_pelotonResistance = bikeResistanceToPeloton(Resistance.value());
|
||||
index += 2;
|
||||
qDebug() << QStringLiteral("Current Resistance: ") + QString::number(Resistance.value());
|
||||
}
|
||||
@@ -633,7 +633,7 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
|
||||
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
|
||||
} else {
|
||||
m_pelotonResistance = (100 * Resistance.value()) / max_resistance;
|
||||
m_pelotonResistance = bikeResistanceToPeloton(Resistance.value());
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
@@ -736,6 +736,19 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
}
|
||||
}
|
||||
|
||||
resistance_t fitplusbike::pelotonToBikeResistance(int pelotonResistance) {
|
||||
QSettings settings;
|
||||
double adjustedPelotonResistance = (pelotonResistance - settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble()) /
|
||||
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble();
|
||||
return (adjustedPelotonResistance * max_resistance) / 100;
|
||||
}
|
||||
|
||||
double fitplusbike::bikeResistanceToPeloton(double resistance) {
|
||||
QSettings settings;
|
||||
return (((resistance * 100) / max_resistance) * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
|
||||
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
|
||||
}
|
||||
|
||||
void fitplusbike::btinit() {
|
||||
|
||||
QSettings settings;
|
||||
@@ -1003,6 +1016,7 @@ void fitplusbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
merach_MRK = true;
|
||||
} else if (device.name().toUpper().startsWith("H9110 OSAKA")) {
|
||||
qDebug() << QStringLiteral("H9110 OSAKA workaround enabled!");
|
||||
max_resistance = 32;
|
||||
H9110_OSAKA = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class fitplusbike : public bike {
|
||||
public:
|
||||
fitplusbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset, double bikeResistanceGain);
|
||||
resistance_t maxResistance() override { return max_resistance; }
|
||||
resistance_t pelotonToBikeResistance(int pelotonResistance) override;
|
||||
bool connected() override;
|
||||
resistance_t resistanceFromPowerRequest(uint16_t power) override;
|
||||
|
||||
@@ -49,6 +50,7 @@ class fitplusbike : public bike {
|
||||
void startDiscover();
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void sendPoll();
|
||||
double bikeResistanceToPeloton(double resistance);
|
||||
uint16_t watts() override;
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ void fitshowtreadmill::update() {
|
||||
if (requestInclination != inc) {
|
||||
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
|
||||
inc = requestInclination;
|
||||
double speed = currentSpeed().value();
|
||||
double speed = currentSpeed().valueRaw();
|
||||
if (requestSpeed != -1) {
|
||||
speed = requestSpeed;
|
||||
requestSpeed = -1;
|
||||
|
||||
@@ -34,6 +34,7 @@ ftmsbike::ftmsbike(bool noWriteResistance, bool noHeartService, int8_t bikeResis
|
||||
this->bikeResistanceGain = bikeResistanceGain;
|
||||
this->bikeResistanceOffset = bikeResistanceOffset;
|
||||
initDone = false;
|
||||
ergModeSupported = true; // by default ftms devices SHOULD have ergMode supported
|
||||
connect(refresh, &QTimer::timeout, this, &ftmsbike::update);
|
||||
refresh->start(settings.value(QZSettings::poll_device_time, QZSettings::default_poll_device_time).toInt());
|
||||
wheelCircumference::GearTable g;
|
||||
@@ -129,14 +130,17 @@ void ftmsbike::init() {
|
||||
if (initDone)
|
||||
return;
|
||||
|
||||
if(ICSE) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
bool ret = writeCharacteristic(write, sizeof(write), "requestControl", false, true);
|
||||
write[0] = {FTMS_RESET};
|
||||
ret = writeCharacteristic(write, sizeof(write), "reset", false, true);
|
||||
}
|
||||
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
bool ret = writeCharacteristic(write, sizeof(write), "requestControl", false, true);
|
||||
if(resistance_lvl_mode && DIRETO_XR) {
|
||||
setWheelDiameter(2070.0);
|
||||
} else {
|
||||
write[0] = {FTMS_START_RESUME};
|
||||
ret = writeCharacteristic(write, sizeof(write), "start simulation", false, true);
|
||||
}
|
||||
write[0] = {FTMS_START_RESUME};
|
||||
ret = writeCharacteristic(write, sizeof(write), "start simulation", false, true);
|
||||
|
||||
if(ret) {
|
||||
initDone = true;
|
||||
@@ -145,10 +149,6 @@ void ftmsbike::init() {
|
||||
}
|
||||
|
||||
ftmsbike::~ftmsbike() {
|
||||
// Set wheel circumference back to 2070 when object is destroyed
|
||||
if (DIRETO_XR) {
|
||||
setWheelDiameter(2070.0);
|
||||
}
|
||||
}
|
||||
|
||||
void ftmsbike::zwiftPlayInit() {
|
||||
@@ -194,7 +194,7 @@ void ftmsbike::zwiftPlayInit() {
|
||||
}
|
||||
|
||||
void ftmsbike::forcePower(int16_t requestPower) {
|
||||
if(resistance_lvl_mode) {
|
||||
if(resistance_lvl_mode || TITAN_7000) {
|
||||
forceResistance(resistanceFromPowerRequest(requestPower));
|
||||
} else {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00};
|
||||
@@ -232,18 +232,31 @@ resistance_t ftmsbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return max_resistance;
|
||||
if(DU30_bike)
|
||||
return max_resistance;
|
||||
else
|
||||
return _ergTable.getMaxResistance();
|
||||
}
|
||||
|
||||
void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
|
||||
QSettings settings;
|
||||
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
|
||||
if (!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool() &&
|
||||
resistance_lvl_mode == false && _3G_Cardio_RB == false && JFBK5_0 == false) {
|
||||
|
||||
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x28, 0x19};
|
||||
|
||||
double fr = (((double)requestResistance) * bikeResistanceGain) + ((double)bikeResistanceOffset);
|
||||
requestResistance = fr;
|
||||
if(ergModeNotSupported) {
|
||||
requestResistance = _inclinationResistanceTable.estimateInclination(requestResistance) * 10.0;
|
||||
qDebug() << "ergMode Not Supported so the resistance will be" << requestResistance;
|
||||
} else {
|
||||
requestResistance = fr;
|
||||
}
|
||||
|
||||
if(TITAN_7000)
|
||||
Resistance = requestResistance;
|
||||
|
||||
write[3] = ((uint16_t)requestResistance * 10) & 0xFF;
|
||||
write[4] = ((uint16_t)requestResistance * 10) >> 8;
|
||||
@@ -251,6 +264,9 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
writeCharacteristic(write, sizeof(write),
|
||||
QStringLiteral("forceResistance ") + QString::number(requestResistance));
|
||||
} else {
|
||||
if(SL010)
|
||||
Resistance = requestResistance;
|
||||
|
||||
if(JFBK5_0 || DIRETO_XR) {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00, 0x00};
|
||||
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
|
||||
@@ -259,7 +275,7 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
QStringLiteral("forceResistance ") + QString::number(requestResistance));
|
||||
} else {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00};
|
||||
if(_3G_Cardio_RB)
|
||||
if(_3G_Cardio_RB || SL010)
|
||||
requestResistance = requestResistance * 10;
|
||||
write[1] = ((uint8_t)(requestResistance));
|
||||
writeCharacteristic(write, sizeof(write),
|
||||
@@ -269,6 +285,9 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
}
|
||||
|
||||
void ftmsbike::update() {
|
||||
|
||||
QSettings settings;
|
||||
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
@@ -276,6 +295,13 @@ void ftmsbike::update() {
|
||||
|
||||
if (initRequest) {
|
||||
zwiftPlayInit();
|
||||
if(ICSE)
|
||||
requestResistance = 1; // to force the engine to send every second a target inclination
|
||||
|
||||
// when we are emulating the zwift protocol, zwift doesn't senf the start simulation frames, so we have to send them
|
||||
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool())
|
||||
init();
|
||||
|
||||
initRequest = false;
|
||||
} else if (bluetoothDevice.isValid() &&
|
||||
m_control->state() == QLowEnergyController::DiscoveredState //&&
|
||||
@@ -299,8 +325,7 @@ void ftmsbike::update() {
|
||||
forceResistance(currentResistance().value());
|
||||
}
|
||||
|
||||
auto virtualBike = this->VirtualBike();
|
||||
QSettings settings;
|
||||
auto virtualBike = this->VirtualBike();
|
||||
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
|
||||
|
||||
if (requestResistance != -1 || lastGearValue != gears()) {
|
||||
@@ -311,32 +336,30 @@ void ftmsbike::update() {
|
||||
requestResistance = 1;
|
||||
}
|
||||
|
||||
if (requestResistance != currentResistance().value() || lastGearValue != gears()) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
double gearMultiplier = 5;
|
||||
if(REEBOK)
|
||||
gearMultiplier = 1;
|
||||
resistance_t rR = requestResistance + (gears() * gearMultiplier);
|
||||
|
||||
if (rR != currentResistance().value() || lastGearValue != gears()) {
|
||||
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
|
||||
qDebug() << QStringLiteral("writing resistance ") << requestResistance << ergModeNotSupported << requestPower << resistance_lvl_mode;
|
||||
// if the FTMS is connected, the ftmsCharacteristicChanged event will do all the stuff because it's a
|
||||
// FTMS bike. This condition handles the peloton requests
|
||||
if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike || resistance_lvl_mode) &&
|
||||
(requestPower == 0 || requestPower == -1)) {
|
||||
// FTMS bike. This condition handles the peloton requests
|
||||
if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike || resistance_lvl_mode || ergModeNotSupported) &&
|
||||
(requestPower == 0 || requestPower == -1 || resistance_lvl_mode || ergModeNotSupported)) {
|
||||
init();
|
||||
|
||||
if(DIRETO_XR && gears_zwift_ratio)
|
||||
setWheelDiameter(wheelCircumference::gearsToWheelDiameter(gears()));
|
||||
else {
|
||||
forceResistance(requestResistance + (gears() * 5));
|
||||
if(DIRETO_XR)
|
||||
Resistance = requestResistance;
|
||||
}
|
||||
forceResistance(rR);
|
||||
}
|
||||
}
|
||||
requestResistance = -1;
|
||||
if(!ICSE)
|
||||
requestResistance = -1;
|
||||
}
|
||||
|
||||
if((virtualBike && virtualBike->ftmsDeviceConnected()) && lastGearValue != gears() && lastRawRequestedInclinationValue != -100 && lastPacketFromFTMS.length() >= 7) {
|
||||
if(DIRETO_XR && gears_zwift_ratio) {
|
||||
setWheelDiameter(wheelCircumference::gearsToWheelDiameter(gears()));
|
||||
} else {
|
||||
qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP" << lastPacketFromFTMS.toHex(' ');
|
||||
ftmsCharacteristicChanged(QLowEnergyCharacteristic(), lastPacketFromFTMS);
|
||||
}
|
||||
qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP" << lastPacketFromFTMS.toHex(' ');
|
||||
ftmsCharacteristicChanged(QLowEnergyCharacteristic(), lastPacketFromFTMS);
|
||||
}
|
||||
|
||||
if(zwiftPlayService && gears_zwift_ratio && lastGearValue != gears()) {
|
||||
@@ -391,7 +414,8 @@ void ftmsbike::update() {
|
||||
|
||||
lastGearValue = gears();
|
||||
|
||||
if (requestPower != -1) {
|
||||
// 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))) {
|
||||
qDebug() << QStringLiteral("writing power") << requestPower;
|
||||
init();
|
||||
forcePower(requestPower);
|
||||
@@ -473,6 +497,26 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
return;
|
||||
}
|
||||
|
||||
if(T2 && characteristic.uuid() == QBluetoothUuid(QStringLiteral("6e400003-b5a3-f393-e0a9-e50e24dcca9e")) && newValue.length() == 62) {
|
||||
int16_t gears = ((int16_t)(((int16_t)((uint8_t)newValue.at(55)) << 8) |
|
||||
(int16_t)((uint8_t)newValue.at(54))));
|
||||
|
||||
qDebug() << QStringLiteral("T2 gears event, actual gear") << gears << QStringLiteral("previous value") << T2_lastGear;
|
||||
|
||||
if (gears < T2_lastGear) {
|
||||
for (int i = 0; i < T2_lastGear - gears; ++i) {
|
||||
gearDown();
|
||||
}
|
||||
} else if (gears > T2_lastGear) {
|
||||
for (int i = 0; i < gears - T2_lastGear; ++i) {
|
||||
gearUp();
|
||||
}
|
||||
}
|
||||
|
||||
T2_lastGear = gears;
|
||||
return;
|
||||
}
|
||||
|
||||
// Wattbike Atom First Generation - Display Gears
|
||||
if(WATTBIKE && characteristic.uuid() == QBluetoothUuid(QStringLiteral("b4cc1224-bc02-4cae-adb9-1217ad2860d1")) &&
|
||||
newValue.length() > 3 && newValue.at(1) == 0x03 && (uint8_t)newValue.at(2) == 0xb6) {
|
||||
@@ -580,10 +624,12 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
double d = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
index += 2;
|
||||
if(Resistance.value() > 0) {
|
||||
if(d > 0) {
|
||||
if(BIKE_)
|
||||
d = d / 10.0;
|
||||
Resistance = d;
|
||||
// for this bike, i will use the resistance that I set directly because the bike sends a different ratio.
|
||||
if(!SL010 && !TITAN_7000)
|
||||
Resistance = d;
|
||||
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
|
||||
emit resistanceRead(Resistance.value());
|
||||
resistance_received = true;
|
||||
@@ -598,15 +644,19 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
double cr = 97.62165482;
|
||||
|
||||
if (Cadence.value() && m_watt.value()) {
|
||||
m_pelotonResistance =
|
||||
(((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 (!resistance_received && !DU30_bike) {
|
||||
if(YS_G1MPLUS) {
|
||||
m_pelotonResistance = Resistance.value(); // 1:1 ratio
|
||||
} else {
|
||||
m_pelotonResistance =
|
||||
(((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 (!resistance_received && !DU30_bike && !SL010) {
|
||||
Resistance = m_pelotonResistance;
|
||||
emit resistanceRead(Resistance.value());
|
||||
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
|
||||
@@ -695,6 +745,181 @@ 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 (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
if (newValue.length() > 3) {
|
||||
m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -789,9 +1014,12 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
}
|
||||
|
||||
if (Flags.resistanceLvl) {
|
||||
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
emit resistanceRead(Resistance.value());
|
||||
if(!TITAN_7000) {
|
||||
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
emit resistanceRead(Resistance.value());
|
||||
resistance_received = true;
|
||||
}
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
|
||||
} else if(!DU30_bike) {
|
||||
@@ -901,6 +1129,9 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
update_hr_from_external();
|
||||
}
|
||||
|
||||
if(resistance_received && requestPower == -1)
|
||||
_inclinationResistanceTable.collectData(Inclination.value(), Resistance.value(), m_watt.value());
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
@@ -955,7 +1186,18 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
|
||||
qDebug() << s->serviceUuid() << QStringLiteral("connected!");
|
||||
|
||||
if (settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || ICSE || SCH_190U || DOMYOS || SMB1) {
|
||||
if (ICSE) {
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
QBluetoothUuid CSCService((quint16)0x1816);
|
||||
if (s->serviceUuid() != ftmsService && s->serviceUuid() != CSCService) {
|
||||
qDebug() << QStringLiteral("ICSE bike wants to be subscribed only to FTMS and CSC services in order "
|
||||
"to send metrics")
|
||||
<< s->serviceUuid();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SCH_190U || DOMYOS || SMB1 || FIT_BK) {
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
if (s->serviceUuid() != ftmsService) {
|
||||
qDebug() << QStringLiteral("hammer racer bike wants to be subscribed only to FTMS service in order "
|
||||
@@ -1015,7 +1257,7 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
}
|
||||
|
||||
QBluetoothUuid _zwiftPlayWriteCharControlPointId(QStringLiteral("00000003-19ca-4651-86e5-fa29dcdd09d1"));
|
||||
if (c.uuid() == _zwiftPlayWriteCharControlPointId && !DIRETO_XR) {
|
||||
if (c.uuid() == _zwiftPlayWriteCharControlPointId) {
|
||||
qDebug() << QStringLiteral("Zwift Play service and Control Point found");
|
||||
zwiftPlayWriteChar = c;
|
||||
zwiftPlayService = s;
|
||||
@@ -1028,10 +1270,14 @@ 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() &&
|
||||
(settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SMB1)) {
|
||||
(settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SMB1 || FIT_BK)) {
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -1075,9 +1321,10 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
|
||||
void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
|
||||
if (!autoResistance() || resistance_lvl_mode) {
|
||||
qDebug() << "ignoring routing FTMS packet to the bike from virtualbike because of auto resistance OFF or resistance lvl mode is on"
|
||||
<< characteristic.uuid() << newValue.toHex(' ');
|
||||
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
|
||||
if (!autoResistance() || resistance_lvl_mode || 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;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1155,10 +1402,12 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
#endif
|
||||
writeCharacteristicZwiftPlay((uint8_t*)message.data(), message.length(), "gearInclination", false, false);
|
||||
return;
|
||||
} else if(b.at(0) == FTMS_SET_TARGET_POWER && zwiftPlayService != nullptr && gears_zwift_ratio) {
|
||||
} 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) {
|
||||
}
|
||||
// gears on erg mode is quite useless and it's confusing
|
||||
/* else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) {
|
||||
lastPacketFromFTMS.clear();
|
||||
for(int i=0; i<b.length(); i++)
|
||||
lastPacketFromFTMS.append(b.at(i));
|
||||
@@ -1170,7 +1419,7 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
b[1] = power & 0xFF;
|
||||
b[2] = power >> 8;
|
||||
qDebug() << "applying gears mod" << gears() << gearsZwiftRatio() << power;
|
||||
}
|
||||
}*/
|
||||
|
||||
writeCharacteristic((uint8_t*)b.data(), b.length(), "injectWrite ", false, true);
|
||||
}
|
||||
@@ -1273,6 +1522,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if((bluetoothDevice.name().toUpper().startsWith("SCH_190U"))) {
|
||||
qDebug() << QStringLiteral("SCH_190U found");
|
||||
SCH_190U = true;
|
||||
max_resistance = 100;
|
||||
} else if(bluetoothDevice.name().toUpper().startsWith("D2RIDE")) {
|
||||
qDebug() << QStringLiteral("D2RIDE found");
|
||||
D2RIDE = true;
|
||||
@@ -1301,6 +1551,40 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("LYDSTO"))) {
|
||||
qDebug() << QStringLiteral("LYDSTO found");
|
||||
LYDSTO = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("SL010-"))) {
|
||||
qDebug() << QStringLiteral("SL010 found");
|
||||
SL010 = true;
|
||||
max_resistance = 25;
|
||||
resistance_lvl_mode = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("REEBOK"))) {
|
||||
qDebug() << QStringLiteral("REEBOK found");
|
||||
REEBOK = true;
|
||||
max_resistance = 32;
|
||||
resistance_lvl_mode = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("TITAN 7000"))) {
|
||||
qDebug() << QStringLiteral("Titan 7000 found");
|
||||
TITAN_7000 = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("T2 "))) {
|
||||
qDebug() << QStringLiteral("T2 found");
|
||||
T2 = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith(QStringLiteral("FIT-BK-")))) {
|
||||
qDebug() << QStringLiteral("FIT-BK found");
|
||||
FIT_BK = true;
|
||||
ergModeSupported = false; // this bike doesn't have ERG mode natively
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith(QStringLiteral("EXPERT-SX9")))) {
|
||||
qDebug() << QStringLiteral("EXPERT-SX9 found");
|
||||
EXPERT_SX9 = true;
|
||||
ergModeSupported = false; // this bike doesn't have ERG mode natively
|
||||
} else if (((bluetoothDevice.name().toUpper().startsWith("YS_G1MPLUS")))) {
|
||||
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!";
|
||||
}
|
||||
|
||||
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {
|
||||
@@ -1379,7 +1663,7 @@ double ftmsbike::maxGears() {
|
||||
QSettings settings;
|
||||
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
|
||||
|
||||
if((zwiftPlayService != nullptr || DIRETO_XR) && gears_zwift_ratio) {
|
||||
if((zwiftPlayService != nullptr) && gears_zwift_ratio) {
|
||||
wheelCircumference::GearTable g;
|
||||
return g.maxGears;
|
||||
} else if(WATTBIKE) {
|
||||
@@ -1393,7 +1677,7 @@ double ftmsbike::minGears() {
|
||||
QSettings settings;
|
||||
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
|
||||
|
||||
if((zwiftPlayService != nullptr || DIRETO_XR) && gears_zwift_ratio ) {
|
||||
if((zwiftPlayService != nullptr) && gears_zwift_ratio ) {
|
||||
return 1;
|
||||
} else if(WATTBIKE) {
|
||||
return 1;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
#include "wheelcircumference.h"
|
||||
#include "devices/bike.h"
|
||||
#include "inclinationresistancetable.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
@@ -104,8 +105,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;
|
||||
@@ -122,6 +125,7 @@ class ftmsbike : public bike {
|
||||
|
||||
bool resistance_lvl_mode = false;
|
||||
bool resistance_received = false;
|
||||
inclinationResistanceTable _inclinationResistanceTable;
|
||||
|
||||
bool DU30_bike = false;
|
||||
bool ICSE = false;
|
||||
@@ -137,9 +141,25 @@ class ftmsbike : public bike {
|
||||
bool BIKE_ = false;
|
||||
bool SMB1 = false;
|
||||
bool LYDSTO = false;
|
||||
bool SL010 = false;
|
||||
bool REEBOK = false;
|
||||
bool TITAN_7000 = false;
|
||||
bool T2 = false;
|
||||
bool FIT_BK = false;
|
||||
bool YS_G1MPLUS = false;
|
||||
bool EXPERT_SX9 = false;
|
||||
bool PM5 = false;
|
||||
bool THINK_X = false;
|
||||
|
||||
int16_t T2_lastGear = 0;
|
||||
|
||||
uint8_t battery_level = 0;
|
||||
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
QDateTime lastGoodCadence = QDateTime::currentDateTime();
|
||||
double lastRawRequestedInclinationValue = -100;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
@@ -78,7 +78,7 @@ void ftmsrower::update() {
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
if(I_ROWER) {
|
||||
if(I_ROWER || ROWER) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(write, sizeof(write), "start", false, true);
|
||||
|
||||
@@ -412,7 +412,7 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
connect(s, &QLowEnergyService::descriptorWritten, this, &ftmsrower::descriptorWritten);
|
||||
connect(s, &QLowEnergyService::descriptorRead, this, &ftmsrower::descriptorRead);
|
||||
|
||||
if (I_ROWER) {
|
||||
if (I_ROWER || ROWER) {
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
if (s->serviceUuid() != ftmsService) {
|
||||
qDebug() << QStringLiteral("I-ROWER wants to be subscribed only to FTMS service in order to send metrics")
|
||||
@@ -612,7 +612,10 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << "DFIT_L_R found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("I-ROWER"))) {
|
||||
I_ROWER = true;
|
||||
qDebug() << "I_ROWER found!";
|
||||
qDebug() << "I_ROWER found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("IROWER "))) {
|
||||
ROWER = true;
|
||||
qDebug() << "ROWER found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) {
|
||||
PM5 = true;
|
||||
qDebug() << "PM5 found!";
|
||||
|
||||
@@ -73,6 +73,7 @@ class ftmsrower : public rower {
|
||||
bool WATER_ROWER = false;
|
||||
bool DFIT_L_R = false;
|
||||
bool I_ROWER = false;
|
||||
bool ROWER = false;
|
||||
QDateTime lastStroke = QDateTime::currentDateTime();
|
||||
double lastStrokesCount = 0;
|
||||
|
||||
|
||||
@@ -927,6 +927,10 @@ void horizontreadmill::update() {
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
// this treadmill can't go below 1
|
||||
if(mobvoi_tmp_treadmill && requestSpeed < 1)
|
||||
requestSpeed = -1;
|
||||
|
||||
if (requestSpeed != -1) {
|
||||
bool minSpeed =
|
||||
fabs(requestSpeed - float_one_point_round(currentSpeed().value())) >= (minStepSpeed() - 0.09);
|
||||
@@ -1216,7 +1220,7 @@ void horizontreadmill::forceSpeed(double requestSpeed) {
|
||||
}
|
||||
} else if (gattFTMSService) {
|
||||
// for the Tecnogym Myrun
|
||||
if(!anplus_treadmill && !trx3500_treadmill && !wellfit_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO) {
|
||||
if(!anplus_treadmill && !trx3500_treadmill && !wellfit_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO && !T3G_PRO && !T3G_ELITE) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), "requestControl", false,
|
||||
false);
|
||||
@@ -1286,7 +1290,7 @@ void horizontreadmill::forceIncline(double requestIncline) {
|
||||
}
|
||||
} else if (gattFTMSService) {
|
||||
// for the Tecnogym Myrun
|
||||
if(!anplus_treadmill && !trx3500_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO) {
|
||||
if(!anplus_treadmill && !trx3500_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO && !T3G_PRO && !T3G_ELITE) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), "requestControl", false,
|
||||
false);
|
||||
@@ -1432,6 +1436,44 @@ void horizontreadmill::forceIncline(double requestIncline) {
|
||||
writeS[1] = 0x00;
|
||||
writeS[2] = 0x00;
|
||||
}
|
||||
} else if(T3G_PRO || T3G_ELITE) {
|
||||
if(requestInclination > 0 && requestInclination < 1) {
|
||||
writeS[1] = 0x10;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 1 && requestInclination < 2) {
|
||||
writeS[1] = 0x1a;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 2 && requestInclination < 3) {
|
||||
writeS[1] = 0x24;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 3 && requestInclination < 4) {
|
||||
writeS[1] = 0x2e;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 4 && requestInclination < 5) {
|
||||
writeS[1] = 0x38;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 5 && requestInclination < 6) {
|
||||
writeS[1] = 0x42;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 6 && requestInclination < 7) {
|
||||
writeS[1] = 0x4c;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 7 && requestInclination < 8) {
|
||||
writeS[1] = 0x56;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 8 && requestInclination < 9) {
|
||||
writeS[1] = 0x60;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 9 && requestInclination < 10) {
|
||||
writeS[1] = 0x6A;
|
||||
writeS[2] = 0x00;
|
||||
} else if(requestInclination >= 10 && requestInclination < 11) {
|
||||
writeS[1] = 0x74;
|
||||
writeS[2] = 0x00;
|
||||
} else {
|
||||
writeS[1] = 0x00;
|
||||
writeS[2] = 0x00;
|
||||
}
|
||||
} else {
|
||||
if(HORIZON_78AT_treadmill)
|
||||
requestIncline = requestIncline / 2.0;
|
||||
@@ -1741,6 +1783,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
} else if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACD)) {
|
||||
bool horizon_treadmill_7_0_at_24 = settings.value(QZSettings::horizon_treadmill_7_0_at_24, QZSettings::default_horizon_treadmill_7_0_at_24).toBool();
|
||||
lastPacket = newValue;
|
||||
|
||||
// default flags for this treadmill is 84 04
|
||||
@@ -1781,7 +1824,8 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
// this treadmill sends the speed in miles!
|
||||
speed *= miles_conversion;
|
||||
}
|
||||
parseSpeed(speed);
|
||||
if(!mobvoi_tmp_treadmill || (mobvoi_tmp_treadmill && !horizonPaused))
|
||||
parseSpeed(speed);
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
|
||||
}
|
||||
@@ -1914,16 +1958,37 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
} else {
|
||||
emit debug(QStringLiteral("Error on parsing heart!"));
|
||||
}
|
||||
// index += 1; //NOTE: clang-analyzer-deadcode.DeadStores
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (Flags.metabolic) {
|
||||
index += 1;
|
||||
// todo
|
||||
}
|
||||
|
||||
if (Flags.elapsedTime) {
|
||||
// todo
|
||||
if (index + 1 < newValue.length()) {
|
||||
static uint16_t old_local_elapsed = 0;
|
||||
static QDateTime last_local_elapsed_change = QDateTime::currentDateTime();
|
||||
uint16_t local_elapsed = ((((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
qDebug() << QStringLiteral("Local Time Elapsed") << local_elapsed;
|
||||
// this treadmill sends here if the user is really running
|
||||
if(old_local_elapsed != local_elapsed) {
|
||||
last_local_elapsed_change = QDateTime::currentDateTime();
|
||||
horizonPaused = false;
|
||||
}
|
||||
QDateTime current_time = QDateTime::currentDateTime();
|
||||
// Only if more than 2 seconds have passed since the last update
|
||||
if(mobvoi_tmp_treadmill && last_local_elapsed_change.secsTo(current_time) >= 2) {
|
||||
Speed = 0;
|
||||
horizonPaused = true;
|
||||
qDebug() << "Forcing Speed to 0 since the treadmill saws that the user is not really running";
|
||||
}
|
||||
old_local_elapsed = local_elapsed;
|
||||
}
|
||||
index += 2;
|
||||
}
|
||||
|
||||
if (Flags.remainingTime) {
|
||||
@@ -2489,6 +2554,12 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("FIT-"))) {
|
||||
qDebug() << QStringLiteral("FIT- found");
|
||||
FIT = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("3G PRO "))) {
|
||||
qDebug() << QStringLiteral("3G PRO");
|
||||
T3G_PRO = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("3G ELITE "))) {
|
||||
qDebug() << QStringLiteral("3G ELITE");
|
||||
T3G_ELITE = true;
|
||||
}
|
||||
|
||||
if (device.name().toUpper().startsWith(QStringLiteral("TRX3500"))) {
|
||||
@@ -2609,7 +2680,7 @@ bool horizontreadmill::autoStartWhenSpeedIsGreaterThenZero() {
|
||||
|
||||
// the horizon starts with a strange speed, since that i can auto start (maybe the best way to solve this
|
||||
// is to understand why it's starting with this strange speed)
|
||||
if (!horizon_paragon_x && !horizon_treadmill_7_8)
|
||||
if (!horizon_paragon_x && !horizon_treadmill_7_8 && !mobvoi_tmp_treadmill)
|
||||
return false;
|
||||
|
||||
if ((lastStop == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStop + 25000)) && requestStop == -1)
|
||||
@@ -3213,7 +3284,7 @@ void horizontreadmill::testProfileCRC() {
|
||||
double horizontreadmill::minStepInclination() {
|
||||
QSettings settings;
|
||||
bool toorx_ftms_treadmill = settings.value(QZSettings::toorx_ftms_treadmill, QZSettings::default_toorx_ftms_treadmill).toBool();
|
||||
if (kettler_treadmill || trx3500_treadmill || toorx_ftms_treadmill || sole_tt8_treadmill || ICONCEPT_FTMS_treadmill || SW_TREADMILL || sole_s77_treadmill || FIT)
|
||||
if (kettler_treadmill || trx3500_treadmill || toorx_ftms_treadmill || sole_tt8_treadmill || ICONCEPT_FTMS_treadmill || SW_TREADMILL || sole_s77_treadmill || FIT || T3G_PRO || T3G_ELITE)
|
||||
return 1.0;
|
||||
else
|
||||
return 0.5;
|
||||
|
||||
@@ -108,6 +108,8 @@ class horizontreadmill : public treadmill {
|
||||
bool YPOO_MINI_PRO = false;
|
||||
bool MX_TM = false;
|
||||
bool FIT = false;
|
||||
bool T3G_PRO = false;
|
||||
bool T3G_ELITE = false;
|
||||
|
||||
void testProfileCRC();
|
||||
void updateProfileCRC();
|
||||
|
||||
@@ -77,15 +77,13 @@ void kineticinroadbike::writeCharacteristic(uint8_t *data, uint8_t data_len, con
|
||||
}
|
||||
|
||||
void kineticinroadbike::forceResistance(resistance_t requestResistance) {
|
||||
/*uint8_t noOpData[] = {0xf0, 0xb1, 0x01, 0x00, 0x00};
|
||||
|
||||
noOpData[3] = requestResistance;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
|
||||
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
|
||||
}
|
||||
|
||||
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("force resistance"), false, true);*/
|
||||
if (noWriteResistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use Smart Control brake mode for direct resistance control
|
||||
smart_control_set_mode_brake_data cmd = smart_control_set_mode_brake_command(requestResistance);
|
||||
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set brake resistance"), false, true);
|
||||
}
|
||||
|
||||
void kineticinroadbike::update() {
|
||||
@@ -108,6 +106,23 @@ void kineticinroadbike::update() {
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
if (requestInclination != -100) {
|
||||
qDebug() << QStringLiteral("writing inclination ") + QString::number(requestInclination);
|
||||
forceInclination(requestInclination);
|
||||
if(!VirtualBike() || (VirtualBike() && !VirtualBike()->ftmsDeviceConnected())) {
|
||||
Inclination = requestInclination;
|
||||
}
|
||||
requestInclination = -100;
|
||||
requestResistance = -1; // Clear resistance request when handling inclination
|
||||
}
|
||||
|
||||
if (requestPower != -1) {
|
||||
qDebug() << QStringLiteral("writing power ") + QString::number(requestPower);
|
||||
changePower(requestPower);
|
||||
requestPower = -1;
|
||||
requestResistance = -1; // Clear resistance request when handling power
|
||||
}
|
||||
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance > max_resistance)
|
||||
requestResistance = max_resistance;
|
||||
@@ -120,6 +135,7 @@ void kineticinroadbike::update() {
|
||||
}
|
||||
requestResistance = -1;
|
||||
}
|
||||
|
||||
if (requestStart != -1) {
|
||||
qDebug() << QStringLiteral("starting...");
|
||||
|
||||
@@ -136,6 +152,42 @@ void kineticinroadbike::update() {
|
||||
}
|
||||
}
|
||||
|
||||
void kineticinroadbike::changePower(int32_t power) {
|
||||
if (noWriteResistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
RequestedPower = power;
|
||||
|
||||
if (power < 0)
|
||||
power = 0;
|
||||
|
||||
// Use Smart Control ERG mode for power-based training
|
||||
smart_control_set_mode_erg_data cmd = smart_control_set_mode_erg_command((uint16_t)power);
|
||||
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set ERG power"), false, true);
|
||||
}
|
||||
|
||||
void kineticinroadbike::forceInclination(double inclination) {
|
||||
if (noWriteResistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update resistance to match inclination (for UI consistency)
|
||||
Resistance = inclination;
|
||||
|
||||
// Smart Control simulation parameters
|
||||
QSettings settings;
|
||||
float weightKG = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
float rollingCoeff = 0.004f; // Standard rolling resistance for asphalt
|
||||
float windCoeff = 0.6f; // Standard wind resistance coefficient
|
||||
float windSpeedMPS = 0.0f; // No headwind/tailwind
|
||||
|
||||
// Use Smart Control simulation mode for inclination control
|
||||
smart_control_set_mode_simulation_data cmd = smart_control_set_mode_simulation_command(
|
||||
weightKG, rollingCoeff, windCoeff, (float)inclination, windSpeedMPS);
|
||||
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set simulation inclination"), false, true);
|
||||
}
|
||||
|
||||
void kineticinroadbike::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
|
||||
}
|
||||
|
||||
@@ -44,10 +44,11 @@ class kineticinroadbike : public bike {
|
||||
resistance_t pelotonToBikeResistance(int pelotonResistance) override;
|
||||
resistance_t maxResistance() override { return max_resistance; }
|
||||
resistance_t resistanceFromPowerRequest(uint16_t power) override;
|
||||
void changePower(int32_t power) override;
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
const resistance_t max_resistance = 32;
|
||||
const resistance_t max_resistance = 999;
|
||||
double bikeResistanceToPeloton(double resistance);
|
||||
double GetDistanceFromPacket(const QByteArray &packet);
|
||||
QTime GetElapsedFromPacket(const QByteArray &packet);
|
||||
@@ -56,6 +57,7 @@ class kineticinroadbike : public bike {
|
||||
bool wait_for_response = false);
|
||||
void startDiscover();
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void forceInclination(double inclination);
|
||||
uint16_t watts() override;
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user