Compare commits

..

13 Commits

Author SHA1 Message Date
Roberto Viola
a1ea0e18a1 Update QZAdbRemote.java 2023-12-28 11:36:50 +01:00
Roberto Viola
34025b2542 Merge branch 'master' into ios_adb_thread 2023-12-28 11:15:43 +01:00
Roberto Viola
85f5275342 Update nordictrackifitadbtreadmill.cpp 2023-12-28 11:14:56 +01:00
Roberto Viola
e25ea93c38 fixing android rx 2023-12-28 11:06:53 +01:00
Roberto Viola
26e8da19f2 adb treadmill done too 2023-12-14 16:14:09 +01:00
Roberto Viola
2c5f5b5554 Merge branch 'master' into ios_adb_thread 2023-12-14 15:19:53 +01:00
Roberto Viola
984ccef3d6 adding the android part 2023-12-14 15:17:06 +01:00
Roberto Viola
ff8b917744 adding debug logs 2023-11-29 11:39:41 +01:00
Roberto Viola
f1134b1c51 added some print debug 2023-11-27 17:14:21 +01:00
Roberto Viola
766faeb9b5 No connection to x22i (Issue #65) 2023-11-27 15:19:30 +01:00
Roberto Viola
98bf761c87 Merge branch 'master' into ios_adb_thread 2023-11-27 12:16:18 +01:00
Roberto Viola
2922d231be Merge branch 'master' into ios_adb_thread 2023-11-27 11:54:48 +01:00
Roberto Viola
43772ea906 No connection to x22i (Issue #65) 2023-11-27 11:47:52 +01:00
1315 changed files with 23216 additions and 210811 deletions

View File

@@ -1,4 +1,3 @@
# This is a basic workflow to help you get started with Actions
name: CI
@@ -124,11 +123,10 @@ jobs:
cd ../..
- name: Secrets
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/main'
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
@@ -142,7 +140,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.* output/
cp qdomyos-zwift.exe output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp "C:/mingw64/bin/libwinpthread-1.dll" .
@@ -169,7 +167,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.* output/
cp qdomyos-zwift.exe output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp "C:/mingw64/bin/libwinpthread-1.dll" .
@@ -197,21 +195,21 @@ jobs:
if: ${{ ! matrix.config.python }}
- name: Archive windows binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: windows-binary
path: windows-binary.zip
if: matrix.config.python
- name: Archive windows binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: windows-binary-no-python
path: windows-binary-no-python.zip
if: ${{ ! matrix.config.python }}
# - name: Exit if not on master branch
# if: github.ref == 'refs/heads/master'
# if: github.ref == 'refs/heads/main'
# run: exit 1
# - uses: actions/checkout@v3
@@ -305,7 +303,6 @@ 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 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
@@ -340,7 +337,7 @@ jobs:
# This workflow contains a single job called "build"
linux-x86-build:
# The type of runner that the job will run on
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
@@ -436,7 +433,7 @@ jobs:
run: qmake; make -j8
- name: Archive linux-desktop binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: linux-desktop-binary
path: src/qdomyos-zwift
@@ -445,7 +442,7 @@ jobs:
run: cd tst; GTEST_OUTPUT=xml:test-results/ GTEST_COLOR=1 ./qdomyos-zwift-tests; cd ..
- name: Upload test results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
if: failure()
with:
name: test_results_xml
@@ -510,7 +507,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-22.04
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
@@ -534,21 +531,11 @@ jobs:
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
# This token is provided by Actions, you do not need to create your own token
token: ${{ secrets.GITHUB_TOKEN }}
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
- name: Checkout submodules with specific branches
run: |
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
submodules: recursive # or 'true' if you want to check out only immediate submodules
- 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
@@ -583,7 +570,7 @@ jobs:
# waiting github.com/jurplel/install-qt-action/issues/63
- name: Install Qt Android
uses: jdpurcell/install-qt-action@v5
uses: jurplel/install-qt-action@v3
with:
version: '5.15.0'
host: 'linux'
@@ -598,7 +585,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '11.0.23+9'
java-version: '11'
- name: patching qt for bluetooth
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
@@ -606,6 +593,14 @@ jobs:
- 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
make -j8
make install
cd ../..
- name: Set Android NDK 21 && build
run: |
# Install NDK 21 after GitHub update
@@ -618,7 +613,6 @@ 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 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
@@ -628,14 +622,6 @@ jobs:
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
# QTHTTPSERVER must use the same NDK
cd src/qthttpserver
qmake
make -j8
make install
cd ../..
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-deployment-settings.json
cat src/android-qdomyos-zwift-deployment-settings.json
@@ -644,97 +630,24 @@ jobs:
run: cd src; androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab
- name: Archive apk binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: fdroid-android-trial
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/
android-emulator-test:
runs-on: ubuntu-latest
needs: android-build
steps:
- name: Checkout code
uses: actions/checkout@v2
# - name: Exit if not on master branch
# if: github.ref == 'refs/heads/main'
# run: exit 1
- 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
# - 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
ios-build:
# The type of runner that the job will run on
@@ -795,7 +708,6 @@ 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 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
@@ -902,55 +814,24 @@ jobs:
cd ../..
- name: Secrets
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/main'
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
run: |
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
qmake
nmake
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.* output/
cp qdomyos-zwift.exe output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp ../../../icons/iOS/iTunesArtwork@2x.png .
@@ -958,7 +839,6 @@ jobs:
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
@@ -969,25 +849,21 @@ jobs:
if: matrix.config.python
- name: Build without python
run: |
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
run: |
qmake
nmake
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.* output/
cp qdomyos-zwift.exe 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 "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 ..
@@ -1007,14 +883,14 @@ jobs:
if: ${{ ! matrix.config.python }}
- name: Archive windows binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: windows-msvc2019-binary
path: windows-msvc2019-binary.zip
if: matrix.config.python
- name: Archive windows binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: windows-msvc2019-binary-no-python
path: windows-msvc2019-binary-no-python.zip
@@ -1054,9 +930,6 @@ jobs:
repository: qt-labs/qthttpserver
path: "src/qthttpserver"
- name: Install CMake
uses: lukka/get-cmake@latest
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
@@ -1090,49 +963,18 @@ jobs:
cd ../..
- name: Secrets
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/main'
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
run: |
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
cd src
echo "#define AISERVER" >> aiserver.h
cd ..
@@ -1141,7 +983,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.* output/
cp qdomyos-zwift.exe output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp ../../../icons/iOS/iTunesArtwork@2x.png .
@@ -1151,7 +993,6 @@ jobs:
cp ../../windows/zwift-workout-ai-server.py zwift-workout.py
cp ../../windows/*.bat .
cp ../../../windows_openssl/*.* .
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\bin\*.* -Destination . -Verbose
mkdir adb
cp ../../adb/* adb/
cd ..
@@ -1165,323 +1006,19 @@ jobs:
run: Compress-Archive src/debug/output windows-msvc2019-ai-server-binary.zip
- name: Archive windows binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: windows-msvc2019-ai-server-binary
path: windows-msvc2019-ai-server-binary.zip
raspberry-pi-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Secrets
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
cd ..
- name: Build for Raspberry Pi
uses: docker://arm32v7/debian:bullseye-20241016
with:
args: >
bash -c "
set -ex &&
apt-get update &&
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 &&
export QT_SELECT=qt5 &&
export PATH=/usr/lib/qt5/bin:$PATH &&
cd /github/workspace &&
sed -i '/QtHttpServer/d' qdomyos-zwift.pro &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/#include <QtHttpServer/\/\/#include <QtHttpServer/' {} + &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/QHttpServer/\/\/QHttpServer/' {} + &&
cat qdomyos-zwift.pro &&
qmake &&
make -j$(nproc)
"
- name: Rename binary
run: mv src/qdomyos-zwift src/qdomyos-zwift-32bit
- name: Archive Raspberry Pi binary
uses: actions/upload-artifact@v4
with:
name: raspberry-pi-binary
path: src/qdomyos-zwift-32bit
raspberry-pi-build-and-image-64bit:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:master
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: v0.19.3
- name: Secrets
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
cd ..
- name: Build for Raspberry Pi 64-bit
uses: docker://arm64v8/debian:bullseye-20241016
with:
args: >
bash -c "
set -ex &&
apt-get update &&
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 &&
export QT_SELECT=qt5 &&
export PATH=/usr/lib/qt5/bin:$PATH &&
cd /github/workspace &&
sed -i '/QtHttpServer/d' qdomyos-zwift.pro &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/#include <QtHttpServer/\/\/#include <QtHttpServer/' {} + &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/QHttpServer/\/\/QHttpServer/' {} + &&
cat qdomyos-zwift.pro &&
qmake &&
make -j$(nproc)
"
- 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-22.04
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
needs: [linux-x86-build, window-msvc2019-build, window-msvc2022-build, ios-build, window-build, android-build, raspberry-pi-build]
needs: [linux-x86-build, window-msvc2019-build, ios-build, window-build, android-build] # Specify the job dependencies
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
- name: Update nightly release
uses: andelf/nightly-release@main
env:
@@ -1499,24 +1036,11 @@ 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-binary/qdomyos-zwift-64bit

1
.gitignore vendored
View File

@@ -50,4 +50,3 @@ 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

6
.gitmodules vendored
View File

@@ -12,11 +12,7 @@
[submodule "tst/googletest"]
path = tst/googletest
url = https://github.com/google/googletest.git
tag = release-1.12.1
branch = tags/release-1.12.1
[submodule "src/qthttpserver"]
path = src/qthttpserver
url = https://github.com/qt-labs/qthttpserver
[submodule "zwiftplay"]
path = zwiftplay
url = https://github.com/cagnulein/zwiftplay.git
branch = lib

16
.vscode/launch.json vendored
View File

@@ -1,16 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "(Windows) Launch",
"type": "cppvsdbg",
"request": "launch",
"program": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/qdomyos-zwift.exe",
"symbolSearchPath": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/qdomyos-zwift.pdb",
"sourceFileMap": {
"d:/a/qdomyos-zwift/qdomyos-zwift": "c:/work/qdomyos-zwift/",
"compiled_source_path": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/"
}
}
]
}

374
CLAUDE.md
View File

@@ -1,374 +0,0 @@
# 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

View File

@@ -1,53 +0,0 @@
#include <NimBLEDevice.h>
#define INDOOR_BIKE_DATA_UUID "00002AD2-0000-1000-8000-00805f9b34fb"
#define CUSTOM_SERVICE_UUID "ce060000-43e5-11e4-916c-0800200c9a66"
NimBLEServer* pServer = nullptr;
NimBLECharacteristic* pIndoorBikeDataChar = nullptr;
class ServerCallbacks: public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer) {
Serial.println("Client connected");
};
void onDisconnect(NimBLEServer* pServer) {
Serial.println("Client disconnected");
}
};
void setup() {
Serial.begin(115200);
Serial.println("Starting NimBLE Server");
NimBLEDevice::init("PM5 431431183 Row");
pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
NimBLEService* pFtmService = pServer->createService("1826");
//NimBLEService* pCustomService = pServer->createService(CUSTOM_SERVICE_UUID);
pIndoorBikeDataChar = pFtmService->createCharacteristic(
INDOOR_BIKE_DATA_UUID,
NIMBLE_PROPERTY::READ |
NIMBLE_PROPERTY::NOTIFY
);
pFtmService->start();
//pCustomService->start();
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID(pFtmService->getUUID());
//pAdvertising->addServiceUUID(CUSTOM_SERVICE_UUID);
const std::string data = { 0x01, 0x10, 0x00 }; // Imposta i valori desiderati
pAdvertising->setServiceData(pFtmService->getUUID(), data);
pAdvertising->start();
Serial.println("Advertising started");
}
void loop() {
// Metti qui il tuo codice principale, da eseguire ripetutamente
// Ad esempio, potresti aggiornare il valore della caratteristica Indoor Bike Data
}

View File

@@ -69,7 +69,6 @@ Zwift bridge for Treadmills and Bike!
|Echelon app Compatibility|X|||||
|Wahoo Dircon Compatibility|X|X|X|X|in order to send data to Zwift or RGT with Wifi only!|
|One device only support for Zwift and Wahoo RGT|X|X|X|X|using Wahoo Dircon https://www.youtube.com/watch?v=gYYUXNWFAok|
|BitGym Compatibility|X|X|X|X||
# Training Program
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
@@ -96,36 +95,34 @@ Zwift bridge for Treadmills and Bike!
|:---|:---:|:---:|:---:|:---:|---:|
|Resistance shifting with bluetooth remote|X||X|||
|TTS support|X|X|X|X||
|Zwift Play & Click support|X|||||
|MQTT integration|X|X|X|X||
|OpenSoundControl integration|X|X|X|X||
### Installation
You can install it on multiple platforms.
Read the [installation procedure](docs/10_Installation.md)
You can install it on multiple platforms.
Read the [installation procedure](docs/10_Installation.md)
### Tested on
The QDomyos-Zwift application can run on [Macintosh or Linux devices](docs/10_Installation.md) iOS, and Android.
It supports any [FTMS-compatible application](docs/20_supported_devices_and_applications.md) software and most [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
You can run the app on [Macintosh or Linux devices](docs/10_Installation.md). IOS and Android are also supported.
QDomyos-Zwift works on every [FTMS-compatible application](docs/20_supported_devices_and_applications.md), and virtually any [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
### No GUI version
run as
$ sudo ./qdomyos-zwift -no-gui
$ sudo ./qdomyos-zwift -no-gui
### Reference
=> GitHub Repository: [QDomyos-Zwift on GitHub](https://github.com/ProH4Ck/treadmill-bridge)
https://github.com/ProH4Ck/treadmill-bridge
=> Treadmill Incline Reference: [What Is 10 Degrees in Incline on a Treadmill?](https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/)
https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/
=> Icon Attribution: Icons used in this documentation are from [Flaticon.com](https://www.flaticon.com)
Icons used in this documentation come from [flaticon.com](https://www.flaticon.com)
### Blog
=> Related Blog: [Roberto Viola's Blog](https://robertoviola.cloud)
https://robertoviola.cloud

View File

@@ -53,17 +53,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_accessibility_support_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bodymovin_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bootstrap_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_clipboard_support_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_devicediscovery_support_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_edid_support_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_eventdispatcher_support_private.pri \
@@ -76,9 +71,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_gui_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri \
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_httpserver.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_macextras.pri \
@@ -103,8 +95,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioning_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmldebug_private.pri \
@@ -117,16 +107,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmlworkerscript_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qtmultimediaquicktools_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2_private.pri \
@@ -140,21 +120,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_remoteobjects_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver.pri \
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sslserver.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_testlib.pri \
@@ -165,8 +136,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uiplugin.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel_private.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_websockets.pri \
@@ -225,26 +194,15 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtiff.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmedia_audioengine.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmultimedia_m3u.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpassthrucanbus.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpeakcanbus.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_cl.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_positionpoll.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_plugin.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_shakeplugin.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_generic.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_ios.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qttinycanbus.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtuiotouchplugin.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualcanbus.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_hangul.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_openwnn.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_pinyin.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_tcime.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_thai.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboardplugin.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtwebview_darwin.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwbmp.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebgl.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebp.pri \
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_scene2d.pri \
../../Qt/5.15.2/ios/mkspecs/features/qt_functions.prf \
@@ -261,9 +219,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/features/default_pre.prf \
../../Qt/5.15.2/ios/mkspecs/features/mac/default_pre.prf \
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_pre.prf \
../defaults.pri \
../src/purchasing/purchasing.pri \
../src/qdomyos-zwift.pri \
../../Qt/5.15.2/ios/mkspecs/features/resolve_config.prf \
../../Qt/5.15.2/ios/mkspecs/features/uikit/resolve_config.prf \
../../Qt/5.15.2/ios/mkspecs/features/default_post.prf \
@@ -271,9 +226,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_post.prf \
../../Qt/5.15.2/ios/mkspecs/macx-ios-clang/features/default_post.prf \
../../Qt/5.15.2/ios/mkspecs/features/mac/objective_c.prf \
../../Qt/5.15.2/ios/mkspecs/features/qmltypes.prf \
../../Qt/5.15.2/ios/mkspecs/features/metatypes.prf \
../../Qt/5.15.2/ios/mkspecs/features/ltcg.prf \
../../Qt/5.15.2/ios/mkspecs/features/qml_debug.prf \
../../Qt/5.15.2/ios/mkspecs/features/mac/mac.prf \
../../Qt/5.15.2/ios/mkspecs/features/uikit/bitcode.prf \
@@ -312,18 +264,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/lib/libqtharfbuzz_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Core_debug.prl \
../../Qt/5.15.2/ios/lib/libqtpcre2_debug.prl \
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfmediaplayer_debug.prl \
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_esri_debug.prl \
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_itemsoverlay_debug.prl \
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapbox_debug.prl \
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapboxgl_debug.prl \
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_nokia_debug.prl \
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_osm_debug.prl \
../../Qt/5.15.2/ios/plugins/webview/libqtwebview_darwin_debug.prl \
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfcamera_debug.prl \
../../Qt/5.15.2/ios/plugins/mediaservice/libqtmedia_audioengine_debug.prl \
../../Qt/5.15.2/ios/plugins/audio/libqtaudio_coreaudio_debug.prl \
../../Qt/5.15.2/ios/plugins/playlistformats/libqtmultimedia_m3u_debug.prl \
../../Qt/5.15.2/ios/plugins/imageformats/libqgif_debug.prl \
../../Qt/5.15.2/ios/plugins/imageformats/libqicns_debug.prl \
../../Qt/5.15.2/ios/plugins/imageformats/libqico_debug.prl \
@@ -348,51 +288,32 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_server_debug.prl \
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_tcp_debug.prl \
../../Qt/5.15.2/ios/plugins/bearer/libqgenericbearer_debug.prl \
../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios_debug.prl \
../../Qt/5.15.2/ios/plugins/sqldrivers/libqsqlite_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5HttpServer_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5SslServer_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Charts_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Widgets_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Location_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5PositioningQuick_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5QuickControls2_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Quick_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Multimedia_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5WebView_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Bluetooth_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Xml_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Positioning_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5QmlModels_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Qml_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5NetworkAuth_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5WebSockets_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Network_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5TextToSpeech_debug.prl \
../../Qt/5.15.2/ios/lib/libQt5Concurrent_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick.2/libqtquick2plugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/libqtquickcontrols2plugin_debug.prl \
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQml/libqmlplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Templates.2/libqtquicktemplates2plugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl \
../../Qt/5.15.2/ios/qml/QtQml/Models.2/libmodelsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQml/WorkerScript.2/libworkerscriptplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtWebView/libdeclarative_webview_debug.prl \
../../Qt/5.15.2/ios/qml/QtCharts/libqtchartsqml2_debug.prl \
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/libdialogplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtPositioning/libdeclarative_positioning_debug.prl \
../../Qt/5.15.2/ios/qml/QtLocation/libdeclarative_location_debug.prl \
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl \
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/Private/libdialogsprivateplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Controls/libqtquickcontrolsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/PrivateWidgets/libwidgetsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/Qt/labs/platform/libqtlabsplatformplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtMultimedia/libdeclarative_multimedia_debug.prl \
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Fusion/libqtquickcontrols2fusionstyleplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Universal/libqtquickcontrols2universalstyleplugin_debug.prl \
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Imagine/libqtquickcontrols2imaginestyleplugin_debug.prl
@@ -440,17 +361,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_accessibility_support_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bodymovin_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bootstrap_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_clipboard_support_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_devicediscovery_support_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_edid_support_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_eventdispatcher_support_private.pri:
@@ -463,9 +379,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_gui_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri:
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_httpserver.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_macextras.pri:
@@ -490,8 +403,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioning_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmldebug_private.pri:
@@ -504,16 +415,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmlworkerscript_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qtmultimediaquicktools_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2_private.pri:
@@ -527,21 +428,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_remoteobjects_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver.pri:
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sslserver.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_testlib.pri:
@@ -552,8 +444,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uiplugin.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel_private.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_websockets.pri:
@@ -612,26 +502,15 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtiff.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmedia_audioengine.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmultimedia_m3u.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpassthrucanbus.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpeakcanbus.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_cl.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_positionpoll.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_plugin.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_shakeplugin.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_generic.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_ios.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qttinycanbus.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtuiotouchplugin.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualcanbus.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_hangul.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_openwnn.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_pinyin.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_tcime.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_thai.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboardplugin.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtwebview_darwin.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwbmp.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebgl.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebp.pri:
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_scene2d.pri:
../../Qt/5.15.2/ios/mkspecs/features/qt_functions.prf:
@@ -648,9 +527,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/features/default_pre.prf:
../../Qt/5.15.2/ios/mkspecs/features/mac/default_pre.prf:
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_pre.prf:
../defaults.pri:
../src/purchasing/purchasing.pri:
../src/qdomyos-zwift.pri:
../../Qt/5.15.2/ios/mkspecs/features/resolve_config.prf:
../../Qt/5.15.2/ios/mkspecs/features/uikit/resolve_config.prf:
../../Qt/5.15.2/ios/mkspecs/features/default_post.prf:
@@ -658,9 +534,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_post.prf:
../../Qt/5.15.2/ios/mkspecs/macx-ios-clang/features/default_post.prf:
../../Qt/5.15.2/ios/mkspecs/features/mac/objective_c.prf:
../../Qt/5.15.2/ios/mkspecs/features/qmltypes.prf:
../../Qt/5.15.2/ios/mkspecs/features/metatypes.prf:
../../Qt/5.15.2/ios/mkspecs/features/ltcg.prf:
../../Qt/5.15.2/ios/mkspecs/features/qml_debug.prf:
../../Qt/5.15.2/ios/mkspecs/features/mac/mac.prf:
../../Qt/5.15.2/ios/mkspecs/features/uikit/bitcode.prf:
@@ -699,18 +572,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/lib/libqtharfbuzz_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Core_debug.prl:
../../Qt/5.15.2/ios/lib/libqtpcre2_debug.prl:
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfmediaplayer_debug.prl:
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_esri_debug.prl:
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_itemsoverlay_debug.prl:
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapbox_debug.prl:
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapboxgl_debug.prl:
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_nokia_debug.prl:
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_osm_debug.prl:
../../Qt/5.15.2/ios/plugins/webview/libqtwebview_darwin_debug.prl:
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfcamera_debug.prl:
../../Qt/5.15.2/ios/plugins/mediaservice/libqtmedia_audioengine_debug.prl:
../../Qt/5.15.2/ios/plugins/audio/libqtaudio_coreaudio_debug.prl:
../../Qt/5.15.2/ios/plugins/playlistformats/libqtmultimedia_m3u_debug.prl:
../../Qt/5.15.2/ios/plugins/imageformats/libqgif_debug.prl:
../../Qt/5.15.2/ios/plugins/imageformats/libqicns_debug.prl:
../../Qt/5.15.2/ios/plugins/imageformats/libqico_debug.prl:
@@ -735,51 +596,32 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_server_debug.prl:
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_tcp_debug.prl:
../../Qt/5.15.2/ios/plugins/bearer/libqgenericbearer_debug.prl:
../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios_debug.prl:
../../Qt/5.15.2/ios/plugins/sqldrivers/libqsqlite_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5HttpServer_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5SslServer_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Charts_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Widgets_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Location_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5PositioningQuick_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5QuickControls2_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Quick_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Multimedia_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5WebView_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Bluetooth_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Xml_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Positioning_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5QmlModels_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Qml_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5NetworkAuth_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5WebSockets_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Network_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5TextToSpeech_debug.prl:
../../Qt/5.15.2/ios/lib/libQt5Concurrent_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick.2/libqtquick2plugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/libqtquickcontrols2plugin_debug.prl:
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQml/libqmlplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Templates.2/libqtquicktemplates2plugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl:
../../Qt/5.15.2/ios/qml/QtQml/Models.2/libmodelsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQml/WorkerScript.2/libworkerscriptplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtWebView/libdeclarative_webview_debug.prl:
../../Qt/5.15.2/ios/qml/QtCharts/libqtchartsqml2_debug.prl:
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/libdialogplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtPositioning/libdeclarative_positioning_debug.prl:
../../Qt/5.15.2/ios/qml/QtLocation/libdeclarative_location_debug.prl:
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl:
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/Private/libdialogsprivateplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Controls/libqtquickcontrolsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/PrivateWidgets/libwidgetsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/Qt/labs/platform/libqtlabsplatformplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtMultimedia/libdeclarative_multimedia_debug.prl:
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Fusion/libqtquickcontrols2fusionstyleplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Universal/libqtquickcontrols2universalstyleplugin_debug.prl:
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Imagine/libqtquickcontrols2imaginestyleplugin_debug.prl:

View File

@@ -107,7 +107,6 @@ extension MainController: WorkoutTrackingDelegate {
WorkoutTracking.speed = WatchKitConnection.speed
WorkoutTracking.power = WatchKitConnection.power
WorkoutTracking.cadence = WatchKitConnection.cadence
WorkoutTracking.steps = WatchKitConnection.steps
if Locale.current.measurementSystem != "Metric" {
self.distanceLabel.setText("Distance \(String(format:"%.2f", WorkoutTracking.distance))")

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -27,7 +27,6 @@ class WatchKitConnection: NSObject {
public static var speed = 0.0
public static var cadence = 0.0
public static var power = 0.0
public static var steps = 0
weak var delegate: WatchKitConnectionDelegate?
private override init() {
@@ -77,10 +76,6 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
WatchKitConnection.power = dPower
let dCadence = Double(result["cadence"] as! Double)
WatchKitConnection.cadence = dCadence
if let stepsDouble = result["steps"] as? Double {
let iSteps = Int(stepsDouble)
WatchKitConnection.steps = iSteps
}
}, errorHandler: { (error) in
print(error)
})

View File

@@ -33,7 +33,6 @@ class WorkoutTracking: NSObject {
public static var cadenceSteps = 0
public static var speed = Double()
public static var power = Double()
public static var steps = Int()
public static var cadence = Double()
public static var lastDateMetric = Date()
var sport: Int = 0
@@ -54,26 +53,20 @@ extension WorkoutTracking {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .distanceCycling):
let distanceUnit = HKUnit.mile()
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
delegate?.didReceiveHealthKitDistanceCycling(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
let energyUnit = HKUnit.kilocalorie()
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
delegate?.didReceiveHealthKitActiveEnergyBurned(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
delegate?.didReceiveHealthKitHeartRate(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .stepCount):
@@ -233,11 +226,9 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: startDate,
start: workoutSession.startDate!,
end: Date())
workoutBuilder.add([sample]) {(success, error) in}
@@ -257,7 +248,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
start: workoutSession.startDate!,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -276,106 +267,8 @@ 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
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, // Use your steps quantity here
start: startDate,
end: Date())
// Add the steps sample to workout builder
workoutBuilder.add([sampleSteps]) { (success, error) in
if let error = error {
print(error)
}
// End the data collection
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
if let error = error {
print(error)
}
// Finish the workout and save total steps
self.workoutBuilder.finishWorkout { (workout, error) in
if let error = error {
print(error)
}
workout?.setValue(stepsQuantity, forKey: "totalSteps")
}
}
}
guard let quantityTypeDistance = HKQuantityType.quantityType(
forIdentifier: .distanceWalkingRunning) else {
return
@@ -383,7 +276,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
start: workoutSession.startDate!,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -509,7 +402,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 1) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
doubleValue: WorkoutTracking.power)
@@ -552,7 +445,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 2) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
doubleValue: WorkoutTracking.speed * 0.277778)

View File

@@ -1,96 +0,0 @@
# Define build image
FROM ubuntu:latest AS build
# Install essential build dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
git \
ca-certificates \
qtquickcontrols2-5-dev \
qtconnectivity5-dev \
qtbase5-private-dev \
qtpositioning5-dev \
libqt5charts5-dev \
libqt5networkauth5-dev \
libqt5websockets5-dev \
qml-module* \
libqt5texttospeech5-dev \
qtlocation5-dev \
qtmultimedia5-dev \
g++ \
make \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Define runtime image
FROM ubuntu:latest AS runtime
# Install essential runtime dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
libqt5bluetooth5 \
libqt5widgets5 \
libqt5positioning5 \
libqt5xml5 \
libqt5charts5 \
qt5-assistant \
libqt5networkauth5 \
libqt5websockets5 \
qml-module* \
libqt5texttospeech5 \
libqt5location5-plugins \
libqt5multimediawidgets5 \
libqt5multimedia5-plugins \
libqt5multimedia5 \
qml-module-qtquick-controls2 \
libqt5location5 \
bluez \
dbus \
tigervnc-standalone-server \
tigervnc-tools \
libgl1-mesa-dri \
xfonts-base \
x11-xserver-utils \
tigervnc-common \
net-tools \
&& rm -rf /var/lib/apt/lists/*
# Stage 1: Build
FROM build AS builder
# Clone the project and build it
WORKDIR /usr/local/src
RUN git clone --recursive https://github.com/cagnulein/qdomyos-zwift.git
WORKDIR /usr/local/src/qdomyos-zwift
RUN git submodule update --init src/smtpclient/ \
&& git submodule update --init src/qmdnsengine/ \
&& git submodule update --init tst/googletest/
WORKDIR /usr/local/src/qdomyos-zwift/src
RUN qmake qdomyos-zwift.pro \
&& make -j4
# Stage 2: Runtime
FROM runtime
# Copy the built binary to /usr/local/bin
COPY --from=builder /usr/local/src/qdomyos-zwift/src/qdomyos-zwift /usr/local/bin/qdomyos-zwift
# VNC configuration
RUN mkdir -p ~/.vnc && \
echo "securepassword" | vncpasswd -f > ~/.vnc/passwd && \
chmod 600 ~/.vnc/passwd
# .Xauthority configuration
RUN touch /root/.Xauthority
ENV DISPLAY=:99
# Start VNC server with authentication
CMD vncserver :99 -depth 24 -localhost no -xstartup qdomyos-zwift && \
sleep infinity

View File

@@ -1,2 +0,0 @@
#!/bin/bash
docker build -t qdomyos-zwift-vnc .

View File

@@ -1,10 +0,0 @@
services:
qdomyos-zwift-vnc:
image: qdomyos-zwift-vnc
container_name: qdomyos-zwift-vnc
privileged: true # Required for Bluetooth functionality
network_mode: "host" # Used to access host Bluetooth and D-Bus
volumes:
- /dev:/dev # Forward host devices (for Bluetooth)
- /run/dbus:/run/dbus # Forward D-Bus for Bluetooth interaction
restart: "no" # Do not restart the container automatically

View File

@@ -1,95 +0,0 @@
# Define build image
FROM ubuntu:latest AS build
# Install essential build dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
git \
ca-certificates \
qtquickcontrols2-5-dev \
qtconnectivity5-dev \
qtbase5-private-dev \
qtpositioning5-dev \
libqt5charts5-dev \
libqt5networkauth5-dev \
libqt5websockets5-dev \
qml-module* \
libqt5texttospeech5-dev \
qtlocation5-dev \
qtmultimedia5-dev \
g++ \
make \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Define runtime image
FROM ubuntu:latest AS runtime
# Install essential runtime dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
libqt5bluetooth5 \
libqt5widgets5 \
libqt5positioning5 \
libqt5xml5 \
libqt5charts5 \
qt5-assistant \
libqt5networkauth5 \
libqt5websockets5 \
qml-module* \
libqt5texttospeech5 \
libqt5location5-plugins \
libqt5multimediawidgets5 \
libqt5multimedia5-plugins \
libqt5multimedia5 \
qml-module-qtquick-controls2 \
libqt5location5 \
bluez \
dbus \
&& rm -rf /var/lib/apt/lists/*
# Stage 1: Build
FROM build AS builder
# Define variables for Qt versions
ARG QT_VERSION=5.15
ARG QT_SUBVERSION=5.15.13
ARG QT_WEBPLUGIN_NAME=qtwebglplugin-everywhere-opensource-src
# Build WebGL plugin
WORKDIR /usr/local/src
RUN wget https://download.qt.io/official_releases/qt/${QT_VERSION}/${QT_SUBVERSION}/submodules/${QT_WEBPLUGIN_NAME}-${QT_SUBVERSION}.zip \
&& unzip ${QT_WEBPLUGIN_NAME}-${QT_SUBVERSION}.zip \
&& mv *-${QT_SUBVERSION} qtwebglplugin-everywhere \
&& cd qtwebglplugin-everywhere \
&& qmake \
&& make
# Clone the project and build it
WORKDIR /usr/local/src
RUN git clone --recursive https://github.com/cagnulein/qdomyos-zwift.git
WORKDIR /usr/local/src/qdomyos-zwift
RUN git submodule update --init src/smtpclient/ \
&& git submodule update --init src/qmdnsengine/ \
&& git submodule update --init tst/googletest/
WORKDIR /usr/local/src/qdomyos-zwift/src
RUN qmake qdomyos-zwift.pro \
&& make -j4
# Stage 2: Runtime
FROM runtime
# Copy the built binary to /usr/local/bin
COPY --from=builder /usr/local/src/qdomyos-zwift/src/qdomyos-zwift /usr/local/bin/qdomyos-zwift
# Copy WebGL plugin to the appropriate location
COPY --from=builder /usr/local/src/qtwebglplugin-everywhere/plugins/platforms/libqwebgl.so /usr/lib/x86_64-linux-gnu/qt5/plugins/platforms/libqwebgl.so
# Set the default command to run the application with WebGL
CMD ["qdomyos-zwift", "-qml", "-platform", "webgl:port=8080"]

View File

@@ -1,2 +0,0 @@
#!/bin/bash
docker build -t qdomyos-zwift-webgl .

View File

@@ -1,19 +0,0 @@
services:
qdomyos-zwift-webgl:
image: qdomyos-zwift-webgl
container_name: qdomyos-zwift-webgl
privileged: true
network_mode: "host"
environment:
- DISPLAY=${DISPLAY}
volumes:
- /dev:/dev
- /run/dbus:/run/dbus
- ./.config:/root/.config
- /tmp/.X11-unix:/tmp/.X11-unix
stdin_open: true
tty: true
restart: "no"
# command: qdomyos-zwift -qml -platform webgl:port=8080
# command: ["qdomyos-zwift", "-no-gui"]

View File

@@ -10,7 +10,7 @@ These instructions build the app itself, not the test project.
```buildoutcfg
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd qdomyos-zwift
$ git submodule update --init src/smtpclient/
@@ -106,7 +106,7 @@ This operation takes a moment to complete.
#### Install qdomyos-zwift from sources
```bash
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
git clone https://github.com/cagnulein/qdomyos-zwift.git
cd qdomyos-zwift
git submodule update --init src/smtpclient/
@@ -117,11 +117,6 @@ qmake qdomyos-zwift.pro
make
```
If you need GUI also do a
```
apt install qml-module*
```
Please note :
- Don't build the application with `-j4` option (this will fail)
- Build operation is circa 45 minutes (subsequent builds are faster)
@@ -177,151 +172,6 @@ If everything is working as expected, **enable your service at boot time** :
Then reboot to check operations (`sudo reboot`)
### (optional) Treadmill Auto-Detection and Service Management
This section provides a reliable way to manage the QZ service based on the treadmill's power state. Using a `bluetoothctl`-based Bash script, this solution ensures the QZ service starts when the treadmill is detected and stops when it is not.
- **Bluetooth Discovery**: Monitors treadmill availability via `bluetoothctl`.
- **Service Control**: Automatically starts and stops the QZ service.
- **Logging**: Tracks treadmill status and actions in a log file.
**Notes:**
- Ensure `bluetoothctl` is installed and working on your system.
- Replace `I_TL` in the script with your treadmill's Bluetooth name. You can find your device name via `bluetoothctl scan on`
- Adjust the sleep interval (`sleep 30`) in the script as needed for your use case.
Step 1: Save the following script as `/root/qz-treadmill-monitor.sh`:
```bash
#!/bin/bash
LOG_FILE="/var/log/qz-treadmill-monitor.log"
TARGET_DEVICE="I_TL"
SCAN_INTERVAL=30 # Time in seconds between checks
SERVICE_NAME="qz"
DEBUG_LOG_DIR="/var/log" # Directory where QZ debug logs are stored
ERROR_MESSAGE="BTLE stateChanged InvalidService"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
is_service_running() {
systemctl is-active --quiet "$SERVICE_NAME"
return $?
}
scan_for_device() {
log "Starting Bluetooth scan for $TARGET_DEVICE..."
# Run bluetoothctl scan in the background and capture output
bluetoothctl scan on &>/dev/null &
SCAN_PID=$!
# Allow some time for devices to appear
sleep 5
# Check if the target device appears in the list
bluetoothctl devices | grep -q "$TARGET_DEVICE"
DEVICE_FOUND=$?
# Stop scanning
kill "$SCAN_PID"
bluetoothctl scan off &>/dev/null
if [ $DEVICE_FOUND -eq 0 ]; then
log "Device '$TARGET_DEVICE' found."
return 0
else
log "Device '$TARGET_DEVICE' not found."
return 1
fi
}
restart_qz_on_error() {
# Get the current date
CURRENT_DATE=$(date '+%a_%b_%d')
# Find the latest QZ debug log file for today
LATEST_LOG=$(ls -t "$DEBUG_LOG_DIR"/debug-"$CURRENT_DATE"_*.log 2>/dev/null | head -n 1)
if [ -z "$LATEST_LOG" ]; then
log "No QZ debug log found for today."
return 0
fi
log "Checking latest log file: $LATEST_LOG for errors..."
# Search the latest log for the error message
if grep -q "$ERROR_MESSAGE" "$LATEST_LOG"; then
log "***** Error detected in QZ log: $ERROR_MESSAGE *****"
log "Restarting QZ service..."
systemctl restart "$SERVICE_NAME"
else
log "No errors detected in $LATEST_LOG."
fi
}
manage_service() {
local device_found=$1
if $device_found; then
if ! is_service_running; then
log "***** Starting QZ service... *****"
systemctl start "$SERVICE_NAME"
else
log "QZ service is already running."
restart_qz_on_error # Check the log for errors when QZ is already running
fi
else
if is_service_running; then
log "***** Stopping QZ service... *****"
systemctl stop "$SERVICE_NAME"
else
log "QZ service is already stopped."
fi
fi
}
while true; do
log "Checking for treadmill status..."
if scan_for_device; then
manage_service true
else
manage_service false
fi
log "Waiting for $SCAN_INTERVAL seconds before next check..."
sleep "$SCAN_INTERVAL"
done
```
Step2: To ensure the script runs continuously, create a systemd service file at `/etc/systemd/system/qz-treadmill-monitor.service`
```bash
[Unit]
Description=QZ Treadmill Monitor Service
After=bluetooth.service
[Service]
Type=simple
ExecStart=/root/qz-treadmill-monitor.sh
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
```
Step 3: Enable and Start the Service
```bash
sudo systemctl daemon-reload
sudo systemctl enable qz-treadmill-monitor
sudo systemctl start qz-treadmill-monitor
```
Monitor logs are written to `/var/log/qz-treadmill-monitor.log`. Use the following command to check logs in real-time:
```bash
sudo tail -f /var/log/qz-treadmill-monitor.log
```
### (optional) Enable overlay FS

View File

@@ -1,396 +0,0 @@
# QDomyos-Zwift Guide to Writing Unit Tests
## About
The testing project tst/qdomyos-zwift-tests.pro contains test code that uses the Google Test library.
## Adding a new device
New devices are added to the main QZ application by creating or modifying a subclass of the bluetoothdevice class.
At minimum, each device has a corresponding BluetoothDeviceTestData object constructed in the DeviceTestDataIndex class in the test project, which is coded to provide information to the test framework to generate tests for device detection and potentially other things.
In the test project
* add a new device name constant to the DeviceIndex class.
* locate the implementation of DeviceTestDataindex::Initialize and build the test data from a call to DeviceTestDataIndex::RegisterNewDeviceTestData(...)
* pass the device name constant defined in the DeviceIndex class to the call to DeviceTestDataIndex::RegisterNewDeviceTestData(...).
The tests are not organised around real devices that are handled, but the bluetoothdevice subclass that handles them - the "driver" of sorts.
You need to provide the following:
- patterns for valid names (e.g. equals a value, starts with a value, case sensitivity, specific length)
- invalid names to ensure the device is not identified when the name is invalid
- configuration settings that are required for the device to be detected, including bluetooth device information configuration
- invalid configurations to test that the device is not detected, e.g. when it's disabled in the settings, but the name is correct
- exclusion devices: for example if a device with the same name but of a higher priority type is detected, this device should not be detected
## Tools in the Test Framework
### TestSettings
The detection of many devices depends on settings that are accessed programmatically using the QSettings class and the constants in the QZSettings namespace. The TestSettings class stores a QSettings object with what is intended to be a unique application and organisation name, to keep the configuration it represents seperate from others in the system. It also makes the stored QSettings object the default by setting the QCoreApplication's organisation and application names to those of the QSettings object. The original values are restored by calling the deactivate() function or on object destruction.
i.e. a test will
* apply a configuration from a TestSettings object
* perform device detection
* use the TestSettings object to restore the previous settings either directly or by letting its destructor be called.
### DeviceDiscoveryInfo
This class:
* stores values for a specific subset of the QZSettings keys.
* provides methods to read and write the values it knows about from and to a QSettings object.
* provides a QBluetoothDeviceInfo object configured with the device name currently being tested.
It is used in conjunction with a TestSettings object to write a configuration during a test.
## Writing a device detection test
Because of the way the BluetoothDeviceTestDataBuilder currently works, it may be necessary to define multiple test data objects to cover the various cases.
For example, if any of a list of names is enough to identify a device, or another group of names but with a certain service in the bluetooth device info, that will require multiple test data objects.
### Recognition by Name
Consider the detection code for the Domyos Bike:
```
} else if (b.name().startsWith(QStringLiteral("Domyos-Bike")) &&
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) {
```
Reading this, to identify this device:
- bluetooth name should start with "Domyos-Bike" using a case sensitive comparison
- bluetooth name should NOT start with "DomyosBridge", also using a case sensitive comparison
- there should not have been a device using the corresponding device class detected already (i.e. domyos)
- filter has not been activated (this isn't tested)
In this case, we are not testing the last two, but can test the first two.
In deviceindex.h:
```
static const QString DomyosBike;
```
In deviceindex.cpp:
```
DEFINE_DEVICE(DomyosBike, "Domyos Bike");
```
This pair adds the "friendly name" for the device as a constant, and also adds the key/value pair to an index.
In DeviceTestDataIndex::Initialize():
```
// Domyos bike
RegisterNewDeviceTestData(DeviceIndex::DomyosBike)
->expectDevice<domyosbike>()
->acceptDeviceName("Domyos-Bike", DeviceNameComparison::StartsWith)
->rejectDeviceName("DomyosBridge", DeviceNameComparison::StartsWith);
```
This set of instructions adds a valid device name, and an invalid one. Various overloads of these methods, other methods, and other members of the comparison enumeration provide other capabilities for specifying test data. If you add a valid device name that says the name should start with a value, additional names will be added automatically to the valid list with additional characters to test that it is in fact a "starts with" relationship. Also, valid and invalid names will be generated based on whether the comparison is case sensitive or not.
### Configuration Settings
Consider the CompuTrainer bike. This device is not detected by name, but only by whether or not it is enabled in the settings.
To specify this in the test data, we use one of the BluetoothDeviceTestData::configureSettingsWith(...) methods, the one for the simple case where there is a single QZSetting with a specific enabling and disabling value.
Settings from QSettings that contribute to tests should be put into the DeviceDiscoveryInfo class.
For example, for the Computrainer Bike, the "computrainer_serialport" value from the QSettings determines if the bike should be detected or not.
The computrainer_serialport QZSettings key should be registered in devicediscoveryinfo.cpp
In devicediscoveryinfo.cpp:
```
void InitializeTrackedSettings() {
...
trackedSettings.insert(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport);
...
}
```
For this test data,
* if enabling configurations are requested, the computrainer_serialport setting will be populated with "COMX"
* if disabling configurations are requested, the computrainer_serialport setting will be populated with ""
DeviceTestDataIndex::Initialize():
```
// Computrainer Bike
RegisterNewDeviceTestData(DeviceIndex::ComputrainerBike)
->expectDevice<computrainerbike>()
->acceptDeviceName("", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::computrainer_serialport, "COMX", "");
```
Similarly, the Pafers Bike has a simple configuration setting:
```
// Pafers Bike
RegisterNewDeviceTestData(DeviceIndex::PafersBike)
->expectDevice<pafersbike>()
->acceptDeviceName("PAFERS_", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::pafers_treadmill,false);
```
In that case, ```configureSettingsWith(QZSettings::pafers_treadmill,false)``` indicates that the pafers_treadmill setting will be false for enabling configurations and true for disabling ones.
A more complicated example is the Pafers Treadmill. It involves a name match, but also some configuration settings obtained earlier...
```
bool pafers_treadmill = settings.value(QZSettings::pafers_treadmill, QZSettings::default_pafers_treadmill).toBool();
...
bool pafers_treadmill_bh_iboxster_plus =
settings
.value(QZSettings::pafers_treadmill_bh_iboxster_plus, QZSettings::default_pafers_treadmill_bh_iboxster_plus)
.toBool();
...
} else if (b.name().toUpper().startsWith(QStringLiteral("PAFERS_")) && !pafersTreadmill &&
(pafers_treadmill || pafers_treadmill_bh_iboxster_plus) && filter) {
```
Here the device could be activated due to a name match and various combinations of settings.
For this, the configureSettingsWith(...) function that takes a lambda function which consumes a vector of DeviceDiscoveryInfo objects which is populated with configurations that lead to the specified result (enable = detected, !enable=not detected).
```
// Pafers Treadmill
RegisterNewDeviceTestData(DeviceIndex::PafersTreadmill)
->expectDevice<paferstreadmill>()
->acceptDeviceName("PAFERS_", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith( [](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void {
DeviceDiscoveryInfo config(info);
if (enable) {
for(int x = 1; x<=3; x++) {
config.setValue(QZSettings::pafers_treadmill, x & 1);
config.setValue(QZSettings::pafers_treadmill_bh_iboxster_plus, x & 2);
configurations.push_back(config);
}
} else {
config.setValue(QZSettings::pafers_treadmill, false);
config.setValue(QZSettings::pafers_treadmill_bh_iboxster_plus, false);
configurations.push_back(config);
}
});
```
### Considering Extra QBluetoothDeviceInfo Content
Detection of some devices requires some specific bluetooth device information.
Supplying enabling and disabling QBluetoothDeviceInfo objects is done by accessing the QBluetoothDeviceInfo member of the DeviceDiscoveryInfo object.
For example, the M3iBike requires specific manufacturer information, using the simpler of the lambda functions accepted by the configureSettingsWith function.
```
// M3I Bike
RegisterNewDeviceTestData(DeviceIndex::M3IBike)
->expectDevice<m3ibike>()
->acceptDeviceName("M3", DeviceNameComparison::StartsWith)
->configureSettingsWith(
[](DeviceDiscoveryInfo& info, bool enable)->void
{
// The M3I bike detector looks into the manufacturer data.
if(!enable) {
info.DeviceInfo()->setManufacturerData(1, QByteArray("Invalid manufacturer data."));
return;
}
int key=0;
info.DeviceInfo()->setManufacturerData(key++, hex2bytes("02010639009F00000000000000000014008001"));
});
```
The test framework populates the incoming QBluetoothDeviceInfo object with a UUID and the name (generated from the acceptDeviceName and rejectDeviceName calls) currently being tested.
This is expected to have nothing else defined.
Another example is one of the test data definitions for detecting a device that uses the stagesbike class:
Detection code from bluetooth.cpp:
```
((b.name().toUpper().startsWith("KICKR CORE")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
```
This condition is actually extracted from a more complicated example where the BluetoothDeviceTestData class can't cover all the detection criteria with one instance.
```
// Stages Bike General
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
//
// ... other stages bike variants
//
// Stages Bike (KICKR CORE)
RegisterNewDeviceTestData(DeviceIndex::StagesBike_KICKRCORE)
->expectDevice<stagesbike>()
->acceptDeviceName("KICKR CORE", DeviceNameComparison::StartsWithIgnoreCase)
->excluding(stagesBikeExclusions)
->configureSettingsWith(
[](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void
{
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
if(enable) {
DeviceDiscoveryInfo result = info;
result.addBluetoothService(QBluetoothUuid((quint16)0x1818));
result.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(result);
} else {
DeviceDiscoveryInfo hasNeither = info;
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1818));
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasInvalid = info;
hasInvalid.addBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasBoth = hasInvalid;
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1818));
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(info); // has neither
configurations.push_back(hasInvalid);
configurations.push_back(hasBoth);
}
});
```
In this case, it populates the vector with the single enabling configuration if that's what's been requested, otherwise 3 disabling ones.
### Exclusions
Sometimes there might be ambiguity when multiple devices are available, and the detection code may specify that if the other conditions match, but certain specific kinds of devices (the exclusion devices) have already been detected, the newly matched device should be ignored.
The test data object can be made to cover this by calling the excluding(...) functions to add type identifiers for the bluetoothdevice classes for the exclusion devices to the object's internal list of exclusions.
Detection code:
```
} else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride &&
!echelonConnectSport && filter) {
```
The excluding<T>() template function is called to specify the exclusion device type. Note that the test for a previously detected device of the same type is not included.
```
// Echelon Connect Sport Bike
RegisterNewDeviceTestData(DeviceIndex::EchelonConnectSportBike)
->expectDevice<echelonconnectsport>()
->acceptDeviceName("ECH", DeviceNameComparison::StartsWith)
->excluding<echelonrower>()
->excluding<echelonstride>();
```
### When a single test data object can't cover all the conditions
Detection code:
```
QString powerSensorName =
settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name).toString();
...
} else if ((b.name().toUpper().startsWith(QStringLiteral("STAGES ")) ||
(b.name().toUpper().startsWith("TACX SATORI")) ||
((b.name().toUpper().startsWith("KICKR CORE")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818))) ||
(b.name().toUpper()==QStringLiteral("QD")) ||
(b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) &&
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
!stagesBike && !ftmsBike && filter) {
```
This presents 3 scenarios for the current test framework.
1. Match names only (starts with:"STAGES ", starts with: "TACX SATORI", equals: "QD")
2. Match the name "KICKR CORE", presence and absence of specific service ids
3. Match the name "ASSIOMA" and the power sensor name setting starts with "Disabled"
The framework is not currently capable of specifying all these scenarios in a single test data object, without checking the name of the supplied QBluetoothDeviceInfo object against name conditions specified and constructing extra configurations based on that.
The generated test data is approximately the combinations of these lists: names * settings * exclusions.
If a combination should not exist, separate test data objects should be used.
In the example of the Stages Bike test data, the exclusions, which apply to all situations, are implemented in an array of type ids:
```
// Stages Bike General
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
```
The name-match only in one test data instance:
```
// Stages Bike
RegisterNewDeviceTestData(DeviceIndex::StagesBike)
->expectDevice<stagesbike>()
->acceptDeviceNames({"STAGES ", "TACX SATORI"}, DeviceNameComparison::StartsWithIgnoreCase)
->acceptDeviceName("QD", DeviceNameComparison::IgnoreCase)
->excluding(stagesBikeExclusions);
```
The name and setting match in another instance:
```
// Stages Bike Stages Bike (Assioma / Power Sensor disabled
RegisterNewDeviceTestData(DeviceIndex::StagesBike_Assioma_PowerSensorDisabled)
->expectDevice<stagesbike>()
->acceptDeviceName("ASSIOMA", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::power_sensor_name, "DisabledX", "XDisabled")
->excluding( stagesBikeExclusions);
```
The name and bluetooth device info configurations in another:
```
// Stages Bike (KICKR CORE)
RegisterNewDeviceTestData(DeviceIndex::StagesBike_KICKRCORE)
->expectDevice<stagesbike>()
->acceptDeviceName("KICKR CORE", DeviceNameComparison::StartsWithIgnoreCase)
->excluding(stagesBikeExclusions)
->configureSettingsWith(
[](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void
{
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
if(enable) {
DeviceDiscoveryInfo result = info;
result.addBluetoothService(QBluetoothUuid((quint16)0x1818));
result.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(result);
} else {
DeviceDiscoveryInfo hasNeither = info;
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1818));
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasInvalid = info;
hasInvalid.addBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasBoth = hasInvalid;
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1818));
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(info); // has neither
configurations.push_back(hasInvalid);
configurations.push_back(hasBoth);
}
});
```
## Telling Google Test Where to Look
The BluetoothDeviceTestSuite configuration specifies that the test data will be obtained from the DeviceTestDataIndex class, so there's nothing more to do.

View File

@@ -1,188 +0,0 @@
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])

View File

@@ -1,331 +0,0 @@
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)

View File

@@ -1,143 +0,0 @@
import sys
import logging
import asyncio
import threading
import random
import struct
import binascii
from typing import Any, Union
from bless import (
BlessServer,
BlessGATTCharacteristic,
GATTCharacteristicProperties,
GATTAttributePermissions,
)
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(name=__name__)
trigger: Union[asyncio.Event, threading.Event]
if sys.platform in ["darwin", "win32"]:
trigger = threading.Event()
else:
trigger = asyncio.Event()
def read_request(characteristic: BlessGATTCharacteristic, **kwargs) -> bytearray:
logger.debug(f"Reading {characteristic.value}")
return characteristic.value
def write_request(characteristic: BlessGATTCharacteristic, value: Any, **kwargs):
characteristic.value = value
logger.debug(f"Char value set to {characteristic.value}")
if characteristic.value == b"\x0f":
logger.debug("NICE")
trigger.set()
def generate_indoor_bike_data():
# Flags (16 bits)
flags = (1 << 2) | (1 << 6) # Instantaneous Cadence and Instantaneous Power present
speed = random.randint(0, 20000) # 0-20000
# Instantaneous Cadence (uint16, 0.5 rpm resolution)
cadence = random.randint(0, 400) # 0-200 rpm
# Instantaneous Power (sint16, watts)
power = random.randint(10, 50)
# Pack data into bytes
data = struct.pack("<HHHh", flags, speed, cadence, power)
return data
def generate_zwift_ride_data():
data_str = "2308ffbfffff0f1a04080010001a04080110001a04080210001a0408031000"
data = binascii.unhexlify(data_str)
return data
async def update_indoor_bike_data(server, service_uuid, char_uuid):
while True:
c = server.get_characteristic(char_uuid)
c.value = bytes(generate_indoor_bike_data())
server.update_value(service_uuid, char_uuid)
await asyncio.sleep(1)
async def update_zwift_ride_data(server, service_uuid, char_uuid):
while True:
c = server.get_characteristic(char_uuid)
c.value = bytes(generate_zwift_ride_data())
server.update_value(service_uuid, char_uuid)
await asyncio.sleep(1)
async def run(loop):
trigger.clear()
# Instantiate the server
server = BlessServer(name="FTMS Indoor Bike", loop=loop)
server.read_request_func = read_request
server.write_request_func = write_request
# Add Fitness Machine Service
ftms_uuid = "00001826-0000-1000-8000-00805f9b34fb"
await server.add_new_service(ftms_uuid)
# Add Indoor Bike Data Characteristic
indoor_bike_data_uuid = "00002ad2-0000-1000-8000-00805f9b34fb"
char_flags = (
GATTCharacteristicProperties.read
| GATTCharacteristicProperties.notify
)
permissions = GATTAttributePermissions.readable
await server.add_new_characteristic(
ftms_uuid, indoor_bike_data_uuid, char_flags, generate_indoor_bike_data(), permissions
)
zwift_ride_uuid = "00000001-19ca-4651-86e5-fa29dcdd09d1"
await server.add_new_service(zwift_ride_uuid)
syncRxChar = "00000003-19CA-4651-86E5-FA29DCDD09D1"
syncRx_flags = (
GATTCharacteristicProperties.write
)
syncRx_permissions = GATTAttributePermissions.writeable
syncTxChar = "00000004-19CA-4651-86E5-FA29DCDD09D1"
syncTx_flags = (
GATTCharacteristicProperties.read
| GATTCharacteristicProperties.indicate
)
syncTx_permissions = GATTAttributePermissions.readable
asyncChar = "00000002-19CA-4651-86E5-FA29DCDD09D1"
async_flags = (
GATTCharacteristicProperties.read
| GATTCharacteristicProperties.notify
)
async_permissions = GATTAttributePermissions.readable
await server.add_new_characteristic(
zwift_ride_uuid, syncRxChar, syncRx_flags, generate_indoor_bike_data(), syncRx_permissions
)
await server.add_new_characteristic(
zwift_ride_uuid, syncTxChar, syncTx_flags, generate_indoor_bike_data(), syncTx_permissions
)
await server.add_new_characteristic(
zwift_ride_uuid, asyncChar, async_flags, generate_zwift_ride_data(), async_permissions
)
logger.debug(server.get_characteristic(indoor_bike_data_uuid))
await server.start()
logger.debug("Advertising")
logger.info(f"FTMS Indoor Bike is now advertising")
# Start updating the indoor bike data
update_task = asyncio.create_task(update_indoor_bike_data(server, ftms_uuid, indoor_bike_data_uuid))
update_task_zwift_ride = asyncio.create_task(update_zwift_ride_data(server, zwift_ride_uuid, asyncChar))
await asyncio.sleep(99999999)
await server.stop()
loop = asyncio.get_event_loop()
loop.run_until_complete(run(loop))

View File

@@ -1,28 +0,0 @@
import json
def generate_code(hex_string, start_index):
hex_pairs = [hex_string[i:i+2] for i in range(0, len(hex_string), 2)]
output = ""
array_name = f"initData{start_index}"
array_elements = ', '.join([f"0x{hex_pair}" for hex_pair in hex_pairs])
output += f"uint8_t {array_name}[] = {{{array_elements}}};\n"
output += f'writeCharacteristic({array_name}, sizeof({array_name}), QStringLiteral("init"), false, false);\n'
output += "QThread::msleep(sleepms);\n\n"
return output
json_file_path = "C:\\Work\\qdomyos-zwift\\helpers\\tmp.json"
with open(json_file_path, 'r') as file:
# Carica i dati JSON
json_data = json.load(file)
line = 0
for item in json_data:
try:
if(item['_source']['layers']['btatt']['btatt.value_raw'][0] != ''):
line = line + 1
print(generate_code(item['_source']['layers']['btatt']['btatt.value_raw'][0], line))
except:
pass

View File

@@ -1,37 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"files.associations": {
"list": "cpp",
"chrono": "cpp",
"complex": "cpp",
"functional": "cpp",
"optional": "cpp",
"system_error": "cpp",
"type_traits": "cpp",
"xlocnum": "cpp",
"xtr1common": "cpp",
"qhttpserver": "cpp",
"array": "cpp",
"deque": "cpp",
"map": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"xstring": "cpp",
"algorithm": "cpp",
"xutility": "cpp",
"xlocale": "cpp",
"filesystem": "cpp",
"bitset": "cpp",
"iterator": "cpp",
"xhash": "cpp",
"xtree": "cpp",
"ostream": "cpp",
"locale": "cpp"
}
}
}

View File

@@ -329,7 +329,6 @@ public slots:
descData.uuid = QBluetoothUuid(descriptorUuid);
charData.descriptorList.insert(descHandle, descData);
if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) {
mIndicateChars << charData.uuid;
ComPtr<IAsyncOperation<ClientCharConfigDescriptorResult *>> readOp;
hr = characteristic->ReadClientCharacteristicConfigurationDescriptorAsync(&readOp);
WARN_AND_CONTINUE_IF_FAILED(hr, "Could not read descriptor value")
@@ -357,7 +356,7 @@ public slots:
descData.value = QByteArray(2, Qt::Uninitialized);
qToLittleEndian(result, descData.value.data());
mIndicateChars << charData.uuid;
} else {
ComPtr<IAsyncOperation<GattReadResult *>> readOp;
hr = descriptor->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached,
@@ -613,7 +612,6 @@ HRESULT QLowEnergyControllerPrivateWinRTNew::onValueChange(IGattCharacteristic *
{
HRESULT hr;
quint16 handle;
qCDebug(QT_BT_WINRT) << __FUNCTION__ << characteristic;
hr = characteristic->get_AttributeHandle(&handle);
RETURN_IF_FAILED("Could not obtain characteristic's handle", return S_OK)
ComPtr<IBuffer> buffer;

View File

@@ -10,87 +10,59 @@ ColumnLayout {
property alias textFont: accordionText.font.family
property alias textFontSize: accordionText.font.pixelSize
property alias indicatRectColor: indicatRect.color
default property alias accordionContent: contentLoader.sourceComponent
// Signal emitted when content becomes visible
signal contentBecameVisible()
default property alias accordionContent: contentPlaceholder.data
spacing: 0
Layout.fillWidth: true
Layout.fillWidth: true;
Rectangle {
id: accordionHeader
color: "red"
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Layout.fillWidth: true;
height: 48
Rectangle {
id: indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
Rectangle{
id:indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
}
Text {
id: accordionText
x: 34; y: 13
x:34;y:13
color: "#FFFFFF"
text: rootElement.title
}
Image {
y: 13
anchors.right: parent.right
y:13
anchors.right: parent.right
anchors.rightMargin: 20
width: 30; height: 30
id: indicatImg
source: "qrc:/icons/arrow-collapse-vertical.png"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
rootElement.isOpen = !rootElement.isOpen
if(rootElement.isOpen) {
if(rootElement.isOpen)
{
indicatImg.source = "qrc:/icons/arrow-expand-vertical.png"
} else {
}else{
indicatImg.source = "qrc:/icons/arrow-collapse-vertical.png"
}
}
}
}
// Loader with enhanced visibility handling
Loader {
id: contentLoader
active: rootElement.isOpen
visible: false // Start invisible
Layout.fillWidth: true
asynchronous: false
onLoaded: {
if (item) {
item.Layout.fillWidth = true
visible = true
rootElement.contentBecameVisible()
}
}
// Handle visibility changes
onVisibleChanged: {
if (visible && status === Loader.Ready) {
rootElement.contentBecameVisible()
}
}
}
// Handle accordion closing
onIsOpenChanged: {
if (!isOpen) {
contentLoader.visible = false
}
// This will get filled with the content
ColumnLayout {
id: contentPlaceholder
visible: rootElement.isOpen
Layout.fillWidth: true;
}
}

View File

@@ -5,29 +5,26 @@
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>ConnectIQ.framework/ConnectIQ</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-armv7_arm64</string>
<key>LibraryPath</key>
<string>ConnectIQ.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>armv7</string>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>ConnectIQ.framework/ConnectIQ</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-i386_x86_64-simulator</string>
<key>LibraryPath</key>
<string>ConnectIQ.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>i386</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>

View File

@@ -6,9 +6,9 @@
//
#import <Foundation/Foundation.h>
#import <ConnectIQ/IQConstants.h>
#import <ConnectIQ/IQDevice.h>
#import <ConnectIQ/IQApp.h>
#import "IQConstants.h"
#import "IQDevice.h"
#import "IQApp.h"
// --------------------------------------------------------------------------------
#pragma mark - PUBLIC TYPES

View File

@@ -6,8 +6,8 @@
//
#import <Foundation/Foundation.h>
#import <ConnectIQ/IQDevice.h>
#import <ConnectIQ/IQAppStatus.h>
#import "IQDevice.h"
#import "IQAppStatus.h"
/// @brief Represents an instance of a ConnectIQ app that is installed on a
/// Garmin device.

View File

@@ -13,9 +13,6 @@ extern int const IQSDKVersion;
/// @brief The bundle identifier for the Garmin Connect Mobile app.
extern NSString * const IQGCMBundle;
/// @brief The bundle identifier for the Garmin Connect Mobile Beta app.
extern NSString * const IQGCMInternalBetaBundle;
/// @brief The result of a SendMessage operation
typedef NS_ENUM(NSInteger, IQSendMessageResult){
///! @brief The message was sent successfully.

View File

@@ -6,9 +6,9 @@
//
#import <Foundation/Foundation.h>
#import <ConnectIQ/IQConstants.h>
#import <ConnectIQ/IQDevice.h>
#import <ConnectIQ/IQApp.h>
#import "IQConstants.h"
#import "IQDevice.h"
#import "IQApp.h"
// --------------------------------------------------------------------------------
#pragma mark - PUBLIC TYPES

View File

@@ -6,8 +6,8 @@
//
#import <Foundation/Foundation.h>
#import <ConnectIQ/IQDevice.h>
#import <ConnectIQ/IQAppStatus.h>
#import "IQDevice.h"
#import "IQAppStatus.h"
/// @brief Represents an instance of a ConnectIQ app that is installed on a
/// Garmin device.

View File

@@ -13,9 +13,6 @@ extern int const IQSDKVersion;
/// @brief The bundle identifier for the Garmin Connect Mobile app.
extern NSString * const IQGCMBundle;
/// @brief The bundle identifier for the Garmin Connect Mobile Beta app.
extern NSString * const IQGCMInternalBetaBundle;
/// @brief The result of a SendMessage operation
typedef NS_ENUM(NSInteger, IQSendMessageResult){
///! @brief The message was sent successfully.

View File

@@ -6,11 +6,11 @@
<dict>
<key>Headers/ConnectIQ.h</key>
<data>
yih4e2KjbC/GqavxdCZ3xQ4mHmA=
F1hICh90Ex4ADEjYLcSi0YPhrPA=
</data>
<key>Headers/IQApp.h</key>
<data>
NDlj8k5C84UPFmD+qEMz2WcZloY=
R7+SmeArgBACIBWHRnEAugyFHKE=
</data>
<key>Headers/IQAppStatus.h</key>
<data>
@@ -18,7 +18,7 @@
</data>
<key>Headers/IQConstants.h</key>
<data>
z5FAXaGG7RDVUTai1Vvqs33zc98=
eI7keKSkaajUZACnuMhgtV1RuBA=
</data>
<key>Headers/IQDevice.h</key>
<data>
@@ -26,11 +26,11 @@
</data>
<key>Info.plist</key>
<data>
YUOCJU/YBLc4CRWV1z8JHDjCx8M=
sMY09qXRBL/m1OGNWejLjfNg04w=
</data>
<key>Modules/module.modulemap</key>
<data>
eEyhq/G44PBlD3KiydN8B1vbfCU=
SSRVAtIAdFmowQqE4HzOpWYLubg=
</data>
<key>ar.lproj/IQLocalizable.strings</key>
<dict>
@@ -298,20 +298,32 @@
<dict>
<key>Headers/ConnectIQ.h</key>
<dict>
<key>hash</key>
<data>
F1hICh90Ex4ADEjYLcSi0YPhrPA=
</data>
<key>hash2</key>
<data>
kAenemss8n98vVLi54JqBUtGwaL1/i+HSejFBZgawHA=
ABtgvHbvmly4QpZO/KmmrwYkL0N+AqV3gXdPVrseysY=
</data>
</dict>
<key>Headers/IQApp.h</key>
<dict>
<key>hash</key>
<data>
R7+SmeArgBACIBWHRnEAugyFHKE=
</data>
<key>hash2</key>
<data>
bSRRooQ0FKFr3BgrFolAnkU402889YFHrH+6EEca3cg=
X4vXt0sO9gxQNzQalIaLqMpSGNRC9ue2USDcfjBYkec=
</data>
</dict>
<key>Headers/IQAppStatus.h</key>
<dict>
<key>hash</key>
<data>
WnybOSMMVqCKGns0rEz9C3EfQOg=
</data>
<key>hash2</key>
<data>
tg9qNXtTmFUvNoJtq7O/aEXBNngcGENVRhvxLJ8C/xo=
@@ -319,13 +331,21 @@
</dict>
<key>Headers/IQConstants.h</key>
<dict>
<key>hash</key>
<data>
eI7keKSkaajUZACnuMhgtV1RuBA=
</data>
<key>hash2</key>
<data>
qVLQDlPhVsyAAQ/LCGOCdEOUaabcgwTHijMQiuWbAXM=
bqDpm8yikc2FIqaSUHcLqPY6TPXLlXSUo+Dl9NUYwmA=
</data>
</dict>
<key>Headers/IQDevice.h</key>
<dict>
<key>hash</key>
<data>
bl545C/cu0mw2KlRmzojKmHPom0=
</data>
<key>hash2</key>
<data>
4N4+64IHeb9iBwyziNxo0SMuCM75ez9Em4UfmtgtTHA=
@@ -333,13 +353,21 @@
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash</key>
<data>
SSRVAtIAdFmowQqE4HzOpWYLubg=
</data>
<key>hash2</key>
<data>
6a9Ehz1N4Sm/6qBlTfQpHUqRlpzQr2JMF26AfW4xUtY=
lQGjVO5Q0wfztjETCwDkwAkQ7nZInCgWdStnHL3o6Co=
</data>
</dict>
<key>ar.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
1CDTE/Qaf1Z/HuhSt9CUnwitv4M=
</data>
<key>hash2</key>
<data>
CWyQue2TCS0heGoGbN4ffetM2QZSk7lqgc2Wer2fgTg=
@@ -349,6 +377,10 @@
</dict>
<key>cs.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
/jkyQ77G2Xd9wy6QptBphGNbtCY=
</data>
<key>hash2</key>
<data>
1mSn+EYeYcTV1dArgHz7PkmZrV6mHWfnuG5aDa6Y87E=
@@ -358,6 +390,10 @@
</dict>
<key>da.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
FYi0wjOu/Hw//Qe96yqxSb9yClc=
</data>
<key>hash2</key>
<data>
yLkvGzd+smkOjicvW/+Oe6wGGyirHS+/YfjuSzyVoMM=
@@ -367,6 +403,10 @@
</dict>
<key>de.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
MitzVbGhXhTLjPvw9vuWcQQa50Q=
</data>
<key>hash2</key>
<data>
DFHv7MWBJmyAkOj993NmSFKbS2t8/vtSev603sBUtjI=
@@ -376,6 +416,10 @@
</dict>
<key>el.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
n82gLcjjjHszaroTFeJUvSrrc0o=
</data>
<key>hash2</key>
<data>
i4FAK4mi+SgS6oZv8zM74kRZToakn49E8GD7FcJBLoQ=
@@ -385,6 +429,10 @@
</dict>
<key>en.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
hcxxLyrTI+aElXlPc5dwr7jdqwc=
</data>
<key>hash2</key>
<data>
vmBi9DFJzFcG0OwaWKSDjgklNi407U8u2pz3EnEENN4=
@@ -394,6 +442,10 @@
</dict>
<key>es.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
ff8DVQtNhO8pF7HFnXjh8foHXbo=
</data>
<key>hash2</key>
<data>
z6RjynaWjrRKHmv4sLirc4eXwKOtQdylzj5+TiHpaTc=
@@ -403,6 +455,10 @@
</dict>
<key>fi.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
R9cr8yqJmu91Xz31tGyprGR3t/s=
</data>
<key>hash2</key>
<data>
6BI0iPRVWaP63/XFdjLBz6z7DsvvuOoaEAS+mYzrx8E=
@@ -412,6 +468,10 @@
</dict>
<key>fr.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
PwFmqFeRTcjdHmkXYrPzNVYoe5o=
</data>
<key>hash2</key>
<data>
geXjZzXre2CRiALecPFBGz4JSJA7MbkDnB4qrEMKNwk=
@@ -421,6 +481,10 @@
</dict>
<key>he.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
/jPUgFtYbbyELG5DZ3Sjoi/If9w=
</data>
<key>hash2</key>
<data>
47mcrSx16SFjWPIiN7guCAG0va8NiJ6I5s45tSVEHlY=
@@ -430,6 +494,10 @@
</dict>
<key>hr.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
H2GtdTeORRPCnogvpWY69Dg9uME=
</data>
<key>hash2</key>
<data>
4bQvygPax6VBpoFlyS5by1N6otnDMliHu+bWsDaWSQc=
@@ -439,6 +507,10 @@
</dict>
<key>hu.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
QIimMhNyYmqp4ZW01hfj554WAMg=
</data>
<key>hash2</key>
<data>
0m2fIyz26vh3RlUqqSXvoNTLovxIixrUyJoL/IDSoVk=
@@ -448,6 +520,10 @@
</dict>
<key>id.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
2/54a0gkcVuk1I3m4ulDAXOLL5o=
</data>
<key>hash2</key>
<data>
hQf9SrG7d8aVWsXIbCIxkKEJjbnW1FLvS+MbOI1VtHQ=
@@ -457,6 +533,10 @@
</dict>
<key>it.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
hNIKYIcP/87e6g7AUP+zKRtJ52M=
</data>
<key>hash2</key>
<data>
XAbEWX6cicDxGzxGgSx3DhF4rjUHX4LV+dO0X3rUEqc=
@@ -466,6 +546,10 @@
</dict>
<key>ja.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
0iU2PbJ/3xgXMZ20ffsqaWpxKWc=
</data>
<key>hash2</key>
<data>
YOqOvZq0WEN4DCoSwc0lcTSRc4C812DqzjIsaid1SHg=
@@ -475,6 +559,10 @@
</dict>
<key>ko.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
ERH8oHR9H9jMHjP0EAgaTtVhnX4=
</data>
<key>hash2</key>
<data>
WJyaRCWn1KqmcDeajRnC41MdNrlpbI+1JbPkXhbKrKY=
@@ -484,6 +572,10 @@
</dict>
<key>ms.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
DkbQA2+v/qSgQWma/fg3647Bkqs=
</data>
<key>hash2</key>
<data>
gztYxa4Hn58HkKmcUIZI1jCz44IETZeMsqrpZSKxJvc=
@@ -493,6 +585,10 @@
</dict>
<key>nb.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
T3zFOvuvrJt5Vnmfqt2Mf/du8as=
</data>
<key>hash2</key>
<data>
Oy6UOwSN+/xPIrthAEvzV8PEn27kfsHpMMLU5w1rww0=
@@ -502,6 +598,10 @@
</dict>
<key>nl.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
t9PD5JEbfoSLaQ7f8M2cLghOReI=
</data>
<key>hash2</key>
<data>
XbijhSaZgmsW59Vo9ZEbhDuUQH18fHizWKzsLosiM0o=
@@ -511,6 +611,10 @@
</dict>
<key>pl.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
wfTnhBccAm6JfwH/JkZKNRKTUAU=
</data>
<key>hash2</key>
<data>
MQYgqA+Hl03JJ261Q19K5Lt64kSTBP+pfpD+jOVE3AU=
@@ -520,6 +624,10 @@
</dict>
<key>pt-PT.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
7yXkcZEpJ4UiRHAzhK+vw/Q857Y=
</data>
<key>hash2</key>
<data>
seINq3QazVameLGOW+pIAtGWLa6NDl5XWRtqnObxywo=
@@ -529,6 +637,10 @@
</dict>
<key>pt.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
tZPncsQs8weCDJa03AKLpijXSUw=
</data>
<key>hash2</key>
<data>
GnzdqEuQwORzVCih99bwr79UHIyzXm+zuN5b9m1NrKY=
@@ -538,6 +650,10 @@
</dict>
<key>ru.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
Ct+byJ3rWeigvg0q6rB/kQaR+yE=
</data>
<key>hash2</key>
<data>
yCN9s/JXYqsMNZ1icaH4hUwyMQ1NtxOmV6sIAtRd9pc=
@@ -547,6 +663,10 @@
</dict>
<key>sk.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
1yTM1nAsAYpSH7NrYU6/nFlqk5E=
</data>
<key>hash2</key>
<data>
OFHDtkGLLSfTuSx8GOTycKDCKOKmX0Wh2QG1CHhRz3I=
@@ -556,6 +676,10 @@
</dict>
<key>sv.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
i84z6vuHLrFpO0qZ2V0zYjixIws=
</data>
<key>hash2</key>
<data>
a3Gk+3USOT5uundOXrNCgnbcD0rDo2lkCO7b7+zg2Is=
@@ -565,6 +689,10 @@
</dict>
<key>th.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
oW5npy+pDJM1wUOgTkw9FY1Ave4=
</data>
<key>hash2</key>
<data>
qxGqAqRMwm0/dMd0W7DUsvbWb9x65GT+3d1zOQEql1w=
@@ -574,6 +702,10 @@
</dict>
<key>tr.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
76rD7PLrQMiT5YTlI8IjEFgsiU4=
</data>
<key>hash2</key>
<data>
Y6TnKQmqO/TAx+0KYqRRG6UOz7I/gM1YmbUwgSfZSQU=
@@ -583,6 +715,10 @@
</dict>
<key>zh-Hans.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
DNlMxUKypOvKArzi7ioJUiFfFXg=
</data>
<key>hash2</key>
<data>
BI3m4MTMHuPI4sQKPGeQnxIlBJJrXwgVuR7Ho1Q5o6Y=
@@ -592,6 +728,10 @@
</dict>
<key>zh-Hant.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
U6I+uL07KIv2b77w0c0glaJlhMg=
</data>
<key>hash2</key>
<data>
14dQnjX3pEz2Um4J/fOdQDRe/LSuXxqkg1hEkO8E5ys=

Some files were not shown because too many files have changed in this diff Show More